From a174d981a86470ea706d14031073907cc8199bc5 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 24 Jan 2026 18:27:58 -0500 Subject: [PATCH] Refactoring to expose current configuration --- .../wholphin/data/ServerRepository.kt | 22 -- .../wholphin/data/model/HomeRowConfig.kt | 16 +- .../wholphin/services/HomeSettingsService.kt | 264 +++++++++++++++--- .../services/SeerrServerRepository.kt | 83 +++--- .../ui/main/settings/HomeSettingsViewModel.kt | 196 ++----------- 5 files changed, 312 insertions(+), 269 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt index c0cd962d..f66dcfca 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi import org.jellyfin.sdk.api.client.extensions.systemApi import org.jellyfin.sdk.api.client.extensions.userApi import org.jellyfin.sdk.model.api.AuthenticationResult @@ -103,27 +102,6 @@ class ServerRepository currentUserId = updatedUser.id.toServerString() }.build() } - val settings = - apiClient.displayPreferencesApi - .getDisplayPreferences( - displayPreferencesId = "settings", - userId = user.id, - client = "wholphin", - ).content - val newSettings = - settings.copy( - customPrefs = - settings.customPrefs.toMutableMap().apply { - put("test_key", "test_value") - }, - ) - apiClient.displayPreferencesApi.updateDisplayPreferences( - displayPreferencesId = "settings", - userId = user.id, - client = "wholphin", - data = newSettings, - ) - Timber.v("settings=$settings") withContext(Dispatchers.Main) { _current.value = CurrentUser(updatedServer, updatedUser) _currentUserDto.value = userDto diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt index 7c1faf74..13d96785 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt @@ -144,9 +144,21 @@ data class HomeRowConfigDisplay( @Serializable @SerialName("HomePageSettings") data class HomePageSettings( - val version: Int = 1, val rows: List, -) + val version: Int = 1, +) { + companion object { + val EMPTY = HomePageSettings(listOf()) + } +} + +data class HomePageResolvedSettings( + val rows: List, +) { + companion object { + val EMPTY = HomePageResolvedSettings(listOf()) + } +} @Serializable data class HomeRowViewOptions( diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt index 31744dda..fc61192b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/HomeSettingsService.kt @@ -2,17 +2,28 @@ package com.github.damontecres.wholphin.services import android.content.Context import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.NavDrawerItemRepository +import com.github.damontecres.wholphin.data.model.HomePageResolvedSettings import com.github.damontecres.wholphin.data.model.HomePageSettings +import com.github.damontecres.wholphin.data.model.HomeRowConfig +import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay +import com.github.damontecres.wholphin.data.model.HomeRowViewOptions +import com.github.damontecres.wholphin.ui.main.settings.Library +import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem import com.github.damontecres.wholphin.ui.toServerString import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.encodeToStream import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.UUID +import org.jellyfin.sdk.model.serializer.toUUID +import timber.log.Timber import java.io.File import javax.inject.Inject import javax.inject.Singleton @@ -23,7 +34,8 @@ class HomeSettingsService constructor( @param:ApplicationContext private val context: Context, private val api: ApiClient, - private val serverRepository: ServerRepository, + private val userPreferencesService: UserPreferencesService, + private val navDrawerItemRepository: NavDrawerItemRepository, ) { val jsonParser = Json { @@ -31,32 +43,35 @@ class HomeSettingsService ignoreUnknownKeys = true } + val currentSettings = MutableStateFlow(HomePageResolvedSettings.EMPTY) + suspend fun saveToServer( + userId: UUID, settings: HomePageSettings, displayPreferencesId: String = DISPLAY_PREF_ID, ) { - serverRepository.currentUser.value?.let { user -> - val current = getDisplayPreferences(user.id, DISPLAY_PREF_ID) - val customPrefs = - current.customPrefs.toMutableMap().apply { - put(CUSTOM_PREF_ID, jsonParser.encodeToString(settings)) - } - api.displayPreferencesApi.updateDisplayPreferences( - displayPreferencesId = displayPreferencesId, - userId = user.id, - client = context.getString(R.string.app_name), - data = current.copy(customPrefs = customPrefs), - ) - } + val current = getDisplayPreferences(userId, DISPLAY_PREF_ID) + val customPrefs = + current.customPrefs.toMutableMap().apply { + put(CUSTOM_PREF_ID, jsonParser.encodeToString(settings)) + } + api.displayPreferencesApi.updateDisplayPreferences( + displayPreferencesId = displayPreferencesId, + userId = userId, + client = context.getString(R.string.app_name), + data = current.copy(customPrefs = customPrefs), + ) } - suspend fun loadFromServer(displayPreferencesId: String = DISPLAY_PREF_ID): HomePageSettings? = - serverRepository.currentUser.value?.let { user -> - val current = getDisplayPreferences(user.id, displayPreferencesId) - current.customPrefs[DISPLAY_PREF_ID]?.let { - Json.decodeFromString(it) - } + suspend fun loadFromServer( + userId: UUID, + displayPreferencesId: String = DISPLAY_PREF_ID, + ): HomePageSettings? { + val current = getDisplayPreferences(userId, displayPreferencesId) + return current.customPrefs[DISPLAY_PREF_ID]?.let { + Json.decodeFromString(it) } + } private suspend fun getDisplayPreferences( userId: UUID, @@ -71,27 +86,202 @@ class HomeSettingsService private fun filename(userId: UUID) = "${CUSTOM_PREF_ID}_${userId.toServerString()}.json" @OptIn(ExperimentalSerializationApi::class) - suspend fun saveToLocal(settings: HomePageSettings) { - serverRepository.currentUser.value?.let { user -> - val dir = File(context.filesDir, CUSTOM_PREF_ID) - dir.mkdirs() - File(dir, filename(user.id)).outputStream().use { - jsonParser.encodeToStream(settings, it) - } + suspend fun saveToLocal( + userId: UUID, + settings: HomePageSettings, + ) { + val dir = File(context.filesDir, CUSTOM_PREF_ID) + dir.mkdirs() + File(dir, filename(userId)).outputStream().use { + jsonParser.encodeToStream(settings, it) } } @OptIn(ExperimentalSerializationApi::class) - suspend fun loadFromLocal(): HomePageSettings? = - serverRepository.currentUser.value?.let { user -> - val dir = File(context.filesDir, CUSTOM_PREF_ID) - val file = File(dir, filename(user.id)) - if (file.exists()) { - file.inputStream().use { - jsonParser.decodeFromStream(it) - } + suspend fun loadFromLocal(userId: UUID): HomePageSettings? { + val dir = File(context.filesDir, CUSTOM_PREF_ID) + val file = File(dir, filename(userId)) + return if (file.exists()) { + file.inputStream().use { + jsonParser.decodeFromStream(it) + } + } else { + null + } + } + + suspend fun updateCurrent(userId: UUID) { + Timber.v("Getting setting for %s", userId) + // User local then server/remote otherwise create a default + val settings = loadFromLocal(userId) ?: loadFromServer(userId) + val resolvedSettings = + if (settings != null) { + Timber.v("Found settings") + // Resolve + val resolvedRows = settings.rows.map { convert(it) } + HomePageResolvedSettings(resolvedRows) } else { - null + createDefault(userId) + } + + currentSettings.update { resolvedSettings } + } + + suspend fun createDefault(userId: UUID): HomePageResolvedSettings { + Timber.v("Creating default settings") + val navDrawerItems = navDrawerItemRepository.getNavDrawerItems() + val libraries = + navDrawerItems + .filter { it is ServerNavDrawerItem } + .map { + it as ServerNavDrawerItem + Library(it.itemId, it.name, it.type) + } + val prefs = + userPreferencesService.getCurrent().appPreferences.homePagePreferences + val includedIds = + navDrawerItemRepository + .getFilteredNavDrawerItems(navDrawerItems) + .filter { it is ServerNavDrawerItem } + .mapIndexed { index, it -> + val id = (it as ServerNavDrawerItem).itemId + val name = libraries.firstOrNull { it.itemId == id }?.name + val title = + name?.let { context.getString(R.string.recently_added_in, it) } + ?: context.getString(R.string.recently_added) + HomeRowConfigDisplay( + title, + HomeRowConfig.RecentlyAdded( + index, + id, + HomeRowViewOptions(), + ), + ) + } + val continueWatchingRows = + if (prefs.combineContinueNext) { + listOf( + HomeRowConfigDisplay( + context.getString(R.string.combine_continue_next), + HomeRowConfig.ContinueWatchingCombined( + includedIds.size + 1, + HomeRowViewOptions(), + ), + ), + ) + } else { + listOf( + HomeRowConfigDisplay( + context.getString(R.string.continue_watching), + HomeRowConfig.ContinueWatching( + includedIds.size + 1, + HomeRowViewOptions(), + ), + ), + HomeRowConfigDisplay( + context.getString(R.string.next_up), + HomeRowConfig.NextUp( + includedIds.size + 2, + HomeRowViewOptions(), + ), + ), + ) + } + val rowConfig = + continueWatchingRows + includedIds + + // TODO remove after testing + listOf( + HomeRowConfigDisplay( + "Collection", + HomeRowConfig.ByParent( + id = 100, + parentId = "34ab6fd1f51c41bb014981f2e334f465".toUUID(), + recursive = true, + viewOptions = HomeRowViewOptions(), + ), + ), + HomeRowConfigDisplay( + "Playlist", + HomeRowConfig.ByParent( + id = 101, + parentId = "f94be36e9836127a0bccfc7843b19e5b".toUUID(), + recursive = true, + viewOptions = HomeRowViewOptions(), + ), + ), + ) + return HomePageResolvedSettings(rowConfig) + } + + suspend fun convert(config: HomeRowConfig): HomeRowConfigDisplay = + when (config) { + is HomeRowConfig.ByParent -> { + val name = + api.userLibraryApi + .getItem(itemId = config.parentId) + .content.name ?: "" + HomeRowConfigDisplay( + name, + config, + ) + } + + is HomeRowConfig.ContinueWatching -> { + HomeRowConfigDisplay( + context.getString(R.string.continue_watching), + config, + ) + } + + is HomeRowConfig.ContinueWatchingCombined -> { + HomeRowConfigDisplay( + context.getString(R.string.combine_continue_next), + config, + ) + } + + is HomeRowConfig.Genres -> { + val name = + api.userLibraryApi + .getItem(itemId = config.parentId) + .content.name ?: "" + HomeRowConfigDisplay( + context.getString(R.string.genres_in, name), + config, + ) + } + + is HomeRowConfig.GetItems -> { + HomeRowConfigDisplay(config.name, config) + } + + is HomeRowConfig.NextUp -> { + HomeRowConfigDisplay( + context.getString(R.string.next_up), + config, + ) + } + + is HomeRowConfig.RecentlyAdded -> { + val name = + api.userLibraryApi + .getItem(itemId = config.parentId) + .content.name ?: "" + HomeRowConfigDisplay( + context.getString(R.string.recently_added_in, name), + config, + ) + } + + is HomeRowConfig.RecentlyReleased -> { + val name = + api.userLibraryApi + .getItem(itemId = config.parentId) + .content.name ?: "" + HomeRowConfigDisplay( + context.getString(R.string.recently_released_in, name), + config, + ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index e267a12f..f6d25786 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.api.seerr.model.PublicSettings import com.github.damontecres.wholphin.api.seerr.model.User import com.github.damontecres.wholphin.data.SeerrServerDao import com.github.damontecres.wholphin.data.ServerRepository +import com.github.damontecres.wholphin.data.model.HomePageResolvedSettings import com.github.damontecres.wholphin.data.model.SeerrAuthMethod import com.github.damontecres.wholphin.data.model.SeerrPermission import com.github.damontecres.wholphin.data.model.SeerrServer @@ -213,6 +214,7 @@ class UserSwitchListener private val seerrServerRepository: SeerrServerRepository, private val seerrServerDao: SeerrServerDao, private val seerrApi: SeerrApi, + private val homeSettingsService: HomeSettingsService, ) { init { context as AppCompatActivity @@ -220,41 +222,58 @@ class UserSwitchListener serverRepository.currentUser.asFlow().collect { user -> Timber.d("New user") seerrServerRepository.clear() + homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY } if (user != null) { - seerrServerDao - .getUsersByJellyfinUser(user.rowId) - .firstOrNull() - ?.let { seerrUser -> - val server = seerrServerDao.getServer(seerrUser.serverId)?.server - if (server != null) { - Timber.i("Found a seerr user & server") - seerrApi.update(server.url, seerrUser.credential) - val userConfig = - if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { - try { - login( - seerrApi.api, - seerrUser.authMethod, - seerrUser.username, - seerrUser.password, - ) - } catch (ex: Exception) { - Timber.w(ex, "Error logging into %s", server.url) - seerrServerRepository.clear() - return@let + // Check for home settings + launchIO { + homeSettingsService.updateCurrent(user.id) + } + // Check for seerr server + launchIO { + seerrServerDao + .getUsersByJellyfinUser(user.rowId) + .firstOrNull() + ?.let { seerrUser -> + val server = + seerrServerDao.getServer(seerrUser.serverId)?.server + if (server != null) { + Timber.i("Found a seerr user & server") + seerrApi.update(server.url, seerrUser.credential) + val userConfig = + if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { + try { + login( + seerrApi.api, + seerrUser.authMethod, + seerrUser.username, + seerrUser.password, + ) + } catch (ex: Exception) { + Timber.w( + ex, + "Error logging into %s", + server.url, + ) + seerrServerRepository.clear() + return@let + } + } else { + try { + seerrApi.api.usersApi.authMeGet() + } catch (ex: Exception) { + Timber.w( + ex, + "Error logging into %s", + server.url, + ) + seerrServerRepository.clear() + return@let + } } - } else { - try { - seerrApi.api.usersApi.authMeGet() - } catch (ex: Exception) { - Timber.w(ex, "Error logging into %s", server.url) - seerrServerRepository.clear() - return@let - } - } - seerrServerRepository.set(server, seerrUser, userConfig) + seerrServerRepository.set(server, seerrUser, userConfig) + } } - } + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt index 623a18c1..8f148886 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomeSettingsViewModel.kt @@ -9,6 +9,7 @@ import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.NavDrawerItemRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.HomePageResolvedSettings import com.github.damontecres.wholphin.data.model.HomePageSettings import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay @@ -33,6 +34,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.userLibraryApi @@ -43,7 +45,6 @@ import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.request.GetGenresRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest -import org.jellyfin.sdk.model.serializer.toUUID import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -77,178 +78,19 @@ class HomeSettingsViewModel it as ServerNavDrawerItem Library(it.itemId, it.name, it.type) } - _state.update { it.copy(libraries = libraries) } - - val localSettings = - try { - homeSettingsService.loadFromLocal() - } catch (ex: Exception) { - Timber.e(ex) - showToast(context, "Error loading settings: ${ex.localizedMessage}") - null - } - if (localSettings != null) { - val displays = localSettings.rows.map { convert(it) } - _state.update { - it.copy(rows = displays) - } - } else { - - // Or create default - val prefs = - userPreferencesService.getCurrent().appPreferences.homePagePreferences - val includedIds = - navDrawerItemRepository - .getFilteredNavDrawerItems(navDrawerItems) - .filter { it is ServerNavDrawerItem } - .mapIndexed { index, it -> - val id = (it as ServerNavDrawerItem).itemId - val name = libraries.firstOrNull { it.itemId == id }?.name - val title = - name?.let { context.getString(R.string.recently_added_in, it) } - ?: context.getString(R.string.recently_added) - HomeRowConfigDisplay( - title, - HomeRowConfig.RecentlyAdded( - index, - id, - HomeRowViewOptions(), - ), - ) - } - val continueWatchingRows = - if (prefs.combineContinueNext) { - listOf( - HomeRowConfigDisplay( - context.getString(R.string.combine_continue_next), - HomeRowConfig.ContinueWatchingCombined( - includedIds.size + 1, - HomeRowViewOptions(), - ), - ), - ) - } else { - listOf( - HomeRowConfigDisplay( - context.getString(R.string.continue_watching), - HomeRowConfig.ContinueWatching( - includedIds.size + 1, - HomeRowViewOptions(), - ), - ), - HomeRowConfigDisplay( - context.getString(R.string.next_up), - HomeRowConfig.NextUp( - includedIds.size + 2, - HomeRowViewOptions(), - ), - ), - ) - } - val rowConfig = - continueWatchingRows + includedIds + - // TODO remove after testing - listOf( - HomeRowConfigDisplay( - "Collection", - HomeRowConfig.ByParent( - id = 100, - parentId = "34ab6fd1f51c41bb014981f2e334f465".toUUID(), - recursive = true, - viewOptions = HomeRowViewOptions(), - ), - ), - HomeRowConfigDisplay( - "Playlist", - HomeRowConfig.ByParent( - id = 101, - parentId = "f94be36e9836127a0bccfc7843b19e5b".toUUID(), - recursive = true, - viewOptions = HomeRowViewOptions(), - ), - ), - ) - _state.update { - it.copy(rows = rowConfig) - } + val currentSettings = + homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } + Timber.v("currentSettings=%s", currentSettings) + _state.update { + it.copy( + libraries = libraries, + rows = currentSettings.rows, + ) } - fetchRowData() } } - private suspend fun convert(config: HomeRowConfig): HomeRowConfigDisplay = - when (config) { - is HomeRowConfig.ByParent -> { - val name = - api.userLibraryApi - .getItem(itemId = config.parentId) - .content.name ?: "" - HomeRowConfigDisplay( - name, - config, - ) - } - - is HomeRowConfig.ContinueWatching -> { - HomeRowConfigDisplay( - context.getString(R.string.continue_watching), - config, - ) - } - - is HomeRowConfig.ContinueWatchingCombined -> { - HomeRowConfigDisplay( - context.getString(R.string.combine_continue_next), - config, - ) - } - - is HomeRowConfig.Genres -> { - val name = - api.userLibraryApi - .getItem(itemId = config.parentId) - .content.name ?: "" - HomeRowConfigDisplay( - context.getString(R.string.genres_in, name), - config, - ) - } - - is HomeRowConfig.GetItems -> { - HomeRowConfigDisplay(config.name, config) - } - - is HomeRowConfig.NextUp -> { - HomeRowConfigDisplay( - context.getString(R.string.next_up), - config, - ) - } - - is HomeRowConfig.RecentlyAdded -> { - val name = - api.userLibraryApi - .getItem(itemId = config.parentId) - .content.name ?: "" - HomeRowConfigDisplay( - context.getString(R.string.recently_added_in, name), - config, - ) - } - - is HomeRowConfig.RecentlyReleased -> { - val name = - api.userLibraryApi - .getItem(itemId = config.parentId) - .content.name ?: "" - HomeRowConfigDisplay( - context.getString(R.string.recently_released_in, name), - config, - ) - } - } - fun updateBackdrop(item: BaseItem) { viewModelScope.launchIO { backdropService.submit(item) @@ -709,14 +551,16 @@ class HomeSettingsViewModel fun saveToLocal() { viewModelScope.launchIO { - val rows = state.value.rows.map { it.config } - val settings = HomePageSettings(rows = rows) - try { - homeSettingsService.saveToLocal(settings) - showToast(context, context.getString(R.string.save), Toast.LENGTH_SHORT) - } catch (ex: Exception) { - Timber.e(ex) - showToast(context, "Error saving: ${ex.localizedMessage}") + serverRepository.currentUser.value?.let { user -> + val rows = state.value.rows.map { it.config } + val settings = HomePageSettings(rows = rows) + try { + homeSettingsService.saveToLocal(user.id, settings) + showToast(context, context.getString(R.string.save), Toast.LENGTH_SHORT) + } catch (ex: Exception) { + Timber.e(ex) + showToast(context, "Error saving: ${ex.localizedMessage}") + } } } }