Option to load from server plugin

This commit is contained in:
Damontecres 2026-05-13 09:44:15 -04:00
parent f02f66c506
commit a27bb2a962
No known key found for this signature in database
8 changed files with 104 additions and 5 deletions

View file

@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.util.EqualityMutableLiveData
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jellyfin.sdk.Jellyfin import org.jellyfin.sdk.Jellyfin
@ -54,6 +55,8 @@ class ServerRepository
val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server } val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }
val currentUser: LiveData<JellyfinUser?> get() = _current.map { it?.user } val currentUser: LiveData<JellyfinUser?> get() = _current.map { it?.user }
val serverPluginInstalled = MutableStateFlow<Boolean>(false)
/** /**
* Adds a server to the app database and updated the [ApiClient] to the server's URL * Adds a server to the app database and updated the [ApiClient] to the server's URL
* *

View file

@ -199,7 +199,7 @@ class HomeSettingsService
try { try {
block.invoke() block.invoke()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.w(ex, "Error loading local settings") Timber.w(ex, "Error loading settings")
null null
} }
@ -213,6 +213,7 @@ class HomeSettingsService
suspend fun loadCurrentSettings(userId: UUID) { suspend fun loadCurrentSettings(userId: UUID) {
Timber.v("Getting setting for %s", userId) Timber.v("Getting setting for %s", userId)
// User local then server/remote otherwise create a default // User local then server/remote otherwise create a default
// TODO figure out priority order
val settings = val settings =
tryLoad { tryLoad {
serverPluginApi.fetchHomePageSettings() serverPluginApi.fetchHomePageSettings()

View file

@ -272,6 +272,7 @@ class UserSwitchListener
private val seerrServerDao: SeerrServerDao, private val seerrServerDao: SeerrServerDao,
private val seerrApi: SeerrApi, private val seerrApi: SeerrApi,
private val homeSettingsService: HomeSettingsService, private val homeSettingsService: HomeSettingsService,
private val serverPluginApi: ServerPluginApi,
) { ) {
init { init {
context as AppCompatActivity context as AppCompatActivity
@ -289,8 +290,19 @@ class UserSwitchListener
private suspend fun switchUser(user: JellyfinUser) = private suspend fun switchUser(user: JellyfinUser) =
supervisorScope { supervisorScope {
// Check for home settings // Check if plugin is installed, then for home settings
launchIO { launchIO {
val serverPluginInstalled =
try {
serverPluginApi.public()
} catch (ex: Exception) {
Timber.e(ex, "Error checking for server plugin")
false
}
Timber.i("Server plugin installed: %s", serverPluginInstalled)
serverRepository.serverPluginInstalled.value = serverPluginInstalled
// Check for home settings
homeSettingsService.loadCurrentSettings(user.id) homeSettingsService.loadCurrentSettings(user.id)
} }
if (BuildConfig.DISCOVER_ENABLED) { if (BuildConfig.DISCOVER_ENABLED) {

View file

@ -9,6 +9,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException import org.jellyfin.sdk.api.client.exception.ApiClientException
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -22,8 +23,24 @@ class ServerPluginApi
private fun createUrl(path: String): String? = private fun createUrl(path: String): String? =
api.baseUrl?.let { if (it.endsWith("/")) "${it}wholphin/$path" else "$it/wholphin/$path" } api.baseUrl?.let { if (it.endsWith("/")) "${it}wholphin/$path" else "$it/wholphin/$path" }
private val json =
Json {
ignoreUnknownKeys = false
}
companion object { companion object {
private const val HOME_CONFIG_PATH = "home" private const val HOME_CONFIG_PATH = "homesettings"
}
suspend fun public(): Boolean {
val url = createUrl("public") ?: return false
val request =
Request
.Builder()
.url(url)
.get()
.build()
return okHttpClient.newCall(request).execute().isSuccessful
} }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@ -37,11 +54,12 @@ class ServerPluginApi
.build() .build()
return okHttpClient.newCall(request).execute().use { res -> return okHttpClient.newCall(request).execute().use { res ->
if (res.isSuccessful) { if (res.isSuccessful) {
Json.decodeFromStream<HomePageSettings>(res.body.byteStream()) json.decodeFromStream<HomePageSettings>(res.body.byteStream())
} else if (res.code == 404) { } else if (res.code == 404) {
Timber.w("fetchHomePageSettings returned 404")
null null
} else { } else {
throw ApiClientException(res.body.string()) throw ApiClientException(res.code.toString() + " " + res.body.string())
} }
} }
} }

View file

@ -37,6 +37,8 @@ fun HomeSettingsGlobal(
onClickSave: () -> Unit, onClickSave: () -> Unit,
onClickLoad: () -> Unit, onClickLoad: () -> Unit,
onClickLoadWeb: () -> Unit, onClickLoadWeb: () -> Unit,
serverPluginActive: Boolean,
onClickLoadPlugin: () -> Unit,
onClickReset: () -> Unit, onClickReset: () -> Unit,
onClickViewNextUp: () -> Unit, onClickViewNextUp: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -166,6 +168,22 @@ fun HomeSettingsGlobal(
modifier = Modifier, modifier = Modifier,
) )
} }
if (serverPluginActive) {
item {
HomeSettingsListItem(
selected = false,
headlineText = stringResource(R.string.load_from_server_plugin),
leadingContent = {
Text(
text = stringResource(R.string.fa_download),
fontFamily = FontAwesome,
)
},
onClick = onClickLoadPlugin,
modifier = Modifier,
)
}
}
item { item {
HomeSettingsListItem( HomeSettingsListItem(
selected = false, selected = false,

View file

@ -65,6 +65,7 @@ fun HomeSettingsPage(
var showRemovedNextUpDialog by remember { mutableStateOf(false) } var showRemovedNextUpDialog by remember { mutableStateOf(false) }
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val serverPluginActive by viewModel.serverPluginActive.collectAsState()
var position by rememberPosition(0, 0) var position by rememberPosition(0, 0)
// TODO discover rows // TODO discover rows
val discoverEnabled = false // by viewModel.discoverEnabled.collectAsState(false) val discoverEnabled = false // by viewModel.discoverEnabled.collectAsState(false)
@ -274,6 +275,13 @@ fun HomeSettingsPage(
viewModel.loadFromRemote() viewModel.loadFromRemote()
} }
}, },
serverPluginActive = serverPluginActive,
onClickLoadPlugin = {
showConfirmDialog =
ShowConfirm(R.string.overwrite_local_settings) {
viewModel.loadFromRemotePlugin()
}
},
onClickLoadWeb = { onClickLoadWeb = {
showConfirmDialog = showConfirmDialog =
ShowConfirm(R.string.overwrite_local_settings) { ShowConfirm(R.string.overwrite_local_settings) {

View file

@ -29,6 +29,7 @@ import com.github.damontecres.wholphin.services.HomeRowConfigDisplay
import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.HomeSettingsService
import com.github.damontecres.wholphin.services.NavDrawerService import com.github.damontecres.wholphin.services.NavDrawerService
import com.github.damontecres.wholphin.services.SeerrServerRepository import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.ServerPluginApi
import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
@ -74,12 +75,15 @@ class HomeSettingsViewModel
private val navDrawerService: NavDrawerService, private val navDrawerService: NavDrawerService,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val seerrServerRepository: SeerrServerRepository, private val seerrServerRepository: SeerrServerRepository,
private val serverPluginApi: ServerPluginApi,
val preferencesDataStore: DataStore<AppPreferences>, val preferencesDataStore: DataStore<AppPreferences>,
@param:IoCoroutineScope private val ioScope: CoroutineScope, @param:IoCoroutineScope private val ioScope: CoroutineScope,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(HomePageSettingsState.EMPTY) private val _state = MutableStateFlow(HomePageSettingsState.EMPTY)
val state: StateFlow<HomePageSettingsState> = _state val state: StateFlow<HomePageSettingsState> = _state
val serverPluginActive get() = serverRepository.serverPluginInstalled
private var idCounter by Delegates.notNull<Int>() private var idCounter by Delegates.notNull<Int>()
val discoverEnabled = seerrServerRepository.active val discoverEnabled = seerrServerRepository.active
@ -767,6 +771,40 @@ class HomeSettingsViewModel
} }
} }
} }
fun loadFromRemotePlugin() {
viewModelScope.launchIO {
Timber.d("Loading home settings from server plugin")
try {
// TODO should this remove local settings?
// TODO how to set up priorities
_state.update { it.copy(loading = LoadingState.Loading) }
val result = serverPluginApi.fetchHomePageSettings()
if (result != null) {
Timber.v("Got remote settings")
val newRows =
result.rows.mapIndexed { index, config ->
homeSettingsService.resolve(index, config)
}
idCounter = newRows.maxOfOrNull { it.id }?.plus(1) ?: 0
_state.update {
it.copy(rows = newRows)
}
} else {
Timber.v("No server plugin settings")
showToast(context, "No server plugin settings found")
}
fetchRowData()
} catch (ex: UnsupportedHomeSettingsVersionException) {
// TODO
Timber.w(ex)
showToast(context, "Error: ${ex.localizedMessage}")
} catch (ex: Exception) {
Timber.e(ex)
showToast(context, "Error: ${ex.localizedMessage}")
}
}
}
} }
data class HomePageSettingsState( data class HomePageSettingsState(

View file

@ -774,6 +774,7 @@
<string name="skip_behavior_summary">For intros, credits, etc</string> <string name="skip_behavior_summary">For intros, credits, etc</string>
<string name="play_with">Play with</string> <string name="play_with">Play with</string>
<string name="transcoding">Transcoding</string> <string name="transcoding">Transcoding</string>
<string name="load_from_server_plugin">Load default from server</string>
<array name="ass_subtitle_modes"> <array name="ass_subtitle_modes">
<item>@string/ass_subtitle_mode_libass</item> <item>@string/ass_subtitle_mode_libass</item>
<item>@string/ass_subtitle_mode_exoplayer</item> <item>@string/ass_subtitle_mode_exoplayer</item>