From b49926f80b1f61f552effa9b10c381399827918b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 8 Jun 2026 13:45:38 -0400 Subject: [PATCH] Store remembered tabs in app database instead of preferences (#1541) ## Description Moves storing the remembered tabs from app preferences to the app database. This means deleting the user will remove the remembered tabs and the app preferences file is smaller and faster to load. The remembered tabs are migrated automatically when the app is updated. There are no significant user facing changes in this PR. ### Related issues N/A ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None --- .../34.json | 720 ++++++++++++++++++ .../damontecres/wholphin/data/AppDatabase.kt | 7 +- .../wholphin/data/RememberedTabDao.kt | 22 + .../wholphin/data/model/RememberedTab.kt | 24 + .../wholphin/services/AppUpgradeHandler.kt | 59 ++ .../wholphin/services/RememberedTabService.kt | 34 + .../wholphin/services/hilt/AppModule.kt | 52 -- .../wholphin/services/hilt/DatabaseModule.kt | 5 + .../ui/components/CircularProgress.kt | 44 ++ .../wholphin/ui/components/TabRow.kt | 35 +- .../wholphin/ui/components/TabbedPage.kt | 140 ++++ .../ui/detail/CollectionFolderLiveTv.kt | 222 +++--- .../ui/detail/CollectionFolderMovie.kt | 82 +- .../ui/detail/CollectionFolderMusic.kt | 95 +-- .../wholphin/ui/detail/CollectionFolderTv.kt | 109 +-- .../wholphin/ui/detail/FavoritesPage.kt | 133 ++-- .../ui/detail/series/SeriesOverviewContent.kt | 21 +- .../wholphin/ui/discover/DiscoverPage.kt | 87 +-- .../wholphin/ui/main/SearchPage.kt | 17 +- .../wholphin/ui/nav/DestinationContent.kt | 1 + .../ui/preferences/PreferencesViewModel.kt | 5 +- .../wholphin/util/RememberTabManager.kt | 45 -- app/src/main/proto/WholphinDataStore.proto | 2 +- .../damontecres/wholphin/ui/TestModule.kt | 17 +- 24 files changed, 1379 insertions(+), 599 deletions(-) create mode 100644 app/schemas/com.github.damontecres.wholphin.data.AppDatabase/34.json create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/RememberedTabDao.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/model/RememberedTab.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/services/RememberedTabService.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/components/TabbedPage.kt delete mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt diff --git a/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/34.json b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/34.json new file mode 100644 index 00000000..a5709ec2 --- /dev/null +++ b/app/schemas/com.github.damontecres.wholphin.data.AppDatabase/34.json @@ -0,0 +1,720 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "278e485064fc9f184a2181599f0f8efb", + "entities": [ + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT" + }, + { + "fieldPath": "lastUsed", + "columnName": "lastUsed", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "rowId", + "columnName": "rowId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT" + }, + { + "fieldPath": "pin", + "columnName": "pin", + "affinity": "TEXT" + }, + { + "fieldPath": "requireLogin", + "columnName": "requireLogin", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "lastUsed", + "columnName": "lastUsed", + "affinity": "TEXT" + }, + { + "fieldPath": "uiLanguage", + "columnName": "uiLanguage", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowId" + ] + }, + "indices": [ + { + "name": "index_users_id_serverId", + "unique": true, + "columnNames": [ + "id", + "serverId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)" + }, + { + "name": "index_users_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_users_serverId", + "unique": false, + "columnNames": [ + "serverId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)" + } + ], + "foreignKeys": [ + { + "table": "servers", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "ItemPlayback", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "rowId", + "columnName": "rowId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "TEXT" + }, + { + "fieldPath": "audioIndex", + "columnName": "audioIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subtitleIndex", + "columnName": "subtitleIndex", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowId" + ] + }, + "indices": [ + { + "name": "index_ItemPlayback_userId_itemId", + "unique": true, + "columnNames": [ + "userId", + "itemId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "NavDrawerPinnedItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "itemId" + ] + }, + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "LibraryDisplayInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "direction", + "columnName": "direction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filter", + "columnName": "filter", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{}'" + }, + { + "fieldPath": "viewOptions", + "columnName": "viewOptions", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "itemId" + ] + }, + "indices": [ + { + "name": "index_LibraryDisplayInfo_userId_itemId", + "unique": true, + "columnNames": [ + "userId", + "itemId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "playback_effects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))", + "fields": [ + { + "fieldPath": "jellyfinUserRowId", + "columnName": "jellyfinUserRowId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "videoFilter.rotation", + "columnName": "rotation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.brightness", + "columnName": "brightness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.contrast", + "columnName": "contrast", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.saturation", + "columnName": "saturation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.hue", + "columnName": "hue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.red", + "columnName": "red", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.green", + "columnName": "green", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.blue", + "columnName": "blue", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "videoFilter.blur", + "columnName": "blur", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "jellyfinUserRowId", + "itemId", + "type" + ] + } + }, + { + "tableName": "PlaybackLanguageChoice", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seriesId", + "columnName": "seriesId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT" + }, + { + "fieldPath": "audioLanguage", + "columnName": "audioLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "subtitleLanguage", + "columnName": "subtitleLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "subtitlesDisabled", + "columnName": "subtitlesDisabled", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "seriesId" + ] + }, + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "ItemTrackModification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "trackIndex", + "columnName": "trackIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delayMs", + "columnName": "delayMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "itemId", + "trackIndex" + ] + }, + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "seerr_servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_seerr_servers_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)" + } + ] + }, + { + "tableName": "seerr_users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "jellyfinUserRowId", + "columnName": "jellyfinUserRowId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authMethod", + "columnName": "authMethod", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT" + }, + { + "fieldPath": "credential", + "columnName": "credential", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "jellyfinUserRowId", + "serverId" + ] + }, + "foreignKeys": [ + { + "table": "seerr_servers", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "serverId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "jellyfinUserRowId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + }, + { + "tableName": "RememberedTab", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "itemId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId", + "itemId" + ] + }, + "indices": [ + { + "name": "index_RememberedTab_userId_itemId", + "unique": true, + "columnNames": [ + "userId", + "itemId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)" + } + ], + "foreignKeys": [ + { + "table": "users", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "userId" + ], + "referencedColumns": [ + "rowId" + ] + } + ] + } + ], + "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, '278e485064fc9f184a2181599f0f8efb')" + ] + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt index 69f2e745..43676509 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/AppDatabase.kt @@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.PlaybackEffect import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice +import com.github.damontecres.wholphin.data.model.RememberedTab import com.github.damontecres.wholphin.data.model.SeerrServer import com.github.damontecres.wholphin.data.model.SeerrUser import com.github.damontecres.wholphin.ui.components.ViewOptions @@ -46,8 +47,9 @@ import java.util.UUID ItemTrackModification::class, SeerrServer::class, SeerrUser::class, + RememberedTab::class, ], - version = 33, + version = 34, exportSchema = true, autoMigrations = [ AutoMigration(3, 4), @@ -64,6 +66,7 @@ import java.util.UUID AutoMigration(30, 31), AutoMigration(31, 32), AutoMigration(32, 33), + AutoMigration(33, 34), ], ) @TypeConverters(Converters::class) @@ -81,6 +84,8 @@ abstract class AppDatabase : RoomDatabase() { abstract fun seerrServerDao(): SeerrServerDao abstract fun playbackEffectDao(): PlaybackEffectDao + + abstract fun rememberedTabDao(): RememberedTabDao } class Converters { diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/RememberedTabDao.kt b/app/src/main/java/com/github/damontecres/wholphin/data/RememberedTabDao.kt new file mode 100644 index 00000000..13398228 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/data/RememberedTabDao.kt @@ -0,0 +1,22 @@ +package com.github.damontecres.wholphin.data + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.github.damontecres.wholphin.data.model.RememberedTab + +@Dao +interface RememberedTabDao { + @Query("SELECT * from RememberedTab WHERE userId=:userId AND itemId=:itemId") + suspend fun getRememberedTab( + userId: Int, + itemId: String, + ): RememberedTab? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun save(item: RememberedTab): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAll(items: List) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/RememberedTab.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/RememberedTab.kt new file mode 100644 index 00000000..2fc6f702 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/RememberedTab.kt @@ -0,0 +1,24 @@ +package com.github.damontecres.wholphin.data.model + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +@Entity( + foreignKeys = [ + ForeignKey( + entity = JellyfinUser::class, + parentColumns = arrayOf("rowId"), + childColumns = arrayOf("userId"), + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ], + indices = [Index("userId", "itemId", unique = true)], + primaryKeys = ["userId", "itemId"], +) +data class RememberedTab( + val userId: Int, + val itemId: String, + val index: Int, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 281eddc2..018b50ba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -6,7 +6,11 @@ import android.os.Build import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.preference.PreferenceManager +import com.github.damontecres.wholphin.data.JellyfinServerDao +import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao +import com.github.damontecres.wholphin.data.model.JellyfinUser +import com.github.damontecres.wholphin.data.model.RememberedTab import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.ScreensaverPreference @@ -28,9 +32,12 @@ import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings import com.github.damontecres.wholphin.ui.setup.seerr.migrateSeerrUrl import com.github.damontecres.wholphin.util.Version import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.first import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber import java.io.File +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -44,6 +51,8 @@ class AppUpgradeHandler @param:ApplicationContext private val context: Context, private val appPreferences: DataStore, private val seerrServerDao: SeerrServerDao, + private val serverDao: JellyfinServerDao, + private val rememberedTabDao: RememberedTabDao, ) { val pkgInfo: PackageInfo get() = context.packageManager.getPackageInfo(context.packageName, 0) val currentVersion: Version get() = Version.fromString(pkgInfo.versionName!!) @@ -350,5 +359,55 @@ class AppUpgradeHandler } } } + + if (previous.isEqualOrBefore(Version.fromString("1.0.1-2-g0"))) { + Timber.i("Starting tab migration") + val users = mutableMapOf() + val tabs = + appPreferences.data.first().interfacePreferences.rememberedTabsMap.mapNotNull { (key, tabIndex) -> + // "serverId_userId_itemId", itemId can have underscores + try { + val firstUnderScore = key.indexOf("_").takeIf { it >= 1 } + if (firstUnderScore != null) { + val secondUnderscore = + key + .indexOf("_", startIndex = firstUnderScore + 1) + .takeIf { it >= 1 } + if (secondUnderscore != null) { + val serverId = key.substring(0, firstUnderScore).toUUIDOrNull() + val userId = + key + .substring(firstUnderScore + 1, secondUnderscore) + .toUUIDOrNull() + val itemId = key.substring(secondUnderscore + 1, key.length) + if (userId != null && serverId != null) { + return@mapNotNull users + .getOrPut(userId) { + serverDao.getUser(serverId, userId) + }?.let { + RememberedTab(it.rowId, itemId, tabIndex) + } + } + } + } + } catch (ex: Exception) { + Timber.w(ex, "Error parsing '%s'", key) + } + null + } + Timber.v("Migrating %s tabs", tabs.size) + try { + if (tabs.isNotEmpty()) { + rememberedTabDao.saveAll(tabs) + } + appPreferences.updateData { + it.updateInterfacePreferences { + clearRememberedTabs() + } + } + } catch (ex: Exception) { + Timber.e(ex, "Error saving migrated tabs") + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RememberedTabService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RememberedTabService.kt new file mode 100644 index 00000000..515caad4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RememberedTabService.kt @@ -0,0 +1,34 @@ +package com.github.damontecres.wholphin.services + +import com.github.damontecres.wholphin.data.RememberedTabDao +import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.RememberedTab +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RememberedTabService + @Inject + constructor( + private val serverRepository: ServerRepository, + private val rememberedTabDao: RememberedTabDao, + ) { + suspend fun getRememberedTab(itemId: String): Int? = + withContext(Dispatchers.IO) { + serverRepository.currentUser?.rowId?.let { userId -> + rememberedTabDao.getRememberedTab(userId, itemId)?.index + } + } + + suspend fun saveRememberedTab( + itemId: String, + tabIndex: Int, + ): Unit = + withContext(Dispatchers.IO) { + serverRepository.currentUser?.rowId?.let { userId -> + rememberedTabDao.save(RememberedTab(userId, itemId, tabIndex)) + } + } + } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt index d1ed9eac..25dd1be3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/AppModule.kt @@ -1,18 +1,12 @@ package com.github.damontecres.wholphin.services.hilt import android.content.Context -import androidx.datastore.core.DataStore import androidx.work.WorkManager import com.github.damontecres.wholphin.BuildConfig import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.ServerRepository -import com.github.damontecres.wholphin.preferences.AppPreferences -import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.services.SeerrApi import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory -import com.github.damontecres.wholphin.util.ExceptionHandler -import com.github.damontecres.wholphin.util.RememberTabManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -22,7 +16,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import okhttp3.OkHttpClient import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.android.androidDevice @@ -160,51 +153,6 @@ object AppModule { @Singleton fun apiClient(jellyfin: Jellyfin) = jellyfin.createApi() - /** - * Implementation of [RememberTabManager] which remembers by server, user, & item - */ - @Provides - @Singleton - fun rememberTabManager( - serverRepository: ServerRepository, - appPreference: DataStore, - @IoCoroutineScope scope: CoroutineScope, - ) = object : RememberTabManager { - fun key(itemId: String): String = - serverRepository.current.value.let { - "${it?.server?.id}_${it?.user?.id}_$itemId" - } - - override fun getRememberedTab( - preferences: UserPreferences, - itemId: String, - defaultTab: Int, - ): Int { - if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { - return preferences.appPreferences.interfacePreferences - .getRememberedTabsOrDefault(key(itemId), defaultTab) - } else { - return defaultTab - } - } - - override fun saveRememberedTab( - preferences: UserPreferences, - itemId: String, - tabIndex: Int, - ) { - if (preferences.appPreferences.interfacePreferences.rememberSelectedTab) { - scope.launch(ExceptionHandler()) { - appPreference.updateData { - it.updateInterfacePreferences { - putRememberedTabs(key(itemId), tabIndex) - } - } - } - } - } - } - @Provides @Singleton @IoDispatcher diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt index 62609628..97718089 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/hilt/DatabaseModule.kt @@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.Migrations import com.github.damontecres.wholphin.data.PlaybackEffectDao import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao +import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.preferences.AppPreferences @@ -74,6 +75,10 @@ object DatabaseModule { @Singleton fun playbackEffectDao(db: AppDatabase): PlaybackEffectDao = db.playbackEffectDao() + @Provides + @Singleton + fun rememberedTabDao(db: AppDatabase): RememberedTabDao = db.rememberedTabDao() + @Provides @Singleton fun userPreferencesDataStore( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt index 4e3b13ab..e00a2185 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CircularProgress.kt @@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -15,6 +18,9 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import com.github.damontecres.wholphin.ui.tryRequestFocus +import kotlinx.coroutines.delay +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds @Composable fun CircularProgress(modifier: Modifier = Modifier) { @@ -58,3 +64,41 @@ fun LoadingPage( ) } } + +/** + * Fill the space and take focus, showing a loading indicator after the specified delay + */ +@Composable +fun DelayedLoadingPage( + modifier: Modifier = Modifier, + focusEnabled: Boolean = true, + delay: Duration = 300.milliseconds, +) { + val focusRequester = remember { FocusRequester() } + if (focusEnabled) { + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + } + var show by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(delay) + show = true + } + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxSize() + .focusRequester(focusRequester) + .focusable(focusEnabled), + ) { + if (show) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.border, + modifier = + Modifier + .align(Alignment.Center) + .size(48.dp), + ) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt index f54fda5b..293005dc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabRow.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned @@ -42,26 +41,26 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.ui.util.StringStringProvider import timber.log.Timber @Composable fun TabRow( selectedTabIndex: Int, - tabs: List, - focusRequesters: List, + tabs: List, onClick: (Int) -> Unit, modifier: Modifier = Modifier, ) { val state = rememberLazyListState() LaunchedEffect(selectedTabIndex) { - if (selectedTabIndex >= 0) { + if (selectedTabIndex in tabs.indices) { state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt()) } } var rowHasFocus by remember { mutableStateOf(false) } val currentSelectedTabIndex by rememberUpdatedState(selectedTabIndex) - val currentFocusRequesters by rememberUpdatedState(focusRequesters) +// val currentFocusRequesters by rememberUpdatedState(focusRequesters) val currentOnClick by rememberUpdatedState(onClick) LazyRow( @@ -76,21 +75,21 @@ fun TabRow( // If entering from left or right, use last or first tab // Otherwise use the selected tab val index = currentSelectedTabIndex - val requesters = currentFocusRequesters +// val requesters = currentFocusRequesters Timber.v("onEnter requestedFocusDirection=$requestedFocusDirection, selectedTabIndex=$index") - val focusRequester = + val tab = if (requestedFocusDirection == FocusDirection.Left) { - requesters.lastOrNull() + tabs.lastOrNull() } else if (requestedFocusDirection == FocusDirection.Right) { - requesters.firstOrNull() + tabs.firstOrNull() } else { - requesters.getOrNull(index) + tabs.getOrNull(index) } - (focusRequester ?: FocusRequester.Default).tryRequestFocus() + (tab?.tabFocusRequester ?: FocusRequester.Default).tryRequestFocus() } }, ) { - itemsIndexed(tabs) { index, tabTitle -> + itemsIndexed(tabs) { index, tab -> val interactionSource = remember { MutableInteractionSource() } val onTabClick = remember(index) { @@ -99,12 +98,12 @@ fun TabRow( } } Tab( - title = tabTitle, + title = tab.title.getString(), selected = index == selectedTabIndex, rowActive = rowHasFocus, interactionSource = interactionSource, onClick = onTabClick, - modifier = Modifier.focusRequester(focusRequesters.getOrElse(index) { FocusRequester() }), + modifier = Modifier.focusRequester(tab.tabFocusRequester), ) } } @@ -195,9 +194,11 @@ private fun TabRowPreview() { WholphinTheme { Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { TabRow( - selectedTabIndex = 1, - tabs = listOf("Tab 1", "Tab 2", "Tab 3"), - focusRequesters = listOf(), + selectedTabIndex = -1, + tabs = + remember { + listOf("Tab 1", "Tab 2", "Tab 3").map { TabDetails(StringStringProvider(it)) } + }, onClick = {}, ) Tab( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabbedPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabbedPage.kt new file mode 100644 index 00000000..7d89e666 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/TabbedPage.kt @@ -0,0 +1,140 @@ +package com.github.damontecres.wholphin.ui.components + +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.services.RememberedTabService +import com.github.damontecres.wholphin.services.UserPreferencesService +import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.util.ResStringProvider +import com.github.damontecres.wholphin.ui.util.StringProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber + +@HiltViewModel(assistedFactory = TabViewModel.Factory::class) +class TabViewModel + @AssistedInject + constructor( + private val userPreferencesService: UserPreferencesService, + private val rememberedTabService: RememberedTabService, + private val backdropService: BackdropService, + @param:Assisted private val itemId: String, + ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create(itemId: String): TabViewModel + } + + private val _state = MutableStateFlow(UNSET) + val state: StateFlow = _state + + init { + viewModelScope.launchIO { + val startingTab = + if (shouldRememberTabs()) { + Timber.v("Getting remembered tab for %s", itemId) + try { + rememberedTabService.getRememberedTab(itemId) + } catch (ex: Exception) { + Timber.e(ex, "Error getting tab for %s", itemId) + null + } ?: 0 + } else { + 0 + } + _state.value = startingTab + } + } + + private suspend fun shouldRememberTabs(): Boolean = + userPreferencesService + .getCurrent() + .appPreferences.interfacePreferences.rememberSelectedTab + + fun updateSelectedTabIndex(tabIndex: Int) { + _state.value = tabIndex + viewModelScope.launchIO { + backdropService.clearBackdrop() + if (shouldRememberTabs()) { + Timber.v("Saving remembered tab for %s: %s", itemId, tabIndex) + rememberedTabService.saveRememberedTab(itemId, tabIndex) + } + } + } + } + +data class TabDetails( + val title: StringProvider, + val tabFocusRequester: FocusRequester = FocusRequester(), + val contentFocusRequester: FocusRequester = FocusRequester(), +) { + constructor( + @StringRes stringResId: Int, + ) : this(ResStringProvider(stringResId)) +} + +const val UNSET = -1234 + +/** + * A page showing multiple tabs + * + * This handles remembering the selected tabs via [TabViewModel] if needed + */ +@Composable +fun TabbedPage( + itemId: String, + tabs: List, + modifier: Modifier = Modifier, + showTabs: Boolean = true, + viewModel: TabViewModel = + hiltViewModel( + key = itemId, + creationCallback = { it.create(itemId) }, + ), + tabContent: @Composable (Int, TabDetails) -> Unit, +) { + val selectedTabIndex by viewModel.state.collectAsState() + Column( + modifier = modifier, + ) { + AnimatedVisibility( + showTabs, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = + Modifier + .padding(vertical = 16.dp), + tabs = tabs, + onClick = viewModel::updateSelectedTabIndex, + ) + } + selectedTabIndex.let { tabIndex -> + if (tabIndex >= 0) { + tabContent.invoke(tabIndex, tabs[tabIndex]) + } else { + DelayedLoadingPage(focusEnabled = false) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt index b6f5b19a..18af298b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -1,25 +1,15 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -32,20 +22,23 @@ import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.CollectionFolderView import com.github.damontecres.wholphin.ui.components.ErrorMessage -import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.components.ViewOptions import com.github.damontecres.wholphin.ui.data.VideoSortOptions import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid import com.github.damontecres.wholphin.ui.launchIO -import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.tryRequestFocus -import com.github.damontecres.wholphin.util.RememberTabManager +import com.github.damontecres.wholphin.ui.util.StringStringProvider +import com.github.damontecres.wholphin.util.DataLoadingState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.liveTvApi +import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -56,24 +49,35 @@ class LiveTvCollectionViewModel val api: ApiClient, val serverRepository: ServerRepository, val navigationManager: NavigationManager, - val rememberTabManager: RememberTabManager, val backdropService: BackdropService, - ) : ViewModel(), - RememberTabManager by rememberTabManager { - val recordingFolders = MutableStateFlow>(emptyList()) + ) : ViewModel() { + val state = MutableStateFlow(LiveTvRecordingFolderState()) init { viewModelScope.launchIO { - val folders = - api.liveTvApi - .getRecordingFolders(userId = serverRepository.currentUser?.id) - .content.items - .map { TabId(it.name ?: "Recordings", it.id) } - recordingFolders.value = folders + try { + val folders = + api.liveTvApi + .getRecordingFolders(userId = serverRepository.currentUser?.id) + .content.items + .map { TabId(it.name ?: "Recordings", it.id) } + state.value = + LiveTvRecordingFolderState(DataLoadingState.Success(folders)) + } catch (ex: CancellationException) { + throw ex + } catch (ex: Exception) { + Timber.e(ex, "Error fetching recording folders") + state.value = + LiveTvRecordingFolderState(DataLoadingState.Error(ex)) + } } } } +data class LiveTvRecordingFolderState( + val folders: DataLoadingState> = DataLoadingState.Pending, +) + data class TabId( val title: String, val id: UUID, @@ -86,103 +90,91 @@ fun CollectionFolderLiveTv( modifier: Modifier = Modifier, viewModel: LiveTvCollectionViewModel = hiltViewModel(), ) { - val rememberedTabIndex = - remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } - val folders by viewModel.recordingFolders.collectAsState() + val state by viewModel.state.collectAsState() - val tvGuideStr = stringResource(R.string.tv_guide) - val tvDvrStr = stringResource(R.string.tv_dvr_schedule) - val tabs = - remember(folders) { - listOf( - TabId(tvGuideStr, UUID.randomUUID()), - TabId(tvDvrStr, UUID.randomUUID()), - ) + folders + when (val st = state.folders) { + is DataLoadingState.Error -> { + ErrorMessage(st, modifier) } - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val focusRequester = remember { FocusRequester() } - val tabFocusRequesters = remember(tabs.size) { List(tabs.size) { FocusRequester() } } - - val firstTabFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } - - LaunchedEffect(selectedTabIndex) { - logTab("livetv", selectedTabIndex) - viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) - viewModel.backdropService.clearBackdrop() - } - val onClickItem = { position: Int, item: BaseItem -> - viewModel.navigationManager.navigateTo(item.destination()) - } - - var showHeader by rememberSaveable { mutableStateOf(true) } - - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } - Column( - modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(vertical = 16.dp) - .focusRequester(firstTabFocusRequester), - tabs = tabs.map { it.title }, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, - ) + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + LoadingPage(modifier) } - when (selectedTabIndex) { - 0 -> { - TvGuideGrid( - preferences = preferences, - onRowPosition = { - showHeader = it <= 0 - }, - Modifier - .fillMaxSize() - .focusRequester(focusRequester), - ) - } - 1 -> { - DvrSchedule( - requestFocusAfterLoading = true, - focusRequesterOnEmpty = tabFocusRequesters[1], - modifier = - Modifier - .fillMaxSize() - .focusRequester(focusRequester), - ) - } + is DataLoadingState.Success> -> { + val tabs = + remember { + listOf( + TabDetails(R.string.tv_guide), + TabDetails(R.string.tv_dvr_schedule), + ) + + st.data.map { + TabDetails(StringStringProvider(it.title)) + } + } + val onClickItem = + remember { + { position: Int, item: BaseItem -> + viewModel.navigationManager.navigateTo(item.destination()) + } + } - else -> { - val folderIndex = selectedTabIndex - 2 - if (folderIndex in folders.indices) { - CollectionFolderView( - preferences = preferences, - onClickItem = onClickItem, - itemId = folders[folderIndex].id, - initialFilter = CollectionFolderFilter(), - showTitle = false, - recursive = false, - sortOptions = VideoSortOptions, - modifier = Modifier, - positionCallback = { columns, position -> - showHeader = position < columns - }, - playEnabled = false, - defaultViewOptions = ViewOptions(), - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), - ) - } else { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + var showHeader by rememberSaveable { mutableStateOf(true) } + TabbedPage( + itemId = destination.itemId.toString(), + tabs = tabs, + modifier = modifier, + showTabs = showHeader, + ) { tabIndex, tabDetails -> + when (tabIndex) { + 0 -> { + TvGuideGrid( + preferences = preferences, + onRowPosition = { + showHeader = it <= 0 + }, + Modifier + .fillMaxSize() + .focusRequester(tabDetails.contentFocusRequester), + ) + } + + 1 -> { + DvrSchedule( + requestFocusAfterLoading = true, + focusRequesterOnEmpty = tabDetails.tabFocusRequester, + modifier = + Modifier + .fillMaxSize() + .focusRequester(tabDetails.contentFocusRequester), + ) + } + + else -> { + val folderIndex = tabIndex - 2 + if (folderIndex in st.data.indices) { + CollectionFolderView( + preferences = preferences, + onClickItem = onClickItem, + itemId = st.data[folderIndex].id, + initialFilter = CollectionFolderFilter(), + showTitle = false, + recursive = false, + sortOptions = VideoSortOptions, + modifier = Modifier.focusRequester(tabDetails.contentFocusRequester), + positionCallback = { columns, position -> + showHeader = position < columns + }, + playEnabled = false, + defaultViewOptions = ViewOptions(), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, + ) + } else { + ErrorMessage("Invalid tab index $tabIndex", null) + } + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt index 6651ae9d..cb1d1812 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMovie.kt @@ -1,24 +1,14 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.CollectionFolderFilter @@ -28,11 +18,11 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderView import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.GenreCardGrid import com.github.damontecres.wholphin.ui.components.RecommendedMovie -import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster import com.github.damontecres.wholphin.ui.data.MovieSortOptions import com.github.damontecres.wholphin.ui.data.VideoSortOptions -import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import org.jellyfin.sdk.model.api.BaseItemKind @@ -45,51 +35,25 @@ fun CollectionFolderMovie( modifier: Modifier = Modifier, preferencesViewModel: PreferencesViewModel = hiltViewModel(), ) { - val rememberedTabIndex = - remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } - val tabs = - listOf( - stringResource(R.string.recommended), - stringResource(R.string.library), - stringResource(R.string.collections), - stringResource(R.string.genres), - ) - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val focusRequester = remember { FocusRequester() } - val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } - - val firstTabFocusRequester = remember { FocusRequester() } -// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } - - LaunchedEffect(selectedTabIndex) { - logTab("movie", selectedTabIndex) - preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) - preferencesViewModel.backdropService.clearBackdrop() - } + remember { + listOf( + TabDetails(R.string.recommended), + TabDetails(R.string.library), + TabDetails(R.string.collections), + TabDetails(R.string.genres), + ) + } var showHeader by rememberSaveable { mutableStateOf(true) } - Column( + TabbedPage( + itemId = destination.itemId.toString(), + tabs = tabs, modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(vertical = 16.dp) - .focusRequester(firstTabFocusRequester), - tabs = tabs, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, - ) - } - when (selectedTabIndex) { + showTabs = showHeader, + ) { tabIndex, tabDetails -> + when (tabIndex) { // Recommended 0 -> { RecommendedMovie( @@ -101,7 +65,7 @@ fun CollectionFolderMovie( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } @@ -128,12 +92,12 @@ fun CollectionFolderMovie( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = true, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -160,12 +124,12 @@ fun CollectionFolderMovie( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -178,12 +142,12 @@ fun CollectionFolderMovie( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } else -> { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + ErrorMessage("Invalid tab index $tabIndex", null) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt index 274e8343..431e8e8e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderMusic.kt @@ -1,24 +1,14 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -36,17 +26,16 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.GenreCardGrid import com.github.damontecres.wholphin.ui.components.GridClickActions import com.github.damontecres.wholphin.ui.components.RecommendedMusic -import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare import com.github.damontecres.wholphin.ui.data.AlbumSortOptions import com.github.damontecres.wholphin.ui.data.ArtistSortOptions import com.github.damontecres.wholphin.ui.data.SongSortOptions import com.github.damontecres.wholphin.ui.launchDefault -import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetItemsRequestHandler -import com.github.damontecres.wholphin.util.RememberTabManager import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -66,10 +55,8 @@ class CollectionFolderMusicViewModel private val musicService: MusicService, private val navigationManager: NavigationManager, val backdropService: BackdropService, - private val rememberTabManager: RememberTabManager, @Assisted private val itemId: UUID, - ) : ViewModel(), - RememberTabManager by rememberTabManager { + ) : ViewModel() { @AssistedFactory interface Factory { fun create(itemId: UUID): CollectionFolderMusicViewModel @@ -132,30 +119,16 @@ fun CollectionFolderMusic( creationCallback = { it.create(destination.itemId) }, ), ) { - val rememberedTabIndex = - remember { viewModel.getRememberedTab(preferences, destination.itemId, 0) } - val tabs = - listOf( - stringResource(R.string.recommended), - stringResource(R.string.albums), - stringResource(R.string.artists), - stringResource(R.string.genres), - stringResource(R.string.songs), - ) - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val focusRequester = remember { FocusRequester() } - val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } - - val firstTabFocusRequester = remember { FocusRequester() } -// LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } - - LaunchedEffect(selectedTabIndex) { - logTab("music", selectedTabIndex) - viewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) - viewModel.backdropService.clearBackdrop() - } - + remember { + listOf( + TabDetails(R.string.recommended), + TabDetails(R.string.albums), + TabDetails(R.string.artists), + TabDetails(R.string.genres), + TabDetails(R.string.songs), + ) + } var showHeader by rememberSaveable { mutableStateOf(true) } val actions = @@ -167,27 +140,13 @@ fun CollectionFolderMusic( onClickPlayRemoteButton = viewModel::onClickPlayRemoteButton, ) } - - Column( + TabbedPage( + itemId = destination.itemId.toString(), + tabs = tabs, modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(vertical = 16.dp) - .focusRequester(firstTabFocusRequester), - tabs = tabs, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, - ) - } - when (selectedTabIndex) { + showTabs = showHeader, + ) { tabIndex, tabDetails -> + when (tabIndex) { // Recommended 0 -> { RecommendedMusic( @@ -199,7 +158,7 @@ fun CollectionFolderMusic( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } @@ -224,12 +183,12 @@ fun CollectionFolderMusic( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = true, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -254,12 +213,12 @@ fun CollectionFolderMusic( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -272,7 +231,7 @@ fun CollectionFolderMusic( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } @@ -297,17 +256,17 @@ fun CollectionFolderMusic( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = true, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } else -> { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + ErrorMessage("Invalid tab index $tabIndex", null) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt index 35875351..2f8fb909 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderTv.kt @@ -1,102 +1,77 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.filter.DefaultTvFilterOptions import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.CollectionFolderView import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.GenreCardGrid import com.github.damontecres.wholphin.ui.components.RecommendedTvShow import com.github.damontecres.wholphin.ui.components.StudioCardGrid -import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster import com.github.damontecres.wholphin.ui.data.SeriesSortOptions -import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.Destination -import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel -import com.github.damontecres.wholphin.ui.tryRequestFocus +import dagger.hilt.android.lifecycle.HiltViewModel import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType +import javax.inject.Inject + +@HiltViewModel +class CollectionFolderTvViewModel + @Inject + constructor( + val navigationManager: NavigationManager, + ) : ViewModel() @Composable fun CollectionFolderTv( preferences: UserPreferences, destination: Destination.MediaItem, modifier: Modifier = Modifier, - preferencesViewModel: PreferencesViewModel = hiltViewModel(), + viewModel: CollectionFolderTvViewModel = hiltViewModel(), ) { - val rememberedTabIndex = - remember { preferencesViewModel.getRememberedTab(preferences, destination.itemId, 0) } - val tabs = - listOf( - stringResource(R.string.recommended), - stringResource(R.string.library), - stringResource(R.string.genres), - stringResource(R.string.studios), - ) - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val focusRequester = remember { FocusRequester() } - val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } - - val firstTabFocusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } - - LaunchedEffect(selectedTabIndex) { - logTab("tv", selectedTabIndex) - preferencesViewModel.saveRememberedTab(preferences, destination.itemId, selectedTabIndex) - preferencesViewModel.backdropService.clearBackdrop() - } - - val onClickItem = { item: BaseItem -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - } + remember { + listOf( + TabDetails(R.string.recommended), + TabDetails(R.string.library), + TabDetails(R.string.genres), + TabDetails(R.string.studios), + ) + } + val onClickItem = + remember { + { position: Int, item: BaseItem -> + viewModel.navigationManager.navigateTo(item.destination()) + } + } var showHeader by rememberSaveable { mutableStateOf(true) } - Column( + TabbedPage( + itemId = destination.itemId.toString(), + tabs = tabs, modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(vertical = 16.dp) - .focusRequester(firstTabFocusRequester), - tabs = tabs, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, - ) - } - when (selectedTabIndex) { + showTabs = showHeader, + ) { tabIndex, tabDetails -> + when (tabIndex) { // Recommended 0 -> { RecommendedTvShow( @@ -108,7 +83,7 @@ fun CollectionFolderTv( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } @@ -132,15 +107,13 @@ fun CollectionFolderTv( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, - onClickItem = { _, item -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - }, + onClickItem = onClickItem, playEnabled = false, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -153,7 +126,7 @@ fun CollectionFolderTv( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } @@ -165,12 +138,12 @@ fun CollectionFolderTv( modifier = Modifier .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), ) } else -> { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + ErrorMessage("Invalid tab index $tabIndex", null) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt index b0124313..f9aedff3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/FavoritesPage.kt @@ -1,36 +1,30 @@ package com.github.damontecres.wholphin.ui.detail -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.filter.DefaultForFavoritesFilterOptions -import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.CollectionFolderFilter import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.ui.components.CollectionFolderView import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.GridClickActions -import com.github.damontecres.wholphin.ui.components.TabRow +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.components.ViewOptionsPoster import com.github.damontecres.wholphin.ui.components.ViewOptionsSquare import com.github.damontecres.wholphin.ui.components.ViewOptionsWide @@ -39,86 +33,55 @@ import com.github.damontecres.wholphin.ui.data.MovieSortOptions import com.github.damontecres.wholphin.ui.data.SeriesSortOptions import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.data.VideoSortOptions -import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.nav.NavDrawerItem -import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel -import com.github.damontecres.wholphin.ui.tryRequestFocus +import dagger.hilt.android.lifecycle.HiltViewModel import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.SortOrder +import javax.inject.Inject + +@HiltViewModel +class FavoritesViewModel + @Inject + constructor( + val navigationManager: NavigationManager, + ) : ViewModel() @Composable fun FavoritesPage( preferences: UserPreferences, modifier: Modifier = Modifier, - preferencesViewModel: PreferencesViewModel = hiltViewModel(), + viewModel: FavoritesViewModel = hiltViewModel(), ) { - val uiPrefs = preferences.appPreferences.interfacePreferences - val rememberedTabIndex = + val tabs = remember { - preferencesViewModel.getRememberedTab( - preferences, - NavDrawerItem.Favorites.id, - 0, + listOf( + TabDetails(R.string.movies), + TabDetails(R.string.tv_shows), + TabDetails(R.string.episodes), + TabDetails(R.string.videos), + TabDetails(R.string.playlists), + TabDetails(R.string.people), + ) + } + val actions = + remember { + GridClickActions( + onClickItem = { _, item -> viewModel.navigationManager.navigateTo(item.destination()) }, ) } - val tabs = - listOf( - stringResource(R.string.movies), - stringResource(R.string.tv_shows), - stringResource(R.string.episodes), - stringResource(R.string.videos), - stringResource(R.string.playlists), - stringResource(R.string.people), - ) - val tabFocusRequesters = remember { List(tabs.size) { FocusRequester() } } - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val focusRequester = remember { FocusRequester() } - - LaunchedEffect(selectedTabIndex) { - logTab("favorites", selectedTabIndex) - preferencesViewModel.saveRememberedTab( - preferences, - NavDrawerItem.Favorites.id, - selectedTabIndex, - ) - preferencesViewModel.backdropService.clearBackdrop() - } var showHeader by rememberSaveable { mutableStateOf(true) } - val onClickItem = { item: BaseItem -> - preferencesViewModel.navigationManager.navigateTo(item.destination()) - } - - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } - Column( +// LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + TabbedPage( + itemId = NavDrawerItem.Favorites.id, + tabs = tabs, + showTabs = showHeader, modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp), - tabs = tabs, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, - ) - } - val actions = - remember { - GridClickActions( - onClickItem = { _, item -> onClickItem.invoke(item) }, - ) - } - + ) { tabIndex, tabDetails -> // TODO playEnabled = true for movies & episodes - when (selectedTabIndex) { + when (tabIndex) { // Movies 0 -> { CollectionFolderView( @@ -141,13 +104,13 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -173,13 +136,13 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -206,13 +169,13 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -238,13 +201,13 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -270,13 +233,13 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = DefaultForFavoritesFilterOptions, - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } @@ -307,18 +270,18 @@ fun FavoritesPage( Modifier .padding(start = 16.dp) .fillMaxSize() - .focusRequester(focusRequester), + .focusRequester(tabDetails.contentFocusRequester), positionCallback = { columns, position -> showHeader = position < columns }, playEnabled = false, filterOptions = listOf(), - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, ) } else -> { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + ErrorMessage("Invalid tab index $tabIndex", null) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt index 5d071387..a3ca1358 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/series/SeriesOverviewContent.kt @@ -57,6 +57,7 @@ import com.github.damontecres.wholphin.ui.cards.PersonRow import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.HeaderUtils import com.github.damontecres.wholphin.ui.components.LoadingPage +import com.github.damontecres.wholphin.ui.components.TabDetails import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.components.TitleOrLogo import com.github.damontecres.wholphin.ui.ifElse @@ -64,6 +65,7 @@ import com.github.damontecres.wholphin.ui.logTab import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.ui.util.StringStringProvider import com.github.damontecres.wholphin.ui.util.rememberDelayedNestedScroll import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.MediaSourceInfo @@ -120,13 +122,21 @@ fun SeriesOverviewContent( var requestFocusAfterSeason by remember { mutableStateOf(false) } val seasonStr = stringResource(R.string.tv_season) + val tabFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } } + val contentFocusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } } val tabs = - seasons.map { season -> - season?.name - ?: season?.data?.indexNumber?.let { "$seasonStr $it" } - ?: "" + seasons.mapIndexed { index, season -> + TabDetails( + title = + StringStringProvider( + season?.name + ?: season?.data?.indexNumber?.let { "$seasonStr $it" } + ?: "", + ), + tabFocusRequester = tabFocusRequesters[index], + contentFocusRequester = contentFocusRequesters[index], + ) } - val focusRequesters = remember(seasons) { List(seasons.size) { FocusRequester() } } val currentOnChangeSeason by rememberUpdatedState(onChangeSeason) @@ -172,7 +182,6 @@ fun SeriesOverviewContent( requestFocusAfterSeason = true } }, - focusRequesters = focusRequesters, modifier = Modifier .focusRequester(tabRowFocusRequester) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt index eba02b62..9751a6d0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/discover/DiscoverPage.kt @@ -1,111 +1,78 @@ package com.github.damontecres.wholphin.ui.discover -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.compose.ui.focus.focusRequester import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect import com.github.damontecres.wholphin.ui.components.ErrorMessage -import com.github.damontecres.wholphin.ui.components.TabRow -import com.github.damontecres.wholphin.ui.logTab +import com.github.damontecres.wholphin.ui.components.TabDetails +import com.github.damontecres.wholphin.ui.components.TabbedPage import com.github.damontecres.wholphin.ui.nav.NavDrawerItem -import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel @Composable fun DiscoverPage( preferences: UserPreferences, modifier: Modifier = Modifier, - preferencesViewModel: PreferencesViewModel = hiltViewModel(), ) { - val rememberedTabIndex = - remember { preferencesViewModel.getRememberedTab(preferences, NavDrawerItem.Discover.id, 0) } - val tabs = - listOf( - stringResource(R.string.discover), - stringResource(R.string.request), - stringResource(R.string.search), - ) - var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } - val tabFocusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } } - - LaunchedEffect(selectedTabIndex) { - logTab("discover", selectedTabIndex) - preferencesViewModel.saveRememberedTab(preferences, NavDrawerItem.Discover.id, selectedTabIndex) - } - OneTimeLaunchedEffect { preferencesViewModel.backdropService.clearBackdrop() } - - var showHeader by rememberSaveable { mutableStateOf(true) } - - Column( - modifier = modifier, - ) { - AnimatedVisibility( - showHeader, - enter = expandVertically(), - exit = shrinkVertically(), - ) { - TabRow( - selectedTabIndex = selectedTabIndex, - modifier = - Modifier - .padding(start = 32.dp, top = 16.dp, bottom = 16.dp), - tabs = tabs, - onClick = { selectedTabIndex = it }, - focusRequesters = tabFocusRequesters, + remember { + listOf( + TabDetails(R.string.discover), + TabDetails(R.string.request), + TabDetails(R.string.search), ) } - when (selectedTabIndex) { + var showHeader by rememberSaveable { mutableStateOf(true) } + + TabbedPage( + itemId = NavDrawerItem.Discover.id, + tabs = tabs, + modifier = modifier, + showTabs = showHeader, + ) { tabIndex, tabDetails -> + when (tabIndex) { // Discover 0 -> { SeerrDiscoverPage( preferences = preferences, modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .focusRequester(tabDetails.contentFocusRequester), ) } // Requests 1 -> { SeerrRequestsPage( - focusRequesterOnEmpty = tabFocusRequesters.getOrNull(selectedTabIndex), + focusRequesterOnEmpty = tabDetails.tabFocusRequester, modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .focusRequester(tabDetails.contentFocusRequester), ) } // Search 2 -> { - LaunchedEffect(Unit) { - preferencesViewModel.backdropService.clearBackdrop() - } DiscoverSearchPage( preferences = preferences, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .focusRequester(tabDetails.contentFocusRequester), ) } else -> { - ErrorMessage("Invalid tab index $selectedTabIndex", null) + ErrorMessage("Invalid tab index $tabIndex", null) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index c3ff42da..aec58ce1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -86,6 +86,7 @@ import com.github.damontecres.wholphin.ui.cards.ItemRowTitle import com.github.damontecres.wholphin.ui.cards.SeasonCard import com.github.damontecres.wholphin.ui.components.ExpandableFaButton import com.github.damontecres.wholphin.ui.components.SearchEditTextBox +import com.github.damontecres.wholphin.ui.components.TabDetails import com.github.damontecres.wholphin.ui.components.TabRow import com.github.damontecres.wholphin.ui.components.VoiceInputManager import com.github.damontecres.wholphin.ui.components.VoiceSearchButton @@ -518,6 +519,15 @@ fun SearchPage( } } } + + val tabs = + remember { + listOf( + TabDetails(R.string.library), + TabDetails(R.string.discover), + ) + } + Column( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp), @@ -610,12 +620,7 @@ fun SearchPage( ) { TabRow( selectedTabIndex = selectedTab, - tabs = - listOf( - stringResource(R.string.library), - stringResource(R.string.discover), - ), - focusRequesters = tabFocusRequesters, + tabs = tabs, onClick = { selectedTab = it val row = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 3e5ab947..db723522 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -359,6 +359,7 @@ fun DestinationContent( } Destination.Discover -> { + LaunchedEffect(Unit) { onClearBackdrop.invoke() } DiscoverPage( preferences = preferences, modifier = modifier, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt index 718febf3..f0e396cb 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesViewModel.kt @@ -28,7 +28,6 @@ import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState -import com.github.damontecres.wholphin.util.RememberTabManager import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow @@ -52,14 +51,12 @@ class PreferencesViewModel val navigationManager: NavigationManager, val backdropService: BackdropService, val screensaverService: ScreensaverService, - private val rememberTabManager: RememberTabManager, private val serverRepository: ServerRepository, private val seerrServerRepository: SeerrServerRepository, private val deviceInfo: DeviceInfo, private val clientInfo: ClientInfo, private val updateChecker: UpdateChecker, - ) : ViewModel(), - RememberTabManager by rememberTabManager { + ) : ViewModel() { val currentUser get() = serverRepository.currentUserFlow.stateIn( diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt b/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt deleted file mode 100644 index 46ec3606..00000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/RememberTabManager.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.damontecres.wholphin.util - -import com.github.damontecres.wholphin.preferences.UserPreferences -import java.util.UUID - -/** - * Functions to remember tabs choices for a library - */ -interface RememberTabManager { - /** - * If enabled, get the remembered tab index for the given item - */ - fun getRememberedTab( - preferences: UserPreferences, - itemId: UUID, - defaultTab: Int, - ): Int = getRememberedTab(preferences, itemId.toString(), defaultTab) - - /** - * If enabled, get the remembered tab index for the given item - */ - fun getRememberedTab( - preferences: UserPreferences, - itemId: String, - defaultTab: Int, - ): Int - - /** - * If enabled, save the remembered tab index for the given item - */ - fun saveRememberedTab( - preferences: UserPreferences, - itemId: UUID, - tabIndex: Int, - ) = saveRememberedTab(preferences, itemId.toString(), tabIndex) - - /** - * If enabled, save the remembered tab index for the given item - */ - fun saveRememberedTab( - preferences: UserPreferences, - itemId: String, - tabIndex: Int, - ) -} diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index bf98744a..12631e17 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -180,7 +180,7 @@ enum DisplayToggle{ message InterfacePreferences { ThemeSongVolume play_theme_songs = 1; bool remember_selected_tab = 2; - map remembered_tabs = 3; + map remembered_tabs = 3 [deprecated = true]; AppThemeColors app_theme_colors = 4; bool nav_drawer_switch_on_focus = 5; bool show_clock = 6; diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt index 8e43a4b0..e2026a4d 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/TestModule.kt @@ -19,6 +19,7 @@ import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao import com.github.damontecres.wholphin.data.Migrations import com.github.damontecres.wholphin.data.PlaybackEffectDao import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao +import com.github.damontecres.wholphin.data.RememberedTabDao import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.data.ServerRepository @@ -35,7 +36,6 @@ import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.services.hilt.IoDispatcher import com.github.damontecres.wholphin.services.hilt.StandardOkHttpClient import com.github.damontecres.wholphin.util.CoroutineContextApiClientFactory -import com.github.damontecres.wholphin.util.RememberTabManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -166,17 +166,6 @@ object TestModule { return api } - /** - * Implementation of [RememberTabManager] which remembers by server, user, & item - */ - @Provides - @Singleton - fun rememberTabManager( - serverRepository: ServerRepository, - appPreference: DataStore, - @IoCoroutineScope scope: CoroutineScope, - ): RememberTabManager = mockk() - @Provides @Singleton @IoDispatcher @@ -267,6 +256,10 @@ object TestDatabaseModule { @Singleton fun playbackEffectDao(db: AppDatabase): PlaybackEffectDao = db.playbackEffectDao() + @Provides + @Singleton + fun rememberedTabDao(db: AppDatabase): RememberedTabDao = db.rememberedTabDao() + @Provides @Singleton fun userPreferencesDataStore(