From a2117bd74b45e11d3a44ed789332250750cc05b9 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 28 Mar 2023 12:16:47 +0200 Subject: [PATCH 1/4] Subscription groups --- .../11.json | 496 ++++++++++++++++++ .../com/github/libretube/db/AppDatabase.kt | 15 +- .../com/github/libretube/db/Converters.kt | 9 + .../libretube/db/dao/SubscriptionGroupsDao.kt | 23 + .../libretube/db/obj/SubscriptionGroup.kt | 10 + .../SubscriptionGroupChannelsAdapter.kt | 42 ++ .../ui/adapters/SubscriptionGroupsAdapter.kt | 61 +++ .../ui/dialogs/ChannelGroupsDialog.kt | 46 ++ .../ui/dialogs/EditChannelGroupDialog.kt | 80 +++ .../ui/fragments/SubscriptionsFragment.kt | 86 ++- .../SubscriptionGroupChannelRowViewHolder.kt | 8 + .../SubscriptionGroupsViewHolder.kt | 8 + app/src/main/res/drawable/ic_edit.xml | 10 + .../res/layout/dialog_edit_channel_group.xml | 58 ++ .../res/layout/dialog_subscription_groups.xml | 12 + .../res/layout/fragment_subscriptions.xml | 41 ++ .../layout/subscription_group_channel_row.xml | 34 ++ .../res/layout/subscription_group_row.xml | 35 ++ app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/style.xml | 9 + 20 files changed, 1066 insertions(+), 21 deletions(-) create mode 100644 app/schemas/com.github.libretube.db.AppDatabase/11.json create mode 100644 app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt create mode 100644 app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt create mode 100644 app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupChannelsAdapter.kt create mode 100644 app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt create mode 100644 app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt create mode 100644 app/src/main/java/com/github/libretube/ui/dialogs/EditChannelGroupDialog.kt create mode 100644 app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupChannelRowViewHolder.kt create mode 100644 app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupsViewHolder.kt create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/layout/dialog_edit_channel_group.xml create mode 100644 app/src/main/res/layout/dialog_subscription_groups.xml create mode 100644 app/src/main/res/layout/subscription_group_channel_row.xml create mode 100644 app/src/main/res/layout/subscription_group_row.xml diff --git a/app/schemas/com.github.libretube.db.AppDatabase/11.json b/app/schemas/com.github.libretube.db.AppDatabase/11.json new file mode 100644 index 000000000..f163f64e5 --- /dev/null +++ b/app/schemas/com.github.libretube.db.AppDatabase/11.json @@ -0,0 +1,496 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "435fb16c318b2b9fdd0d0120931f7400", + "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": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "query" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "channelId" + ] + }, + "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": { + "autoGenerate": false, + "columnNames": [ + "playlistId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LocalPlaylist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LocalPlaylistItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "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": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "download", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))", + "fields": [ + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploadDate", + "columnName": "uploadDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailPath", + "columnName": "thumbnailPath", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "videoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "downloadItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoId", + "columnName": "videoId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quality", + "columnName": "quality", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadSize", + "columnName": "downloadSize", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_downloadItem_path", + "unique": true, + "columnNames": [ + "path" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)" + } + ], + "foreignKeys": [ + { + "table": "download", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "videoId" + ], + "referencedColumns": [ + "videoId" + ] + } + ] + }, + { + "tableName": "subscriptionGroups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channels", + "columnName": "channels", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "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, '435fb16c318b2b9fdd0d0120931f7400')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/db/AppDatabase.kt b/app/src/main/java/com/github/libretube/db/AppDatabase.kt index 371dd9957..54aceb9b6 100644 --- a/app/src/main/java/com/github/libretube/db/AppDatabase.kt +++ b/app/src/main/java/com/github/libretube/db/AppDatabase.kt @@ -10,6 +10,7 @@ import com.github.libretube.db.dao.LocalPlaylistsDao 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.SubscriptionGroupsDao import com.github.libretube.db.dao.WatchHistoryDao import com.github.libretube.db.dao.WatchPositionDao import com.github.libretube.db.obj.CustomInstance @@ -20,6 +21,7 @@ import com.github.libretube.db.obj.LocalPlaylistItem 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.SubscriptionGroup import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.db.obj.WatchPosition @@ -34,13 +36,15 @@ import com.github.libretube.db.obj.WatchPosition LocalPlaylist::class, LocalPlaylistItem::class, Download::class, - DownloadItem::class + DownloadItem::class, + SubscriptionGroup::class ], - version = 10, + version = 11, autoMigrations = [ AutoMigration(from = 7, to = 8), AutoMigration(from = 8, to = 9), - AutoMigration(from = 9, to = 10) + AutoMigration(from = 9, to = 10), + AutoMigration(from = 10, to = 11) ] ) @TypeConverters(Converters::class) @@ -84,4 +88,9 @@ abstract class AppDatabase : RoomDatabase() { * Downloads */ abstract fun downloadDao(): DownloadDao + + /** + * Subscription groups + */ + abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao } diff --git a/app/src/main/java/com/github/libretube/db/Converters.kt b/app/src/main/java/com/github/libretube/db/Converters.kt index e6e9cf59a..340457c45 100644 --- a/app/src/main/java/com/github/libretube/db/Converters.kt +++ b/app/src/main/java/com/github/libretube/db/Converters.kt @@ -1,10 +1,13 @@ package com.github.libretube.db import androidx.room.TypeConverter +import com.github.libretube.api.JsonHelper import java.nio.file.Path import java.nio.file.Paths import kotlinx.datetime.LocalDate import kotlinx.datetime.toLocalDate +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString object Converters { @TypeConverter @@ -18,4 +21,10 @@ object Converters { @TypeConverter fun stringToPath(string: String?) = string?.let { Paths.get(it) } + + @TypeConverter + fun stringListToJson(value: List) = JsonHelper.json.encodeToString(value) + + @TypeConverter + fun jsonToStringList(value: String) = JsonHelper.json.decodeFromString>(value) } diff --git a/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt new file mode 100644 index 000000000..51bbaa484 --- /dev/null +++ b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt @@ -0,0 +1,23 @@ +package com.github.libretube.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.github.libretube.db.obj.SubscriptionGroup + +@Dao() +interface SubscriptionGroupsDao { + @Query("SELECT * FROM subscriptionGroups") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun createGroup(subscriptionGroup: SubscriptionGroup) + + @Update + suspend fun updateGroup(subscriptionGroup: SubscriptionGroup) + + @Query("DELETE FROM subscriptionGroups WHERE name = :name") + suspend fun deleteGroup(name: String) +} diff --git a/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt new file mode 100644 index 000000000..75eedadec --- /dev/null +++ b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt @@ -0,0 +1,10 @@ +package com.github.libretube.db.obj + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "subscriptionGroups") +data class SubscriptionGroup( + @PrimaryKey var name: String, + val channels: MutableList +) diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupChannelsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupChannelsAdapter.kt new file mode 100644 index 000000000..577296ab9 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupChannelsAdapter.kt @@ -0,0 +1,42 @@ +package com.github.libretube.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.github.libretube.api.obj.Subscription +import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding +import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.extensions.toID +import com.github.libretube.helpers.ImageHelper +import com.github.libretube.ui.viewholders.SubscriptionGroupChannelRowViewHolder + +class SubscriptionGroupChannelsAdapter( + private val channels: List, + private val group: SubscriptionGroup, + private val onGroupChanged: (SubscriptionGroup) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SubscriptionGroupChannelRowViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = SubscriptionGroupChannelRowBinding.inflate(layoutInflater, parent, false) + return SubscriptionGroupChannelRowViewHolder(binding) + } + + override fun getItemCount() = channels.size + + override fun onBindViewHolder(holder: SubscriptionGroupChannelRowViewHolder, position: Int) { + val channel = channels[position] + val channelId = channel.url.toID() + holder.binding.apply { + subscriptionChannelName.text = channel.name + ImageHelper.loadImage(channel.avatar, subscriptionChannelImage) + channelIncluded.isChecked = group.channels.contains(channelId) + channelIncluded.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) group.channels.add(channelId) else group.channels.remove(channelId) + onGroupChanged(group) + } + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt new file mode 100644 index 000000000..9f9097241 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt @@ -0,0 +1,61 @@ +package com.github.libretube.ui.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.RecyclerView +import com.github.libretube.databinding.SubscriptionGroupRowBinding +import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.ui.dialogs.EditChannelGroupDialog +import com.github.libretube.ui.viewholders.SubscriptionGroupsViewHolder +import kotlinx.coroutines.runBlocking + +class SubscriptionGroupsAdapter( + private val groups: MutableList, + private val parentFragmentManager: FragmentManager, + private val onGroupsChanged: (List) -> Unit +) : RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SubscriptionGroupsViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = SubscriptionGroupRowBinding.inflate(layoutInflater, parent, false) + return SubscriptionGroupsViewHolder(binding) + } + + override fun getItemCount() = groups.size + + override fun onBindViewHolder(holder: SubscriptionGroupsViewHolder, position: Int) { + val subscriptionGroup = groups[position] + holder.binding.apply { + groupName.text = subscriptionGroup.name + deleteGroup.setOnClickListener { + groups.remove(subscriptionGroup) + runBlocking { + DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup( + subscriptionGroup.name + ) + } + notifyItemRemoved(position) + notifyItemRangeChanged(position, itemCount) + } + editGroup.setOnClickListener { + EditChannelGroupDialog(subscriptionGroup) { + groups[position] = it + runBlocking { + DatabaseHolder.Database.subscriptionGroupsDao().updateGroup(it) + } + notifyItemChanged(position) + onGroupsChanged(groups) + }.show(parentFragmentManager, null) + } + } + } + + fun insertItem(subscriptionsGroup: SubscriptionGroup) { + groups.add(subscriptionsGroup) + notifyItemInserted(itemCount - 1) + } +} diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt new file mode 100644 index 000000000..e33ddd02d --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt @@ -0,0 +1,46 @@ +package com.github.libretube.ui.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.libretube.R +import com.github.libretube.databinding.DialogSubscriptionGroupsBinding +import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.ui.adapters.SubscriptionGroupsAdapter +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.runBlocking + +class ChannelGroupsDialog( + private val groups: MutableList, + private val onGroupsChanged: (List) -> Unit +) : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val binding = DialogSubscriptionGroupsBinding.inflate(layoutInflater) + + binding.groupsRV.layoutManager = LinearLayoutManager(context) + val adapter = SubscriptionGroupsAdapter( + groups.toMutableList(), + parentFragmentManager, + onGroupsChanged + ) + binding.groupsRV.adapter = adapter + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.channel_groups) + .setView(binding.root) + .setPositiveButton(R.string.okay, null) + .setNeutralButton(R.string.new_group) { _, _ -> + EditChannelGroupDialog(SubscriptionGroup("", mutableListOf())) { + runBlocking { + DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it) + } + groups.add(it) + adapter.insertItem(it) + onGroupsChanged(groups) + }.show(parentFragmentManager, null) + } + .create() + } +} diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/EditChannelGroupDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/EditChannelGroupDialog.kt new file mode 100644 index 000000000..2204d2c4d --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/dialogs/EditChannelGroupDialog.kt @@ -0,0 +1,80 @@ +package com.github.libretube.ui.dialogs + +import android.app.Dialog +import android.os.Bundle +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.libretube.R +import com.github.libretube.api.SubscriptionHelper +import com.github.libretube.api.obj.Subscription +import com.github.libretube.databinding.DialogEditChannelGroupBinding +import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.ui.adapters.SubscriptionGroupChannelsAdapter +import com.github.libretube.ui.models.SubscriptionsViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class EditChannelGroupDialog( + private var group: SubscriptionGroup, + private val onGroupChanged: (SubscriptionGroup) -> Unit +) : DialogFragment() { + private val subscriptionsModel: SubscriptionsViewModel by activityViewModels() + private lateinit var binding: DialogEditChannelGroupBinding + private var channels: List = listOf() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogEditChannelGroupBinding.inflate(layoutInflater) + binding.groupName.setText(group.name) + + binding.channelsRV.layoutManager = LinearLayoutManager(context) + fetchSubscriptions() + + binding.searchInput.addTextChangedListener { + showChannels(channels, it?.toString()) + } + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.edit_group) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.okay) { _, _ -> + group.name = binding.groupName.text.toString() + if (group.name.isBlank()) return@setPositiveButton + onGroupChanged(group) + } + .setView(binding.root) + .create() + } + + private fun fetchSubscriptions() { + subscriptionsModel.subscriptions.value?.let { + channels = it + showChannels(it, null) + return + } + lifecycleScope.launch(Dispatchers.IO) { + channels = runCatching { + SubscriptionHelper.getSubscriptions() + }.getOrNull().orEmpty() + withContext(Dispatchers.Main) { + showChannels(channels, null) + } + } + } + + private fun showChannels(channels: List, query: String?) { + binding.channelsRV.adapter = SubscriptionGroupChannelsAdapter( + channels.filter { query == null || it.name.lowercase().contains(query.lowercase()) }, + group + ) { + group = it + } + binding.subscriptionsContainer.isVisible = true + binding.progress.isVisible = false + } +} diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index ec589008e..46f3cbb72 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -5,10 +5,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import com.github.libretube.R @@ -16,19 +18,25 @@ import com.github.libretube.api.obj.StreamItem import com.github.libretube.constants.PreferenceKeys import com.github.libretube.databinding.FragmentSubscriptionsBinding import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.extensions.toID import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.ui.adapters.LegacySubscriptionAdapter import com.github.libretube.ui.adapters.SubscriptionChannelAdapter import com.github.libretube.ui.adapters.VideosAdapter +import com.github.libretube.ui.dialogs.ChannelGroupsDialog import com.github.libretube.ui.models.SubscriptionsViewModel import com.github.libretube.ui.sheets.BaseBottomSheet +import com.google.android.material.chip.Chip import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking class SubscriptionsFragment : Fragment() { private lateinit var binding: FragmentSubscriptionsBinding private val viewModel: SubscriptionsViewModel by activityViewModels() + private var channelGroups: List = listOf() + private var selectedFilterGroup: Int = 0 var subscriptionsAdapter: VideosAdapter? = null private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0) @@ -82,14 +90,12 @@ class SubscriptionsFragment : Fragment() { } viewModel.videoFeed.observe(viewLifecycleOwner) { - if (!isShowingFeed()) return@observe - if (it == null) return@observe + if (!isShowingFeed() || it == null) return@observe showFeed() } viewModel.subscriptions.observe(viewLifecycleOwner) { - if (isShowingFeed()) return@observe - if (it == null) return@observe + if (isShowingFeed() || it == null) return@observe showSubscriptions() } @@ -150,29 +156,73 @@ class SubscriptionsFragment : Fragment() { binding.subRefresh.isRefreshing = false } } + + lifecycleScope.launch { + initChannelGroups() + } + } + + private suspend fun initChannelGroups() { + channelGroups = DatabaseHolder.Database.subscriptionGroupsDao().getAll() + + binding.chipAll.isSelected = true + binding.channelGroups.children.forEachIndexed { index, view -> + if (index != 0) binding.channelGroups.removeView(view) + } + + channelGroups.forEachIndexed { index, group -> + val chip = Chip(context, null, R.style.ElevatedFilterChip).apply { + id = View.generateViewId() + isCheckable = true + isClickable = true + text = group.name + setOnClickListener { + selectedFilterGroup = index + 1 // since the first one is "All" + showFeed() + } + } + + binding.channelGroups.addView(chip) + } + + binding.editGroups.setOnClickListener { + ChannelGroupsDialog(channelGroups.toMutableList()) { + lifecycleScope.launch { initChannelGroups() } + }.show(childFragmentManager, null) + } } private fun showFeed() { if (viewModel.videoFeed.value == null) return binding.subRefresh.isRefreshing = false - val feed = viewModel.videoFeed.value!!.filter { - // apply the selected filter - when (selectedFilter) { - 0 -> true - 1 -> !it.isShort - 2 -> it.isShort - else -> throw IllegalArgumentException() - } - }.let { streams -> - runBlocking { - if (!PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) { - streams + val feed = viewModel.videoFeed.value!! + .filter { streamItem -> + // filter for selected channel groups + if (selectedFilterGroup == 0) { + true } else { - removeWatchVideosFromFeed(streams) + val channelId = streamItem.uploaderUrl.orEmpty().toID() + channelGroups.getOrNull(selectedFilterGroup + 1)?.channels?.contains(channelId) != false + } + } + .filter { + // apply the selected filter + when (selectedFilter) { + 0 -> true + 1 -> !it.isShort + 2 -> it.isShort + else -> throw IllegalArgumentException() + } + }.let { streams -> + runBlocking { + if (!PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) { + streams + } else { + removeWatchVideosFromFeed(streams) + } } } - } // sort the feed val sortedFeed = when (selectedSortOrder) { diff --git a/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupChannelRowViewHolder.kt b/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupChannelRowViewHolder.kt new file mode 100644 index 000000000..eb25fb491 --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupChannelRowViewHolder.kt @@ -0,0 +1,8 @@ +package com.github.libretube.ui.viewholders + +import androidx.recyclerview.widget.RecyclerView +import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding + +class SubscriptionGroupChannelRowViewHolder( + val binding: SubscriptionGroupChannelRowBinding +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupsViewHolder.kt b/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupsViewHolder.kt new file mode 100644 index 000000000..6f021364a --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/viewholders/SubscriptionGroupsViewHolder.kt @@ -0,0 +1,8 @@ +package com.github.libretube.ui.viewholders + +import androidx.recyclerview.widget.RecyclerView +import com.github.libretube.databinding.SubscriptionGroupRowBinding + +class SubscriptionGroupsViewHolder( + val binding: SubscriptionGroupRowBinding +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..5fb90ad4f --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_edit_channel_group.xml b/app/src/main/res/layout/dialog_edit_channel_group.xml new file mode 100644 index 000000000..ccc16a090 --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_channel_group.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_subscription_groups.xml b/app/src/main/res/layout/dialog_subscription_groups.xml new file mode 100644 index 000000000..623eb5943 --- /dev/null +++ b/app/src/main/res/layout/dialog_subscription_groups.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index 8ecf17855..bd054b7ec 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -131,6 +131,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subscription_group_row.xml b/app/src/main/res/layout/subscription_group_row.xml new file mode 100644 index 000000000..bee902a47 --- /dev/null +++ b/app/src/main/res/layout/subscription_group_row.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 979d0f754..8ecc5c8b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,6 +456,10 @@ Disable Piped proxy Load videos and images directly from YouTube\'s servers. Only enable the option if you use a VPN anyways! Note that this might not work with content from YT music. Auto fullscreen on short videos + Channel groups + New + Group name + Edit group Download Service diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml index 01fc1ed30..4dc84e3ed 100644 --- a/app/src/main/res/values/style.xml +++ b/app/src/main/res/values/style.xml @@ -250,4 +250,13 @@ + + \ No newline at end of file From 4402bf1baf3d808ca285809606953e3ed9e192df Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 28 Mar 2023 17:40:48 +0200 Subject: [PATCH 2/4] Fix some minor subscription group issues --- .../libretube/db/dao/SubscriptionGroupsDao.kt | 4 ---- .../ui/adapters/SubscriptionGroupsAdapter.kt | 11 ++++++--- .../ui/dialogs/ChannelGroupsDialog.kt | 3 ++- .../ui/fragments/SubscriptionsFragment.kt | 24 ++++++++++++------- .../res/layout/dialog_edit_channel_group.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt index 51bbaa484..22fc79a37 100644 --- a/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt +++ b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt @@ -4,7 +4,6 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Update import com.github.libretube.db.obj.SubscriptionGroup @Dao() @@ -15,9 +14,6 @@ interface SubscriptionGroupsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun createGroup(subscriptionGroup: SubscriptionGroup) - @Update - suspend fun updateGroup(subscriptionGroup: SubscriptionGroup) - @Query("DELETE FROM subscriptionGroups WHERE name = :name") suspend fun deleteGroup(name: String) } diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt index 9f9097241..ec4422129 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt @@ -9,6 +9,7 @@ import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.ui.dialogs.EditChannelGroupDialog import com.github.libretube.ui.viewholders.SubscriptionGroupsViewHolder +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking class SubscriptionGroupsAdapter( @@ -33,7 +34,7 @@ class SubscriptionGroupsAdapter( groupName.text = subscriptionGroup.name deleteGroup.setOnClickListener { groups.remove(subscriptionGroup) - runBlocking { + runBlocking(Dispatchers.IO) { DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup( subscriptionGroup.name ) @@ -44,8 +45,12 @@ class SubscriptionGroupsAdapter( editGroup.setOnClickListener { EditChannelGroupDialog(subscriptionGroup) { groups[position] = it - runBlocking { - DatabaseHolder.Database.subscriptionGroupsDao().updateGroup(it) + runBlocking(Dispatchers.IO) { + // delete the old one as it might have a different name + DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup( + subscriptionGroup.name + ) + DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it) } notifyItemChanged(position) onGroupsChanged(groups) diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt index e33ddd02d..0c6cd335a 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/ChannelGroupsDialog.kt @@ -10,6 +10,7 @@ import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.ui.adapters.SubscriptionGroupsAdapter import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking class ChannelGroupsDialog( @@ -33,7 +34,7 @@ class ChannelGroupsDialog( .setPositiveButton(R.string.okay, null) .setNeutralButton(R.string.new_group) { _, _ -> EditChannelGroupDialog(SubscriptionGroup("", mutableListOf())) { - runBlocking { + runBlocking(Dispatchers.IO) { DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it) } groups.add(it) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index 46f3cbb72..3db87c0ca 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -166,25 +166,26 @@ class SubscriptionsFragment : Fragment() { channelGroups = DatabaseHolder.Database.subscriptionGroupsDao().getAll() binding.chipAll.isSelected = true - binding.channelGroups.children.forEachIndexed { index, view -> - if (index != 0) binding.channelGroups.removeView(view) + binding.channelGroups.children.forEach { view -> + if (view.id != R.id.chip_all) binding.channelGroups.removeView(view) } - channelGroups.forEachIndexed { index, group -> + channelGroups.forEach { group -> val chip = Chip(context, null, R.style.ElevatedFilterChip).apply { id = View.generateViewId() isCheckable = true isClickable = true text = group.name - setOnClickListener { - selectedFilterGroup = index + 1 // since the first one is "All" - showFeed() - } } binding.channelGroups.addView(chip) } + binding.channelGroups.setOnCheckedStateChangeListener { group, checkedIds -> + selectedFilterGroup = group.children.indexOfFirst { it.id == checkedIds.first() } + showFeed() + } + binding.editGroups.setOnClickListener { ChannelGroupsDialog(channelGroups.toMutableList()) { lifecycleScope.launch { initChannelGroups() } @@ -203,7 +204,8 @@ class SubscriptionsFragment : Fragment() { true } else { val channelId = streamItem.uploaderUrl.orEmpty().toID() - channelGroups.getOrNull(selectedFilterGroup + 1)?.channels?.contains(channelId) != false + val group = channelGroups.getOrNull(selectedFilterGroup - 1) + group?.channels?.contains(channelId) != false } } .filter { @@ -216,7 +218,11 @@ class SubscriptionsFragment : Fragment() { } }.let { streams -> runBlocking { - if (!PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) { + if (!PreferenceHelper.getBoolean( + PreferenceKeys.HIDE_WATCHED_FROM_FEED, + false + ) + ) { streams } else { removeWatchVideosFromFeed(streams) diff --git a/app/src/main/res/layout/dialog_edit_channel_group.xml b/app/src/main/res/layout/dialog_edit_channel_group.xml index ccc16a090..b30c4b07d 100644 --- a/app/src/main/res/layout/dialog_edit_channel_group.xml +++ b/app/src/main/res/layout/dialog_edit_channel_group.xml @@ -8,6 +8,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ecc5c8b0..c15b448ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -460,6 +460,7 @@ New Group name Edit group + Group already exists! Download Service From a4088189bf63e7950935a575e133df45b0df207b Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 28 Mar 2023 17:45:21 +0200 Subject: [PATCH 3/4] Allow creating backups of subscripton groups --- .../github/libretube/db/dao/SubscriptionGroupsDao.kt | 3 +++ .../com/github/libretube/db/obj/SubscriptionGroup.kt | 2 ++ .../java/com/github/libretube/helpers/BackupHelper.kt | 7 ++++--- .../main/java/com/github/libretube/obj/BackupFile.kt | 4 +++- .../com/github/libretube/ui/dialogs/BackupDialog.kt | 11 ++++++++--- app/src/main/res/values/strings.xml | 1 - 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt index 22fc79a37..8e9be5f27 100644 --- a/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt +++ b/app/src/main/java/com/github/libretube/db/dao/SubscriptionGroupsDao.kt @@ -14,6 +14,9 @@ interface SubscriptionGroupsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun createGroup(subscriptionGroup: SubscriptionGroup) + @Insert + suspend fun insertAll(subscriptionGroups: List) + @Query("DELETE FROM subscriptionGroups WHERE name = :name") suspend fun deleteGroup(name: String) } diff --git a/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt index 75eedadec..7569a28ec 100644 --- a/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt +++ b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt @@ -2,7 +2,9 @@ package com.github.libretube.db.obj import androidx.room.Entity import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable +@Serializable @Entity(tableName = "subscriptionGroups") data class SubscriptionGroup( @PrimaryKey var name: String, diff --git a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt index 1b4ec02dc..fb25d7676 100644 --- a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt @@ -52,13 +52,14 @@ object BackupHelper { Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty()) Database.customInstanceDao().insertAll(backupFile.customInstances.orEmpty()) Database.playlistBookmarkDao().insertAll(backupFile.playlistBookmarks.orEmpty()) + Database.subscriptionGroupsDao().insertAll(backupFile.channelGroups.orEmpty()) backupFile.localPlaylists?.forEach { Database.localPlaylistsDao().createPlaylist(it.playlist) val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id - it.videos.forEach { - it.playlistId = playlistId - Database.localPlaylistsDao().addPlaylistVideo(it) + it.videos.forEach { playlistItem -> + playlistItem.playlistId = playlistId + Database.localPlaylistsDao().addPlaylistVideo(playlistItem) } } diff --git a/app/src/main/java/com/github/libretube/obj/BackupFile.kt b/app/src/main/java/com/github/libretube/obj/BackupFile.kt index 0768c2724..503a3f318 100644 --- a/app/src/main/java/com/github/libretube/obj/BackupFile.kt +++ b/app/src/main/java/com/github/libretube/obj/BackupFile.kt @@ -5,6 +5,7 @@ import com.github.libretube.db.obj.LocalPlaylistWithVideos 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.SubscriptionGroup import com.github.libretube.db.obj.WatchHistoryItem import com.github.libretube.db.obj.WatchPosition import kotlinx.serialization.Serializable @@ -18,5 +19,6 @@ data class BackupFile( var customInstances: List? = emptyList(), var playlistBookmarks: List? = emptyList(), var localPlaylists: List? = emptyList(), - var preferences: List? = emptyList() + var preferences: List? = emptyList(), + var channelGroups: List? = emptyList() ) diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt index 8328c99be..d5626bcb3 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt @@ -17,11 +17,11 @@ import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive class BackupDialog( - private val createBackupFile: (BackupFile) -> Unit + private val createBackupFile: (BackupFile) -> Unit, ) : DialogFragment() { sealed class BackupOption( @StringRes val name: Int, - val onSelected: suspend (BackupFile) -> Unit + val onSelected: suspend (BackupFile) -> Unit, ) { object WatchHistory : BackupOption(R.string.watch_history, onSelected = { it.watchHistory = Database.watchHistoryDao().getAll() @@ -51,6 +51,10 @@ class BackupDialog( it.localPlaylists = Database.localPlaylistsDao().getAll() }) + object SubscriptionGroups : BackupOption(R.string.channel_groups, onSelected = { + it.channelGroups = Database.subscriptionGroupsDao().getAll() + }) + object Preferences : BackupOption(R.string.preferences, onSelected = { file -> file.preferences = PreferenceHelper.settings.all.map { (key, value) -> val jsonValue = when (value) { @@ -73,7 +77,8 @@ class BackupDialog( BackupOption.CustomInstances, BackupOption.PlaylistBookmarks, BackupOption.LocalPlaylists, - BackupOption.Preferences + BackupOption.SubscriptionGroups, + BackupOption.Preferences, ) val backupItems = backupOptions.map { context?.getString(it.name)!! }.toTypedArray() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c15b448ef..8ecc5c8b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -460,7 +460,6 @@ New Group name Edit group - Group already exists! Download Service From caea47d0ed163456c89c3779b878fa8f0e33a96c Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 28 Mar 2023 18:08:50 +0200 Subject: [PATCH 4/4] Fix some UI subscription groups issues --- .../ui/adapters/SubscriptionGroupsAdapter.kt | 5 +++-- .../com/github/libretube/ui/dialogs/BackupDialog.kt | 6 +++--- .../libretube/ui/fragments/SubscriptionsFragment.kt | 13 +++++++------ app/src/main/res/layout/filter_chip.xml | 5 +++++ 4 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/layout/filter_chip.xml diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt index ec4422129..03c1c402f 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/SubscriptionGroupsAdapter.kt @@ -33,12 +33,13 @@ class SubscriptionGroupsAdapter( holder.binding.apply { groupName.text = subscriptionGroup.name deleteGroup.setOnClickListener { - groups.remove(subscriptionGroup) + groups.removeAt(position) runBlocking(Dispatchers.IO) { DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup( subscriptionGroup.name ) } + onGroupsChanged(groups) notifyItemRemoved(position) notifyItemRangeChanged(position, itemCount) } @@ -48,7 +49,7 @@ class SubscriptionGroupsAdapter( runBlocking(Dispatchers.IO) { // delete the old one as it might have a different name DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup( - subscriptionGroup.name + groupName.text.toString() ) DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it) } diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt index d5626bcb3..f57e4383c 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt @@ -17,11 +17,11 @@ import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive class BackupDialog( - private val createBackupFile: (BackupFile) -> Unit, + private val createBackupFile: (BackupFile) -> Unit ) : DialogFragment() { sealed class BackupOption( @StringRes val name: Int, - val onSelected: suspend (BackupFile) -> Unit, + val onSelected: suspend (BackupFile) -> Unit ) { object WatchHistory : BackupOption(R.string.watch_history, onSelected = { it.watchHistory = Database.watchHistoryDao().getAll() @@ -78,7 +78,7 @@ class BackupDialog( BackupOption.PlaylistBookmarks, BackupOption.LocalPlaylists, BackupOption.SubscriptionGroups, - BackupOption.Preferences, + BackupOption.Preferences ) val backupItems = backupOptions.map { context?.getString(it.name)!! }.toTypedArray() diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index 3db87c0ca..48b29fb46 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -1,5 +1,6 @@ package com.github.libretube.ui.fragments +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -162,19 +163,19 @@ class SubscriptionsFragment : Fragment() { } } + @SuppressLint("InflateParams") private suspend fun initChannelGroups() { channelGroups = DatabaseHolder.Database.subscriptionGroupsDao().getAll() - binding.chipAll.isSelected = true - binding.channelGroups.children.forEach { view -> - if (view.id != R.id.chip_all) binding.channelGroups.removeView(view) - } + binding.chipAll.isChecked = true + binding.channelGroups.removeAllViews() + binding.channelGroups.addView(binding.chipAll) channelGroups.forEach { group -> - val chip = Chip(context, null, R.style.ElevatedFilterChip).apply { + val chip = layoutInflater.inflate(R.layout.filter_chip, null) as Chip + chip.apply { id = View.generateViewId() isCheckable = true - isClickable = true text = group.name } diff --git a/app/src/main/res/layout/filter_chip.xml b/app/src/main/res/layout/filter_chip.xml new file mode 100644 index 000000000..0f287ddcb --- /dev/null +++ b/app/src/main/res/layout/filter_chip.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file