diff --git a/app/schemas/com.github.libretube.db.AppDatabase/13.json b/app/schemas/com.github.libretube.db.AppDatabase/13.json index 31bcb3160..12f5c285c 100644 --- a/app/schemas/com.github.libretube.db.AppDatabase/13.json +++ b/app/schemas/com.github.libretube.db.AppDatabase/13.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 13, - "identityHash": "ee7e6417524e834a73c6d4f8847bce11", + "identityHash": "a07f3a23fa32cba2d80830e903c145c4", "entities": [ { "tableName": "watchHistoryItem", @@ -474,7 +474,7 @@ }, { "tableName": "subscriptionGroups", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, PRIMARY KEY(`name`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`name`))", "fields": [ { "fieldPath": "name", @@ -487,6 +487,12 @@ "columnName": "channels", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -502,7 +508,7 @@ "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, 'ee7e6417524e834a73c6d4f8847bce11')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a07f3a23fa32cba2d80830e903c145c4')" ] } } \ No newline at end of file diff --git a/app/schemas/com.github.libretube.db.AppDatabase/14.json b/app/schemas/com.github.libretube.db.AppDatabase/14.json new file mode 100644 index 000000000..3d9b03c9e --- /dev/null +++ b/app/schemas/com.github.libretube.db.AppDatabase/14.json @@ -0,0 +1,514 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "4b4658954c6632e2aa1ebd25a9d7c731", + "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, `videos` INTEGER NOT NULL, 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 + }, + { + "fieldPath": "videos", + "columnName": "videos", + "affinity": "INTEGER", + "notNull": true + } + ], + "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, `description` TEXT)", + "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 + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + } + ], + "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, `index` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "channels", + "columnName": "channels", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "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, '4b4658954c6632e2aa1ebd25a9d7c731')" + ] + } +} \ 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 c57a479b0..282322dbe 100644 --- a/app/src/main/java/com/github/libretube/db/AppDatabase.kt +++ b/app/src/main/java/com/github/libretube/db/AppDatabase.kt @@ -39,7 +39,7 @@ import com.github.libretube.db.obj.WatchPosition DownloadItem::class, SubscriptionGroup::class ], - version = 13, + version = 14, autoMigrations = [ AutoMigration(from = 7, to = 8), AutoMigration(from = 8, to = 9), diff --git a/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt b/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt index 58e871f18..7b9264227 100644 --- a/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt +++ b/app/src/main/java/com/github/libretube/db/DatabaseHolder.kt @@ -23,9 +23,17 @@ object DatabaseHolder { } } + private val MIGRATION_13_14 = object : Migration(13, 14) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "ALTER TABLE 'subscriptionGroups' ADD COLUMN 'index' INTEGER NOT NULL DEFAULT 0" + ) + } + } + val Database by lazy { Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME) - .addMigrations(MIGRATION_11_12, MIGRATION_12_13) + .addMigrations(MIGRATION_11_12, MIGRATION_12_13, MIGRATION_13_14) .fallbackToDestructiveMigration() .build() } 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 7b4279f4d..0db9887fd 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,11 +4,12 @@ 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") + @Query("SELECT * FROM subscriptionGroups ORDER BY `index` ASC") suspend fun getAll(): List @Query("SELECT EXISTS(SELECT * FROM subscriptionGroups WHERE name = :name)") @@ -20,6 +21,9 @@ interface SubscriptionGroupsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(subscriptionGroups: List) + @Update + suspend fun updateAll(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 d6af291a2..4312dd8ec 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 @@ -8,5 +8,6 @@ import kotlinx.serialization.Serializable @Entity(tableName = "subscriptionGroups") data class SubscriptionGroup( @PrimaryKey var name: String, - var channels: List + var channels: List, + var index: Int ) 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 5ce11d318..f7181ef27 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 @@ -187,6 +187,7 @@ class SubscriptionsFragment : Fragment() { binding.channelGroups.removeAllViews() binding.channelGroups.addView(binding.chipAll) + channelGroups = channelGroups.sortedBy { it.index } channelGroups.forEach { group -> val chip = layoutInflater.inflate(R.layout.filter_chip, null) as Chip chip.apply { diff --git a/app/src/main/java/com/github/libretube/ui/sheets/ChannelGroupsSheet.kt b/app/src/main/java/com/github/libretube/ui/sheets/ChannelGroupsSheet.kt index 7cca64f7b..87b778194 100644 --- a/app/src/main/java/com/github/libretube/ui/sheets/ChannelGroupsSheet.kt +++ b/app/src/main/java/com/github/libretube/ui/sheets/ChannelGroupsSheet.kt @@ -4,10 +4,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.github.libretube.databinding.DialogSubscriptionGroupsBinding import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.SubscriptionGroup +import com.github.libretube.extensions.move import com.github.libretube.ui.adapters.SubscriptionGroupsAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -32,7 +35,7 @@ class ChannelGroupsSheet( binding.groupsRV.adapter = adapter binding.newGroup.setOnClickListener { - EditChannelGroupSheet(SubscriptionGroup("", mutableListOf())) { + EditChannelGroupSheet(SubscriptionGroup("", mutableListOf(), 0)) { runBlocking(Dispatchers.IO) { DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it) } @@ -46,6 +49,35 @@ class ChannelGroupsSheet( dismiss() } + val callback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + 0 + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val from = viewHolder.absoluteAdapterPosition + val to = target.absoluteAdapterPosition + + groups.move(from, to) + adapter.notifyItemMoved(from, to) + + groups.forEachIndexed { index, group -> group.index = index } + runBlocking(Dispatchers.IO) { + DatabaseHolder.Database.subscriptionGroupsDao().updateAll(groups) + } + onGroupsChanged(groups) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + } + + val itemTouchHelper = ItemTouchHelper(callback) + itemTouchHelper.attachToRecyclerView(binding.groupsRV) + return binding.root } } diff --git a/app/src/main/res/layout/subscription_group_row.xml b/app/src/main/res/layout/subscription_group_row.xml index 05f55c678..59bdfb83b 100644 --- a/app/src/main/res/layout/subscription_group_row.xml +++ b/app/src/main/res/layout/subscription_group_row.xml @@ -6,6 +6,13 @@ android:paddingHorizontal="5dp" android:paddingVertical="10dp"> + + - \ No newline at end of file +