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
This commit is contained in:
Damontecres 2026-06-08 13:45:38 -04:00 • committed by GitHub
parent a4ee1e5175
commit b49926f80b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1379 additions and 599 deletions

View file

@ -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<AppPreferences>,
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<UUID, JellyfinUser?>()
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")
}
}
}
}

View file

@ -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))
}
}
}

View file

@ -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<AppPreferences>,
@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

View file

@ -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(