Refactoring to expose current configuration

This commit is contained in:
Damontecres 2026-01-24 18:27:58 -05:00
parent ee8a4ac0ac
commit a174d981a8
No known key found for this signature in database
5 changed files with 312 additions and 269 deletions

View file

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

View file

@ -144,9 +144,21 @@ data class HomeRowConfigDisplay(
@Serializable
@SerialName("HomePageSettings")
data class HomePageSettings(
val version: Int = 1,
val rows: List<HomeRowConfig>,
)
val version: Int = 1,
) {
companion object {
val EMPTY = HomePageSettings(listOf())
}
}
data class HomePageResolvedSettings(
val rows: List<HomeRowConfigDisplay>,
) {
companion object {
val EMPTY = HomePageResolvedSettings(listOf())
}
}
@Serializable
data class HomeRowViewOptions(

View file

@ -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,29 +43,32 @@ 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 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 = user.id,
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 {
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<HomePageSettings>(it)
}
}
@ -71,22 +86,22 @@ 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 ->
suspend fun saveToLocal(
userId: UUID,
settings: HomePageSettings,
) {
val dir = File(context.filesDir, CUSTOM_PREF_ID)
dir.mkdirs()
File(dir, filename(user.id)).outputStream().use {
File(dir, filename(userId)).outputStream().use {
jsonParser.encodeToStream(settings, it)
}
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun loadFromLocal(): HomePageSettings? =
serverRepository.currentUser.value?.let { user ->
suspend fun loadFromLocal(userId: UUID): HomePageSettings? {
val dir = File(context.filesDir, CUSTOM_PREF_ID)
val file = File(dir, filename(user.id))
if (file.exists()) {
val file = File(dir, filename(userId))
return if (file.exists()) {
file.inputStream().use {
jsonParser.decodeFromStream<HomePageSettings>(it)
}
@ -95,6 +110,181 @@ class HomeSettingsService
}
}
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 {
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,
)
}
}
companion object {
const val DISPLAY_PREF_ID = "default"
const val CUSTOM_PREF_ID = "home_settings"

View file

@ -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,12 +222,20 @@ class UserSwitchListener
serverRepository.currentUser.asFlow().collect { user ->
Timber.d("New user")
seerrServerRepository.clear()
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
if (user != null) {
// 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
val server =
seerrServerDao.getServer(seerrUser.serverId)?.server
if (server != null) {
Timber.i("Found a seerr user & server")
seerrApi.update(server.url, seerrUser.credential)
@ -239,7 +249,11 @@ class UserSwitchListener
seerrUser.password,
)
} catch (ex: Exception) {
Timber.w(ex, "Error logging into %s", server.url)
Timber.w(
ex,
"Error logging into %s",
server.url,
)
seerrServerRepository.clear()
return@let
}
@ -247,7 +261,11 @@ class UserSwitchListener
try {
seerrApi.api.usersApi.authMeGet()
} catch (ex: Exception) {
Timber.w(ex, "Error logging into %s", server.url)
Timber.w(
ex,
"Error logging into %s",
server.url,
)
seerrServerRepository.clear()
return@let
}
@ -260,3 +278,4 @@ class UserSwitchListener
}
}
}
}

View file

@ -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) }
val currentSettings =
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
Timber.v("currentSettings=%s", currentSettings)
_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(),
),
it.copy(
libraries = libraries,
rows = currentSettings.rows,
)
}
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)
}
}
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,10 +551,11 @@ class HomeSettingsViewModel
fun saveToLocal() {
viewModelScope.launchIO {
serverRepository.currentUser.value?.let { user ->
val rows = state.value.rows.map { it.config }
val settings = HomePageSettings(rows = rows)
try {
homeSettingsService.saveToLocal(settings)
homeSettingsService.saveToLocal(user.id, settings)
showToast(context, context.getString(R.string.save), Toast.LENGTH_SHORT)
} catch (ex: Exception) {
Timber.e(ex)
@ -721,6 +564,7 @@ class HomeSettingsViewModel
}
}
}
}
data class HomePageSettingsState(
val loading: LoadingState,