Merge pull request #1899 from Bnyro/bookmarks

Bookmark playlists
This commit is contained in:
Bnyro 2022-11-18 18:31:15 +01:00 committed by GitHub
commit 130d9167bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 653 additions and 14 deletions

View File

@ -18,6 +18,12 @@ android {
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
resValue "string", "app_name", "LibreTube" resValue "string", "app_name", "LibreTube"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
} }
buildFeatures { buildFeatures {

View File

@ -0,0 +1,174 @@
{
"formatVersion": 1,
"database": {
"version": 7,
"identityHash": "c9803a67ce206dbda6e44ed761f80136",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9803a67ce206dbda6e44ed761f80136')"
]
}
}

View File

@ -0,0 +1,224 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "eb8d0ff1131448df6216b549bbfa7c21",
"entities": [
{
"tableName": "watchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "uploadDate",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "watchPosition",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
"fields": [
{
"fieldPath": "videoId",
"columnName": "videoId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistoryItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
"fields": [
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "customInstance",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "apiUrl",
"columnName": "apiUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "frontendUrl",
"columnName": "frontendUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "localSubscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
"fields": [
{
"fieldPath": "channelId",
"columnName": "channelId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "playlistBookmark",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, PRIMARY KEY(`playlistId`))",
"fields": [
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistName",
"columnName": "playlistName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploaderUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploaderAvatar",
"columnName": "uploaderAvatar",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"playlistId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eb8d0ff1131448df6216b549bbfa7c21')"
]
}
}

View File

@ -1,14 +1,17 @@
package com.github.libretube.db package com.github.libretube.db
import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.github.libretube.db.dao.CustomInstanceDao import com.github.libretube.db.dao.CustomInstanceDao
import com.github.libretube.db.dao.LocalSubscriptionDao import com.github.libretube.db.dao.LocalSubscriptionDao
import com.github.libretube.db.dao.PlaylistBookmarkDao
import com.github.libretube.db.dao.SearchHistoryDao import com.github.libretube.db.dao.SearchHistoryDao
import com.github.libretube.db.dao.WatchHistoryDao import com.github.libretube.db.dao.WatchHistoryDao
import com.github.libretube.db.dao.WatchPositionDao import com.github.libretube.db.dao.WatchPositionDao
import com.github.libretube.db.obj.CustomInstance import com.github.libretube.db.obj.CustomInstance
import com.github.libretube.db.obj.LocalSubscription import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition import com.github.libretube.db.obj.WatchPosition
@ -19,9 +22,13 @@ import com.github.libretube.db.obj.WatchPosition
WatchPosition::class, WatchPosition::class,
SearchHistoryItem::class, SearchHistoryItem::class,
CustomInstance::class, CustomInstance::class,
LocalSubscription::class LocalSubscription::class,
PlaylistBookmark::class
], ],
version = 7 version = 8,
autoMigrations = [
AutoMigration(from = 7, to = 8)
]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
/** /**
@ -48,4 +55,9 @@ abstract class AppDatabase : RoomDatabase() {
* Local Subscriptions * Local Subscriptions
*/ */
abstract fun localSubscriptionDao(): LocalSubscriptionDao abstract fun localSubscriptionDao(): LocalSubscriptionDao
/**
* Bookmarked Playlists
*/
abstract fun playlistBookmarkDao(): PlaylistBookmarkDao
} }

View File

@ -0,0 +1,29 @@
package com.github.libretube.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.github.libretube.db.obj.PlaylistBookmark
@Dao
interface PlaylistBookmarkDao {
@Query("SELECT * FROM playlistBookmark")
fun getAll(): List<PlaylistBookmark>
@Query("SELECT * FROM playlistBookmark WHERE playlistId LIKE :playlistId LIMIT 1")
fun findById(playlistId: String): PlaylistBookmark
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg bookmarks: PlaylistBookmark)
@Delete
fun delete(playlistBookmark: PlaylistBookmark)
@Query("SELECT EXISTS(SELECT * FROM playlistBookmark WHERE playlistId= :playlistId)")
fun includes(playlistId: String): Boolean
@Query("DELETE FROM playlistBookmark")
fun deleteAll()
}

View File

@ -0,0 +1,16 @@
package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "playlistBookmark")
data class PlaylistBookmark(
@PrimaryKey
val playlistId: String = "",
val playlistName: String? = null,
var thumbnailUrl: String? = null,
var uploader: String? = null,
var uploaderUrl: String? = null,
var uploaderAvatar: String? = null
)

View File

@ -0,0 +1,49 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.PlaylistBookmarkRowBinding
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistBookmarkViewHolder
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class PlaylistBookmarkAdapter(
private val bookmarks: List<PlaylistBookmark>
) : RecyclerView.Adapter<PlaylistBookmarkViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaylistBookmarkViewHolder {
val binding = PlaylistBookmarkRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PlaylistBookmarkViewHolder(binding)
}
override fun getItemCount(): Int {
return bookmarks.size
}
override fun onBindViewHolder(holder: PlaylistBookmarkViewHolder, position: Int) {
val bookmark = bookmarks[position]
holder.binding.apply {
ImageHelper.loadImage(bookmark.thumbnailUrl, thumbnail)
playlistName.text = bookmark.playlistName
uploaderName.text = bookmark.uploader
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, bookmark.playlistId, false)
}
root.setOnLongClickListener {
PlaylistOptionsBottomSheet(
playlistId = bookmark.playlistId,
playlistName = bookmark.playlistName ?: "",
isOwner = false
).show(
(root.context as AppCompatActivity).supportFragmentManager
)
true
}
}
}
}

View File

@ -13,7 +13,10 @@ import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.adapters.PlaylistsAdapter import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
@ -111,6 +114,18 @@ class HomeFragment : BaseFragment() {
}) })
} }
} }
runOrError {
val bookmarkedPlaylists = awaitQuery {
DatabaseHolder.Database.playlistBookmarkDao().getAll()
}
if (bookmarkedPlaylists.isEmpty()) return@runOrError
runOnUiThread {
makeVisible(binding.bookmarksTV, binding.bookmarksRV)
binding.bookmarksRV.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.bookmarksRV.adapter = PlaylistBookmarkAdapter(bookmarkedPlaylists)
}
}
} }
private fun runOrError(action: suspend () -> Unit) { private fun runOrError(action: suspend () -> Unit) {

View File

@ -14,13 +14,14 @@ import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.FragmentPlaylistBinding import com.github.libretube.databinding.FragmentPlaylistBinding
import com.github.libretube.enums.ShareObjectType import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.obj.ShareData
import com.github.libretube.ui.adapters.PlaylistAdapter import com.github.libretube.ui.adapters.PlaylistAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper import com.github.libretube.util.NavigationHelper
@ -37,6 +38,7 @@ class PlaylistFragment : BaseFragment() {
private var nextPage: String? = null private var nextPage: String? = null
private var playlistAdapter: PlaylistAdapter? = null private var playlistAdapter: PlaylistAdapter? = null
private var isLoading = true private var isLoading = true
private var isBookmarked = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -62,11 +64,24 @@ class PlaylistFragment : BaseFragment() {
binding.playlistRecView.layoutManager = LinearLayoutManager(context) binding.playlistRecView.layoutManager = LinearLayoutManager(context)
binding.playlistProgress.visibility = View.VISIBLE binding.playlistProgress.visibility = View.VISIBLE
isBookmarked = awaitQuery {
DatabaseHolder.Database.playlistBookmarkDao().includes(playlistId!!)
}
updateBookmarkRes()
fetchPlaylist() fetchPlaylist()
} }
private fun updateBookmarkRes() {
binding.bookmark.setIconResource(
if (isBookmarked) R.drawable.ic_bookmark else R.drawable.ic_bookmark_outlined
)
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun fetchPlaylist() { private fun fetchPlaylist() {
binding.playlistScrollview.visibility = View.GONE
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
// load locally stored playlists with the auth api // load locally stored playlists with the auth api
@ -83,6 +98,7 @@ class PlaylistFragment : BaseFragment() {
Log.e(TAG(), "HttpException, unexpected response") Log.e(TAG(), "HttpException, unexpected response")
return@launchWhenCreated return@launchWhenCreated
} }
binding.playlistScrollview.visibility = View.VISIBLE
nextPage = response.nextpage nextPage = response.nextpage
playlistName = response.name playlistName = response.name
isLoading = false isLoading = false
@ -114,12 +130,29 @@ class PlaylistFragment : BaseFragment() {
) )
} }
binding.share.setOnClickListener { if (isOwner) binding.bookmark.visibility = View.GONE
ShareDialog(
playlistId!!, binding.bookmark.setOnClickListener {
ShareObjectType.PLAYLIST, isBookmarked = !isBookmarked
ShareData(currentPlaylist = response.name) updateBookmarkRes()
).show(childFragmentManager, null) query {
if (!isBookmarked) {
DatabaseHolder.Database.playlistBookmarkDao().delete(
DatabaseHolder.Database.playlistBookmarkDao().findById(playlistId!!)
)
} else {
DatabaseHolder.Database.playlistBookmarkDao().insertAll(
PlaylistBookmark(
playlistId = playlistId!!,
playlistName = response.name,
thumbnailUrl = response.thumbnailUrl,
uploader = response.uploader,
uploaderAvatar = response.uploaderAvatar,
uploaderUrl = response.uploaderUrl
)
)
}
}
} }
playlistAdapter = PlaylistAdapter( playlistAdapter = PlaylistAdapter(

View File

@ -0,0 +1,8 @@
package com.github.libretube.ui.viewholders
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.PlaylistBookmarkRowBinding
class PlaylistBookmarkViewHolder(
val binding: PlaylistBookmarkRowBinding
) : RecyclerView.ViewHolder(binding.root)

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

View File

@ -59,6 +59,18 @@
</RelativeLayout> </RelativeLayout>
<TextView
android:id="@+id/bookmarksTV"
style="@style/HomeCategoryTitle"
android:text="@string/bookmarks" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmarksRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/playlistsTV" android:id="@+id/playlistsTV"
style="@style/HomeCategoryTitle" style="@style/HomeCategoryTitle"

View File

@ -88,14 +88,14 @@
app:icon="@drawable/ic_playlist" /> app:icon="@drawable/ic_playlist" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/share" android:id="@+id/bookmark"
style="@style/Widget.Material3.Button" style="@style/Widget.Material3.Button"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp" android:layout_marginHorizontal="10dp"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/share" android:text="@string/bookmark"
app:icon="@drawable/ic_share_outlined" /> app:icon="@drawable/ic_bookmark_outlined" />
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/rounded_ripple"
android:orientation="vertical"
android:padding="5dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Medium"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/playlistName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:ellipsize="end"
android:maxLines="1"
android:textStyle="bold" />
<TextView
android:id="@+id/uploaderName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp" />
</LinearLayout>

View File

@ -381,6 +381,9 @@
<string name="trends">Trends</string> <string name="trends">Trends</string>
<string name="featured">Featured</string> <string name="featured">Featured</string>
<string name="trending">What\'s trending now</string> <string name="trending">What\'s trending now</string>
<string name="bookmarks">Bookmarks</string>
<string name="bookmark">Bookmark</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>
<string name="download_channel_description">Shows a notification when downloading media.</string> <string name="download_channel_description">Shows a notification when downloading media.</string>