From 52ea0f1c37685a9f5d0ba001830b2ec0e6df817f Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 30 Mar 2024 16:22:50 -0400 Subject: [PATCH] Implement configuration of server settings during runtime --- android/build.gradle.kts | 4 + data/src/commonMain/graphql/Settings.graphql | 184 ++++- .../ca/gosyer/jui/data/DataComponent.kt | 8 +- .../data/settings/SettingsRepositoryImpl.kt | 179 +++++ .../ca/gosyer/jui/data/util/ApolloUtils.kt | 11 + desktop/build.gradle.kts | 1 + .../jui/desktop/logging/LoggingSetup.kt | 1 - .../server/service/ServerHostPreferences.kt | 29 + .../jui/domain/SharedDomainComponent.kt | 6 + .../server/service/ServerHostPreferences.kt | 16 + .../domain/settings/interactor/AboutServer.kt | 6 +- .../domain/settings/interactor/CheckUpdate.kt | 6 +- .../domain/settings/interactor/GetSettings.kt | 33 + .../domain/settings/interactor/SetSettings.kt | 34 + .../domain/settings/model/SetSettingsInput.kt | 52 ++ .../jui/domain/settings/model/Settings.kt | 55 ++ .../jui/domain/settings/model/WebUIChannel.kt | 14 + .../jui/domain/settings/model/WebUIFlavor.kt | 14 + .../domain/settings/model/WebUIInterface.kt | 13 + .../settings/service/SettingsRepository.kt | 13 +- .../settings/service/SettingsRepositoryOld.kt | 21 + .../ca/gosyer/jui/domain/DomainComponent.kt | 5 - .../server/service/ServerHostPreferences.kt | 55 +- .../service/host/ServerHostPreference.kt | 80 +- .../server/service/ServerHostPreferences.kt | 29 + gradle/libs.versions.toml | 1 + .../resources/MR/values/base/strings.xml | 57 +- ios/build.gradle.kts | 1 + presentation/build.gradle.kts | 1 + .../jui/ui/base/prefs/PreferencesUiBuilder.kt | 10 +- .../jui/ui/settings/SettingsLibraryScreen.kt | 2 +- .../jui/ui/settings/SettingsServerScreen.kt | 741 +++++++++++++++++- .../components/SourceSettingsScreenContent.kt | 1 - .../settings/DesktopSettingsServerScreen.kt | 150 ++-- 34 files changed, 1544 insertions(+), 289 deletions(-) create mode 100644 data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt create mode 100644 data/src/commonMain/kotlin/ca/gosyer/jui/data/util/ApolloUtils.kt create mode 100644 domain/src/androidMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/SetSettingsInput.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/Settings.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIChannel.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIFlavor.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIInterface.kt create mode 100644 domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepositoryOld.kt create mode 100644 domain/src/iosMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index cc155e30..07c0b5a0 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(libs.imageloader.core) implementation(libs.imageloader.moko) implementation(libs.materialDialogs.core) + implementation(libs.materialDialogs.datetime) // Android implementation(libs.androidx.core) @@ -111,6 +112,9 @@ android { buildTypes { getByName("debug") { applicationIdSuffix = ".debug" + isMinifyEnabled = true + isShrinkResources = true + setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) } getByName("release") { isMinifyEnabled = true diff --git a/data/src/commonMain/graphql/Settings.graphql b/data/src/commonMain/graphql/Settings.graphql index 4f576456..bdcd32af 100644 --- a/data/src/commonMain/graphql/Settings.graphql +++ b/data/src/commonMain/graphql/Settings.graphql @@ -1,46 +1,146 @@ +fragment SettingsTypeFragment on SettingsType { + autoDownloadNewChapters + autoDownloadNewChaptersLimit + backupInterval + backupPath + backupTTL + backupTime + basicAuthEnabled + basicAuthPassword + basicAuthUsername + debugLogsEnabled + downloadAsCbz + downloadsPath + electronPath + excludeCompleted + excludeEntryWithUnreadChapters + excludeNotStarted + excludeUnreadChapters + extensionRepos + flareSolverrEnabled + flareSolverrSessionName + flareSolverrSessionTtl + flareSolverrTimeout + flareSolverrUrl + globalUpdateInterval + gqlDebugLogsEnabled + initialOpenInBrowserEnabled + ip + localSourcePath + maxSourcesInParallel + port + socksProxyEnabled + socksProxyHost + socksProxyPassword + socksProxyPort + socksProxyUsername + socksProxyVersion + systemTrayEnabled + updateMangas + webUIChannel + webUIFlavor + webUIInterface + webUIUpdateCheckInterval +} + query AllSettings { settings { - autoDownloadNewChapters - autoDownloadNewChaptersLimit - backupInterval - backupPath - backupTTL - backupTime - basicAuthEnabled - basicAuthPassword - basicAuthUsername - debugLogsEnabled - downloadAsCbz - downloadsPath - electronPath - excludeCompleted - excludeEntryWithUnreadChapters - excludeNotStarted - excludeUnreadChapters - extensionRepos - flareSolverrEnabled - flareSolverrSessionName - flareSolverrSessionTtl - flareSolverrTimeout - flareSolverrUrl - globalUpdateInterval - gqlDebugLogsEnabled - initialOpenInBrowserEnabled - ip - localSourcePath - maxSourcesInParallel - port - socksProxyEnabled - socksProxyHost - socksProxyPassword - socksProxyPort - socksProxyUsername - socksProxyVersion - systemTrayEnabled - updateMangas - webUIChannel - webUIFlavor - webUIInterface - webUIUpdateCheckInterval + ...SettingsTypeFragment + } +} + +mutation SetSettings( + $autoDownloadNewChapters: Boolean = null, + $autoDownloadNewChaptersLimit: Int = null, + $backupInterval: Int = null, + $backupPath: String = null, + $backupTTL: Int = null, + $backupTime: String = null, + $basicAuthEnabled: Boolean = null, + $basicAuthPassword: String = null, + $basicAuthUsername: String = null, + $debugLogsEnabled: Boolean = null, + $downloadAsCbz: Boolean = null, + $downloadsPath: String = null, + $electronPath: String = null, + $excludeCompleted: Boolean = null, + $excludeEntryWithUnreadChapters: Boolean = null, + $excludeNotStarted: Boolean = null, + $excludeUnreadChapters: Boolean = null, + $extensionRepos: [String!] = null, + $flareSolverrEnabled: Boolean = null, + $flareSolverrSessionName: String = null, + $flareSolverrSessionTtl: Int = null, + $flareSolverrTimeout: Int = null, + $flareSolverrUrl: String = null, + $globalUpdateInterval: Float = null, + $gqlDebugLogsEnabled: Boolean = null, + $initialOpenInBrowserEnabled: Boolean = null, + $ip: String = null, + $localSourcePath: String = null, + $maxSourcesInParallel: Int = null, + $port: Int = null, + $socksProxyEnabled: Boolean = null, + $socksProxyHost: String = null, + $socksProxyPassword: String = null, + $socksProxyPort: String = null, + $socksProxyUsername: String = null, + $socksProxyVersion: Int = null, + $systemTrayEnabled: Boolean = null, + $updateMangas: Boolean = null, + $webUIChannel: WebUIChannel = null, + $webUIFlavor: WebUIFlavor = null, + $webUIInterface: WebUIInterface = null, + $webUIUpdateCheckInterval: Float = null +) { + setSettings( + input: { + settings: { + autoDownloadNewChapters: $autoDownloadNewChapters, + autoDownloadNewChaptersLimit: $autoDownloadNewChaptersLimit, + backupInterval: $backupInterval, + backupPath: $backupPath, + backupTTL: $backupTTL, + backupTime: $backupTime, + basicAuthEnabled: $basicAuthEnabled, + basicAuthPassword: $basicAuthPassword, + basicAuthUsername: $basicAuthUsername, + debugLogsEnabled: $debugLogsEnabled, + downloadAsCbz: $downloadAsCbz, + downloadsPath: $downloadsPath, + electronPath: $electronPath, + excludeCompleted: $excludeCompleted, + excludeEntryWithUnreadChapters: $excludeEntryWithUnreadChapters, + excludeNotStarted: $excludeNotStarted, + excludeUnreadChapters: $excludeUnreadChapters, + extensionRepos: $extensionRepos, + flareSolverrEnabled: $flareSolverrEnabled, + flareSolverrSessionName: $flareSolverrSessionName, + flareSolverrSessionTtl: $flareSolverrSessionTtl, + flareSolverrTimeout: $flareSolverrTimeout, + flareSolverrUrl: $flareSolverrUrl, + globalUpdateInterval: $globalUpdateInterval, + gqlDebugLogsEnabled: $gqlDebugLogsEnabled, + initialOpenInBrowserEnabled: $initialOpenInBrowserEnabled, + ip: $ip, + localSourcePath: $localSourcePath, + maxSourcesInParallel: $maxSourcesInParallel, + port: $port, + socksProxyEnabled: $socksProxyEnabled, + socksProxyHost: $socksProxyHost, + socksProxyPassword: $socksProxyPassword, + socksProxyPort: $socksProxyPort, + socksProxyUsername: $socksProxyUsername, + socksProxyVersion: $socksProxyVersion, + systemTrayEnabled: $systemTrayEnabled, + updateMangas: $updateMangas, + webUIChannel: $webUIChannel, + webUIFlavor: $webUIFlavor, + webUIInterface: $webUIInterface, + webUIUpdateCheckInterval: $webUIUpdateCheckInterval + } + } + ) { + clientMutationId } } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt index a6d27930..28d946a2 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt @@ -7,6 +7,7 @@ package ca.gosyer.jui.data import ca.gosyer.jui.core.lang.addSuffix +import ca.gosyer.jui.data.settings.SettingsRepositoryImpl import ca.gosyer.jui.domain.backup.service.BackupRepository import ca.gosyer.jui.domain.category.service.CategoryRepository import ca.gosyer.jui.domain.chapter.service.ChapterRepository @@ -18,6 +19,7 @@ import ca.gosyer.jui.domain.manga.service.MangaRepository import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.domain.server.service.ServerPreferences import ca.gosyer.jui.domain.settings.service.SettingsRepository +import ca.gosyer.jui.domain.settings.service.SettingsRepositoryOld import ca.gosyer.jui.domain.source.service.SourceRepository import ca.gosyer.jui.domain.updates.service.UpdatesRepository import com.apollographql.apollo3.ApolloClient @@ -79,11 +81,15 @@ interface DataComponent { fun mangaRepository(ktorfit: Ktorfit) = ktorfit.create() @Provides - fun settingsRepository(ktorfit: Ktorfit) = ktorfit.create() + fun settingsRepositoryOld(ktorfit: Ktorfit) = ktorfit.create() @Provides fun sourceRepository(ktorfit: Ktorfit) = ktorfit.create() @Provides fun updatesRepository(ktorfit: Ktorfit) = ktorfit.create() + + @Provides + fun settingsRepository(apolloClient: ApolloClient): SettingsRepository = + SettingsRepositoryImpl(apolloClient) } diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt new file mode 100644 index 00000000..af36f545 --- /dev/null +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/settings/SettingsRepositoryImpl.kt @@ -0,0 +1,179 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.data.settings + +import ca.gosyer.jui.data.graphql.AllSettingsQuery +import ca.gosyer.jui.data.graphql.SetSettingsMutation +import ca.gosyer.jui.data.graphql.fragment.SettingsTypeFragment +import ca.gosyer.jui.data.graphql.type.WebUIChannel +import ca.gosyer.jui.data.graphql.type.WebUIFlavor +import ca.gosyer.jui.data.graphql.type.WebUIInterface +import ca.gosyer.jui.data.util.toOptional +import ca.gosyer.jui.domain.settings.model.SetSettingsInput +import ca.gosyer.jui.domain.settings.model.Settings +import ca.gosyer.jui.domain.settings.service.SettingsRepository +import com.apollographql.apollo3.ApolloClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import ca.gosyer.jui.domain.settings.model.WebUIChannel as DomainWebUIChannel +import ca.gosyer.jui.domain.settings.model.WebUIFlavor as DomainWebUIFlavor +import ca.gosyer.jui.domain.settings.model.WebUIInterface as DomainWebUIInterface + +class SettingsRepositoryImpl(private val apolloClient: ApolloClient) : SettingsRepository { + + private fun SettingsTypeFragment.toSettings() = Settings( + autoDownloadNewChapters = autoDownloadNewChapters, + autoDownloadNewChaptersLimit = autoDownloadNewChaptersLimit, + backupInterval = backupInterval, + backupPath = backupPath, + backupTTL = backupTTL, + backupTime = backupTime, + basicAuthEnabled = basicAuthEnabled, + basicAuthPassword = basicAuthPassword, + basicAuthUsername = basicAuthUsername, + debugLogsEnabled = debugLogsEnabled, + downloadAsCbz = downloadAsCbz, + downloadsPath = downloadsPath, + electronPath = electronPath, + excludeCompleted = excludeCompleted, + excludeEntryWithUnreadChapters = excludeEntryWithUnreadChapters, + excludeNotStarted = excludeNotStarted, + excludeUnreadChapters = excludeUnreadChapters, + extensionRepos = extensionRepos, + flareSolverrEnabled = flareSolverrEnabled, + flareSolverrSessionName = flareSolverrSessionName, + flareSolverrSessionTtl = flareSolverrSessionTtl, + flareSolverrTimeout = flareSolverrTimeout, + flareSolverrUrl = flareSolverrUrl, + globalUpdateInterval = globalUpdateInterval, + gqlDebugLogsEnabled = gqlDebugLogsEnabled, + initialOpenInBrowserEnabled = initialOpenInBrowserEnabled, + ip = ip, + localSourcePath = localSourcePath, + maxSourcesInParallel = maxSourcesInParallel, + port = port, + socksProxyEnabled = socksProxyEnabled, + socksProxyHost = socksProxyHost, + socksProxyPassword = socksProxyPassword, + socksProxyPort = socksProxyPort, + socksProxyUsername = socksProxyUsername, + socksProxyVersion = socksProxyVersion, + systemTrayEnabled = systemTrayEnabled, + updateMangas = updateMangas, + webUIChannel = webUIChannel.toDomain(), + webUIFlavor = webUIFlavor.toDomain(), + webUIInterface = webUIInterface.toDomain(), + webUIUpdateCheckInterval = webUIUpdateCheckInterval + ) + + private fun WebUIChannel.toDomain() = when (this) { + WebUIChannel.BUNDLED -> DomainWebUIChannel.BUNDLED + WebUIChannel.STABLE -> DomainWebUIChannel.STABLE + WebUIChannel.PREVIEW -> DomainWebUIChannel.PREVIEW + WebUIChannel.UNKNOWN__ -> DomainWebUIChannel.UNKNOWN__ + } + + private fun WebUIFlavor.toDomain() = when (this) { + WebUIFlavor.WEBUI -> DomainWebUIFlavor.WEBUI + WebUIFlavor.VUI -> DomainWebUIFlavor.VUI + WebUIFlavor.CUSTOM -> DomainWebUIFlavor.CUSTOM + WebUIFlavor.UNKNOWN__ -> DomainWebUIFlavor.UNKNOWN__ + } + + private fun WebUIInterface.toDomain() = when (this) { + WebUIInterface.BROWSER -> DomainWebUIInterface.BROWSER + WebUIInterface.ELECTRON -> DomainWebUIInterface.ELECTRON + WebUIInterface.UNKNOWN__ -> DomainWebUIInterface.UNKNOWN__ + } + + private fun DomainWebUIChannel.toGraphQL() = when (this) { + DomainWebUIChannel.BUNDLED -> WebUIChannel.BUNDLED + DomainWebUIChannel.STABLE -> WebUIChannel.STABLE + DomainWebUIChannel.PREVIEW -> WebUIChannel.PREVIEW + DomainWebUIChannel.UNKNOWN__ -> WebUIChannel.UNKNOWN__ + } + + private fun DomainWebUIFlavor.toGraphQL() = when (this) { + DomainWebUIFlavor.WEBUI -> WebUIFlavor.WEBUI + DomainWebUIFlavor.VUI -> WebUIFlavor.VUI + DomainWebUIFlavor.CUSTOM -> WebUIFlavor.CUSTOM + DomainWebUIFlavor.UNKNOWN__ -> WebUIFlavor.UNKNOWN__ + } + + private fun DomainWebUIInterface.toGraphQL() = when (this) { + DomainWebUIInterface.BROWSER -> WebUIInterface.BROWSER + DomainWebUIInterface.ELECTRON -> WebUIInterface.ELECTRON + DomainWebUIInterface.UNKNOWN__ -> WebUIInterface.UNKNOWN__ + } + + override fun getSettings(): Flow { + return apolloClient.query(AllSettingsQuery()).toFlow() + .map { + it.dataOrThrow().settings.settingsTypeFragment.toSettings() + } + .flowOn(Dispatchers.IO) + } + + + private fun SetSettingsInput.toMutation() = SetSettingsMutation( + autoDownloadNewChapters = autoDownloadNewChapters.toOptional(), + autoDownloadNewChaptersLimit = autoDownloadNewChaptersLimit.toOptional(), + backupInterval = backupInterval.toOptional(), + backupPath = backupPath.toOptional(), + backupTTL = backupTTL.toOptional(), + backupTime = backupTime.toOptional(), + basicAuthEnabled = basicAuthEnabled.toOptional(), + basicAuthPassword = basicAuthPassword.toOptional(), + basicAuthUsername = basicAuthUsername.toOptional(), + debugLogsEnabled = debugLogsEnabled.toOptional(), + downloadAsCbz = downloadAsCbz.toOptional(), + downloadsPath = downloadsPath.toOptional(), + electronPath = electronPath.toOptional(), + excludeCompleted = excludeCompleted.toOptional(), + excludeEntryWithUnreadChapters = excludeEntryWithUnreadChapters.toOptional(), + excludeNotStarted = excludeNotStarted.toOptional(), + excludeUnreadChapters = excludeUnreadChapters.toOptional(), + extensionRepos = extensionRepos.toOptional(), + flareSolverrEnabled = flareSolverrEnabled.toOptional(), + flareSolverrSessionName = flareSolverrSessionName.toOptional(), + flareSolverrSessionTtl = flareSolverrSessionTtl.toOptional(), + flareSolverrTimeout = flareSolverrTimeout.toOptional(), + flareSolverrUrl = flareSolverrUrl.toOptional(), + globalUpdateInterval = globalUpdateInterval.toOptional(), + gqlDebugLogsEnabled = gqlDebugLogsEnabled.toOptional(), + initialOpenInBrowserEnabled = initialOpenInBrowserEnabled.toOptional(), + ip = ip.toOptional(), + localSourcePath = localSourcePath.toOptional(), + maxSourcesInParallel = maxSourcesInParallel.toOptional(), + port = port.toOptional(), + socksProxyEnabled = socksProxyEnabled.toOptional(), + socksProxyHost = socksProxyHost.toOptional(), + socksProxyPassword = socksProxyPassword.toOptional(), + socksProxyPort = socksProxyPort.toOptional(), + socksProxyUsername = socksProxyUsername.toOptional(), + socksProxyVersion = socksProxyVersion.toOptional(), + systemTrayEnabled = systemTrayEnabled.toOptional(), + updateMangas = updateMangas.toOptional(), + webUIChannel = webUIChannel?.toGraphQL().toOptional(), + webUIFlavor = webUIFlavor?.toGraphQL().toOptional(), + webUIInterface = webUIInterface?.toGraphQL().toOptional(), + webUIUpdateCheckInterval = webUIUpdateCheckInterval.toOptional(), + ) + + override fun setSettings(input: SetSettingsInput): Flow { + return apolloClient.mutation(input.toMutation()) + .toFlow() + .map { + it.dataOrThrow() + Unit + } + .flowOn(Dispatchers.IO) + } +} diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/util/ApolloUtils.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/util/ApolloUtils.kt new file mode 100644 index 00000000..993e34f8 --- /dev/null +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/util/ApolloUtils.kt @@ -0,0 +1,11 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.data.util + +import com.apollographql.apollo3.api.Optional + +internal fun T?.toOptional() = Optional.presentIfNotNull(this) diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 4557ff2f..659b714a 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(libs.imageloader.core) implementation(libs.imageloader.moko) implementation(libs.materialDialogs.core) + implementation(libs.materialDialogs.datetime) // UI (Swing) implementation(libs.darklaf) diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/LoggingSetup.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/LoggingSetup.kt index 9bcd0edf..e930cad1 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/LoggingSetup.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/LoggingSetup.kt @@ -36,7 +36,6 @@ const val filePattern = '$' + "{LOG_EXCEPTION_CONVERSION_WORD:-%xEx}" -@Suppress("UPPER_BOUND_VIOLATED_WARNING") fun initializeLogger(loggingLocation: Path) { val ctx = LogManager.getContext(false) as LoggerContext val builder = ConfigurationBuilderFactory.newConfigurationBuilder() diff --git a/domain/src/androidMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt b/domain/src/androidMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt new file mode 100644 index 00000000..775a057e --- /dev/null +++ b/domain/src/androidMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.server.service + +import ca.gosyer.jui.core.prefs.Preference +import ca.gosyer.jui.core.prefs.PreferenceStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +actual class ServerHostPreferences actual constructor( + @Suppress("unused") private val preferenceStore: PreferenceStore, +) { + actual fun host(): Preference = object : Preference { + override fun key(): String = "host" + override fun get(): Boolean = false + override fun isSet(): Boolean = false + override fun delete() {} + override fun defaultValue(): Boolean = false + override fun changes(): Flow = MutableStateFlow(false) + override fun stateIn(scope: CoroutineScope): StateFlow = MutableStateFlow(false) + override fun set(value: Boolean) {} + } +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt index 18277fb6..311f0402 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt @@ -17,6 +17,7 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences import ca.gosyer.jui.domain.reader.service.ReaderPreferences import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.domain.server.httpClient +import ca.gosyer.jui.domain.server.service.ServerHostPreferences import ca.gosyer.jui.domain.server.service.ServerPreferences import ca.gosyer.jui.domain.source.service.CatalogPreferences import ca.gosyer.jui.domain.ui.service.UiPreferences @@ -107,6 +108,11 @@ interface SharedDomainComponent : CoreComponent { val updatePreferencesFactory: UpdatePreferences get() = UpdatePreferences(preferenceFactory.create("update")) + @get:AppScope + @get:Provides + val serverHostPreferencesFactory: ServerHostPreferences + get() = ServerHostPreferences(preferenceFactory.create("host")) + @get:AppScope @get:Provides val libraryUpdateServiceFactory: LibraryUpdateService diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt new file mode 100644 index 00000000..3627be33 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt @@ -0,0 +1,16 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.server.service + +import ca.gosyer.jui.core.prefs.Preference +import ca.gosyer.jui.core.prefs.PreferenceStore + +expect class ServerHostPreferences( + preferenceStore: PreferenceStore, +) { + fun host(): Preference +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt index 5e3dc412..1d36cca3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt @@ -6,7 +6,7 @@ package ca.gosyer.jui.domain.settings.interactor -import ca.gosyer.jui.domain.settings.service.SettingsRepository +import ca.gosyer.jui.domain.settings.service.SettingsRepositoryOld import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject @@ -15,7 +15,7 @@ import org.lighthousegames.logging.logging class AboutServer @Inject constructor( - private val settingsRepository: SettingsRepository, + private val settingsRepositoryOld: SettingsRepositoryOld, ) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() @@ -25,7 +25,7 @@ class AboutServer } .singleOrNull() - fun asFlow() = settingsRepository.aboutServer() + fun asFlow() = settingsRepositoryOld.aboutServer() companion object { private val log = logging() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt index 5dad9573..2829ce87 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt @@ -6,7 +6,7 @@ package ca.gosyer.jui.domain.settings.interactor -import ca.gosyer.jui.domain.settings.service.SettingsRepository +import ca.gosyer.jui.domain.settings.service.SettingsRepositoryOld import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject @@ -15,7 +15,7 @@ import org.lighthousegames.logging.logging class CheckUpdate @Inject constructor( - private val settingsRepository: SettingsRepository, + private val settingsRepositoryOld: SettingsRepositoryOld, ) { suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() @@ -25,7 +25,7 @@ class CheckUpdate } .singleOrNull() - fun asFlow() = settingsRepository.checkUpdate() + fun asFlow() = settingsRepositoryOld.checkUpdate() companion object { private val log = logging() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt new file mode 100644 index 00000000..460b2974 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/GetSettings.kt @@ -0,0 +1,33 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.interactor + +import ca.gosyer.jui.domain.settings.service.SettingsRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.singleOrNull +import me.tatarka.inject.annotations.Inject +import org.lighthousegames.logging.logging + +class GetSettings + @Inject + constructor( + private val settingsRepository: SettingsRepository, + ) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to check for server updates" } + } + .singleOrNull() + + fun asFlow() = settingsRepository.getSettings() + + companion object { + private val log = logging() + } + } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt new file mode 100644 index 00000000..551a5f23 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/SetSettings.kt @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.interactor + +import ca.gosyer.jui.domain.settings.model.SetSettingsInput +import ca.gosyer.jui.domain.settings.service.SettingsRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.singleOrNull +import me.tatarka.inject.annotations.Inject +import org.lighthousegames.logging.logging + +class SetSettings + @Inject + constructor( + private val settingsRepository: SettingsRepository, + ) { + suspend fun await(input: SetSettingsInput, onError: suspend (Throwable) -> Unit = {}) = + asFlow(input) + .catch { + onError(it) + log.warn(it) { "Failed to check for server updates" } + } + .singleOrNull() + + fun asFlow(input: SetSettingsInput) = settingsRepository.setSettings(input) + + companion object { + private val log = logging() + } + } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/SetSettingsInput.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/SetSettingsInput.kt new file mode 100644 index 00000000..2a82aa99 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/SetSettingsInput.kt @@ -0,0 +1,52 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.model + +class SetSettingsInput( + val autoDownloadNewChapters: Boolean? = null, + val autoDownloadNewChaptersLimit: Int? = null, + val backupInterval: Int? = null, + val backupPath: String? = null, + val backupTTL: Int? = null, + val backupTime: String? = null, + val basicAuthEnabled: Boolean? = null, + val basicAuthPassword: String? = null, + val basicAuthUsername: String? = null, + val debugLogsEnabled: Boolean? = null, + val downloadAsCbz: Boolean? = null, + val downloadsPath: String? = null, + val electronPath: String? = null, + val excludeCompleted: Boolean? = null, + val excludeEntryWithUnreadChapters: Boolean? = null, + val excludeNotStarted: Boolean? = null, + val excludeUnreadChapters: Boolean? = null, + val extensionRepos: List? = null, + val flareSolverrEnabled: Boolean? = null, + val flareSolverrSessionName: String? = null, + val flareSolverrSessionTtl: Int? = null, + val flareSolverrTimeout: Int? = null, + val flareSolverrUrl: String? = null, + val globalUpdateInterval: Double? = null, + val gqlDebugLogsEnabled: Boolean? = null, + val initialOpenInBrowserEnabled: Boolean? = null, + val ip: String? = null, + val localSourcePath: String? = null, + val maxSourcesInParallel: Int? = null, + val port: Int? = null, + val socksProxyEnabled: Boolean? = null, + val socksProxyHost: String? = null, + val socksProxyPassword: String? = null, + val socksProxyPort: String? = null, + val socksProxyUsername: String? = null, + val socksProxyVersion: Int? = null, + val systemTrayEnabled: Boolean? = null, + val updateMangas: Boolean? = null, + val webUIChannel: WebUIChannel? = null, + val webUIFlavor: WebUIFlavor? = null, + val webUIInterface: WebUIInterface? = null, + val webUIUpdateCheckInterval: Double? = null, +) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/Settings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/Settings.kt new file mode 100644 index 00000000..c862fab9 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/Settings.kt @@ -0,0 +1,55 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.model + +import androidx.compose.runtime.Stable + +@Stable +class Settings( + val autoDownloadNewChapters: Boolean, + val autoDownloadNewChaptersLimit: Int, + val backupInterval: Int, + val backupPath: String, + val backupTTL: Int, + val backupTime: String, + val basicAuthEnabled: Boolean, + val basicAuthPassword: String, + val basicAuthUsername: String, + val debugLogsEnabled: Boolean, + val downloadAsCbz: Boolean, + val downloadsPath: String, + val electronPath: String, + val excludeCompleted: Boolean, + val excludeEntryWithUnreadChapters: Boolean, + val excludeNotStarted: Boolean, + val excludeUnreadChapters: Boolean, + val extensionRepos: List, + val flareSolverrEnabled: Boolean, + val flareSolverrSessionName: String, + val flareSolverrSessionTtl: Int, + val flareSolverrTimeout: Int, + val flareSolverrUrl: String, + val globalUpdateInterval: Double, + val gqlDebugLogsEnabled: Boolean, + val initialOpenInBrowserEnabled: Boolean, + val ip: String, + val localSourcePath: String, + val maxSourcesInParallel: Int, + val port: Int, + val socksProxyEnabled: Boolean, + val socksProxyHost: String, + val socksProxyPassword: String, + val socksProxyPort: String, + val socksProxyUsername: String, + val socksProxyVersion: Int, + val systemTrayEnabled: Boolean, + val updateMangas: Boolean, + val webUIChannel: WebUIChannel, + val webUIFlavor: WebUIFlavor, + val webUIInterface: WebUIInterface, + val webUIUpdateCheckInterval: Double, +) diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIChannel.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIChannel.kt new file mode 100644 index 00000000..ae49c667 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIChannel.kt @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.model + +enum class WebUIChannel { + BUNDLED, + STABLE, + PREVIEW, + UNKNOWN__; +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIFlavor.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIFlavor.kt new file mode 100644 index 00000000..34e0f84e --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIFlavor.kt @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.model + +enum class WebUIFlavor { + WEBUI, + VUI, + CUSTOM, + UNKNOWN__; +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIInterface.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIInterface.kt new file mode 100644 index 00000000..53339fb4 --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/model/WebUIInterface.kt @@ -0,0 +1,13 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.model + +enum class WebUIInterface { + BROWSER, + ELECTRON, + UNKNOWN__; +} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepository.kt index c9a2b17a..86ba9707 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepository.kt @@ -6,16 +6,13 @@ package ca.gosyer.jui.domain.settings.service -import ca.gosyer.jui.domain.settings.model.About -import de.jensklingenberg.ktorfit.http.GET -import de.jensklingenberg.ktorfit.http.POST -import io.ktor.client.statement.HttpResponse +import ca.gosyer.jui.domain.settings.model.SetSettingsInput +import ca.gosyer.jui.domain.settings.model.Settings import kotlinx.coroutines.flow.Flow interface SettingsRepository { - @GET("api/v1/settings/about") - fun aboutServer(): Flow - @POST("api/v1/settings/check-update") - fun checkUpdate(): Flow + fun getSettings(): Flow + + fun setSettings(input: SetSettingsInput): Flow } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepositoryOld.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepositoryOld.kt new file mode 100644 index 00000000..065cd6de --- /dev/null +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/service/SettingsRepositoryOld.kt @@ -0,0 +1,21 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.settings.service + +import ca.gosyer.jui.domain.settings.model.About +import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.POST +import io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.flow.Flow + +interface SettingsRepositoryOld { + @GET("api/v1/settings/about") + fun aboutServer(): Flow + + @POST("api/v1/settings/check-update") + fun checkUpdate(): Flow +} diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt index 2be72ffa..bf6d410f 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt @@ -18,11 +18,6 @@ actual interface DomainComponent : SharedDomainComponent { val serverHostPreferences: ServerHostPreferences - @get:AppScope - @get:Provides - val serverHostPreferencesFactory: ServerHostPreferences - get() = ServerHostPreferences(preferenceFactory.create("host")) - @get:AppScope @get:Provides val serverServiceFactory: ServerService diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt index 77f30ec5..585e8aa5 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt @@ -10,10 +10,10 @@ import ca.gosyer.jui.core.prefs.Preference import ca.gosyer.jui.core.prefs.PreferenceStore import ca.gosyer.jui.domain.server.service.host.ServerHostPreference -class ServerHostPreferences( +actual class ServerHostPreferences actual constructor( private val preferenceStore: PreferenceStore, ) { - fun host(): Preference = preferenceStore.getBoolean("host", true) + actual fun host(): Preference = preferenceStore.getBoolean("host", true) private val ip = ServerHostPreference.IP(preferenceStore) @@ -23,45 +23,20 @@ class ServerHostPreferences( fun port(): Preference = port.preference() - // Proxy - private val socksProxyEnabled = ServerHostPreference.SocksProxyEnabled(preferenceStore) - - fun socksProxyEnabled(): Preference = socksProxyEnabled.preference() - - private val socksProxyHost = ServerHostPreference.SocksProxyHost(preferenceStore) - - fun socksProxyHost(): Preference = socksProxyHost.preference() - - private val socksProxyPort = ServerHostPreference.SocksProxyPort(preferenceStore) - - fun socksProxyPort(): Preference = socksProxyPort.preference() - - // Misc - private val debugLogsEnabled = ServerHostPreference.DebugLogsEnabled(preferenceStore) - - fun debugLogsEnabled(): Preference = debugLogsEnabled.preference() - - private val systemTrayEnabled = ServerHostPreference.SystemTrayEnabled(preferenceStore) - - fun systemTrayEnabled(): Preference = systemTrayEnabled.preference() - // Downloader private val downloadPath = ServerHostPreference.DownloadPath(preferenceStore) fun downloadPath(): Preference = downloadPath.preference() - private val downloadAsCbz = ServerHostPreference.DownloadAsCbz(preferenceStore) + // Backup + private val backupPath = ServerHostPreference.BackupPath(preferenceStore) - fun downloadAsCbz(): Preference = downloadAsCbz.preference() + fun backupPath(): Preference = backupPath.preference() - // WebUI - private val webUIEnabled = ServerHostPreference.WebUIEnabled(preferenceStore) + // LocalSource + private val localSourcePath = ServerHostPreference.LocalSourcePath(preferenceStore) - fun webUIEnabled(): Preference = webUIEnabled.preference() - - private val openInBrowserEnabled = ServerHostPreference.OpenInBrowserEnabled(preferenceStore) - - fun openInBrowserEnabled(): Preference = openInBrowserEnabled.preference() + fun localSourcePath(): Preference = localSourcePath.preference() // Authentication private val basicAuthEnabled = ServerHostPreference.BasicAuthEnabled(preferenceStore) @@ -80,19 +55,15 @@ class ServerHostPreferences( listOf( ip, port, - socksProxyEnabled, - socksProxyHost, - socksProxyPort, - debugLogsEnabled, - systemTrayEnabled, downloadPath, - downloadAsCbz, - webUIEnabled, - openInBrowserEnabled, + backupPath, + localSourcePath, basicAuthEnabled, basicAuthUsername, basicAuthPassword, ).mapNotNull { it.getProperty() - }.toTypedArray() + }.plus( + "-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false" + ).toTypedArray() } diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/host/ServerHostPreference.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/host/ServerHostPreference.kt index a1dc1ff6..0600635f 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/host/ServerHostPreference.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/host/ServerHostPreference.kt @@ -79,49 +79,6 @@ sealed class ServerHostPreference { 4567, ) - // Proxy - class SocksProxyEnabled( - preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "socksProxyEnabled", - false, - ) - - class SocksProxyHost( - preferenceStore: PreferenceStore, - ) : StringServerHostPreference( - preferenceStore, - "socksProxyHost", - "", - ) - - class SocksProxyPort( - override val preferenceStore: PreferenceStore, - ) : IntServerHostPreference( - preferenceStore, - "socksProxyPort", - 0, - ) - - // Misc - class DebugLogsEnabled( - preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "debugLogsEnabled", - false, - ) - - class SystemTrayEnabled( - preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "systemTrayEnabled", - false, - true, - ) - // Downloader class DownloadPath( preferenceStore: PreferenceStore, @@ -131,32 +88,23 @@ sealed class ServerHostPreference { "", ) - class DownloadAsCbz( + // Backup + class BackupPath( preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "downloadAsCbz", - false, - ) + ) : StringServerHostPreference( + preferenceStore, + "backupPath", + "", + ) - // WebUI - class WebUIEnabled( + // LocalSource + class LocalSourcePath( preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "webUIEnabled", - false, - true, - ) - - class OpenInBrowserEnabled( - preferenceStore: PreferenceStore, - ) : BooleanServerHostPreference( - preferenceStore, - "initialOpenInBrowserEnabled", - false, - true, - ) + ) : StringServerHostPreference( + preferenceStore, + "localSourcePath", + "", + ) // Authentication class BasicAuthEnabled( diff --git a/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt b/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt new file mode 100644 index 00000000..775a057e --- /dev/null +++ b/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.jui.domain.server.service + +import ca.gosyer.jui.core.prefs.Preference +import ca.gosyer.jui.core.prefs.PreferenceStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +actual class ServerHostPreferences actual constructor( + @Suppress("unused") private val preferenceStore: PreferenceStore, +) { + actual fun host(): Preference = object : Preference { + override fun key(): String = "host" + override fun get(): Boolean = false + override fun isSet(): Boolean = false + override fun delete() {} + override fun defaultValue(): Boolean = false + override fun changes(): Flow = MutableStateFlow(false) + override fun stateIn(scope: CoroutineScope): StateFlow = MutableStateFlow(false) + override fun set(value: Boolean) {} + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7358102f..108c5222 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -99,6 +99,7 @@ accompanist-systemUIController = { module = "com.google.accompanist:accompanist- imageloader-core = { module = "io.github.qdsfdhvh:image-loader", version.ref = "imageloader" } imageloader-moko = { module = "io.github.qdsfdhvh:image-loader-extension-moko-resources", version.ref = "imageloader" } materialDialogs-core = { module = "ca.gosyer:compose-material-dialogs-core", version.ref = "materialDialogs" } +materialDialogs-datetime = { module = "ca.gosyer:compose-material-dialogs-datetime", version.ref = "materialDialogs" } # Android androidx-core = { module = "androidx.core:core-ktx", version.ref = "core" } diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml index 9a87c26d..6abffdb5 100644 --- a/i18n/src/commonMain/resources/MR/values/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml @@ -220,9 +220,9 @@ Restore Backup - Restore a backup into Tachidesk + Restore a backup to your library Create Backup - Create a backup from Tachidesk + Create a backup from your library Missing sources: @@ -282,17 +282,21 @@ Host server inside Tachidesk-JUI - Tachidesk-Server settings - The below settings configure the internal Tachidesk-Server + Suwayomi-Server settings + The below settings configure the internal Suwayomi-Server + Server IP Current server IP: %1$s Server PORT Current server PORT: %1$s - Server SOCKS5 Proxy - Server SOCKS5 Proxy HOST + Server SOCKS Proxy + Server SOCKS Proxy HOST Current Proxy HOST: %1$s - Server SOCKS5 Proxy PORT + Server SOCKS Proxy PORT Current Proxy PORT: %1$s + Server SOCKS Proxy username + Server SOCKS Proxy password + Server SOCKS Proxy version Server debug logs Output debug logs from the server to JUI Server system tray icon @@ -300,10 +304,16 @@ Download path Current download path: %1$s Using default download path + Backup path + Current backup path: %1$s + Using default backup path + Local source path + Current local source path: %1$s + Using default local source path Download as CBZ Download chapters into CBZ archives Server WebUI - Whether the server\'s default WebUI is enabled, makes you able to use Tachidesk in your browser + Whether the server\'s default WebUI is enabled, makes you able to use Suwayomi in your browser Open Server WebUI on startup Open the WebUI inside your browser on server startup. Requires the WebUI be enabled Use basic auth to protect your library, requires username and password @@ -327,6 +337,37 @@ Digest auth Auth username Auth password + The below settings configure the connected Suwayomi-Server + Extension repos + Configure your extension repos to allow extensions to be found + Global update interval + Update interval in hours, 0 to disable, values under 6 hours will be ignored + Update manga info + Update manga info alongside manga chapters in the library update + Exclude completed + Exclude completed manga from library update + Exclude unread + Exclude manga with unread chapters from library update + Exclude not started + Exclude manga with no read chapters from library update + Max sources in parallel + How many sources can be updated or downloaded in parallel + Server GraphQL debug logs + Output GraphQL debug logs from the server to JUI + Download new chapters + Chapter download limit + Limit the amount of new chapters that are going to get downloaded + Ignore entries with unread chapters + Backup interval + Automatic backup interval in days, 0 to disable it + Backup TTL + How long to keep a specific backup, in days, 0 to disable + Backup time + When to run the automatic backup + FlareSolverr server url + FlareSolverr request timeout + FlareSolverr session name + FlareSolverr session TTL Check for updates diff --git a/ios/build.gradle.kts b/ios/build.gradle.kts index 0f176134..57504e1b 100644 --- a/ios/build.gradle.kts +++ b/ios/build.gradle.kts @@ -60,6 +60,7 @@ kotlin { implementation(libs.imageloader.core) implementation(libs.imageloader.moko) implementation(libs.materialDialogs.core) + implementation(libs.materialDialogs.datetime) // Threading implementation(libs.coroutines.core) diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 62117b0a..81127198 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -66,6 +66,7 @@ kotlin { api(libs.voyager.transitions) api(libs.voyager.screenmodel) api(libs.materialDialogs.core) + api(libs.materialDialogs.datetime) api(libs.accompanist.pager) api(libs.accompanist.pagerIndicators) api(libs.accompanist.flowLayout) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/PreferencesUiBuilder.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/PreferencesUiBuilder.kt index 6fc19d74..0417ae39 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/PreferencesUiBuilder.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/PreferencesUiBuilder.kt @@ -71,7 +71,6 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar import ca.gosyer.jui.uicore.components.keyboardHandler import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding -import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.jui.uicore.resources.stringResource import com.vanpra.composematerialdialogs.MaterialDialog import com.vanpra.composematerialdialogs.MaterialDialogButtons @@ -85,6 +84,7 @@ import com.vanpra.composematerialdialogs.title import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow @Composable fun PreferenceRow( @@ -151,7 +151,7 @@ fun PreferenceRow( @Composable fun SwitchPreference( - preference: PreferenceMutableStateFlow, + preference: MutableStateFlow, title: String, subtitle: String? = null, icon: ImageVector? = null, @@ -176,7 +176,7 @@ fun SwitchPreference( @Composable fun EditTextPreference( - preference: PreferenceMutableStateFlow, + preference: MutableStateFlow, title: String, subtitle: String? = null, icon: ImageVector? = null, @@ -221,7 +221,7 @@ fun EditTextPreference( @Composable fun ChoicePreference( - preference: PreferenceMutableStateFlow, + preference: MutableStateFlow, choices: ImmutableMap, title: String, subtitle: String? = null, @@ -344,7 +344,7 @@ fun MultiSelectDialog( @Composable fun ColorPreference( - preference: PreferenceMutableStateFlow, + preference: MutableStateFlow, title: String, subtitle: String? = null, enabled: Boolean = true, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt index eeed547e..2932091c 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt @@ -121,7 +121,7 @@ class SettingsLibraryViewModel @Composable fun getDisplayModeChoices() = - DisplayMode.values() + DisplayMode.entries .associateWith { stringResource(it.res) } .toImmutableMap() } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt index 53ff2a07..18f63367 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt @@ -6,40 +6,72 @@ package ca.gosyer.jui.ui.settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Warning import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import ca.gosyer.jui.core.lang.launchIO import ca.gosyer.jui.domain.server.model.Auth import ca.gosyer.jui.domain.server.model.Proxy +import ca.gosyer.jui.domain.server.service.ServerHostPreferences import ca.gosyer.jui.domain.server.service.ServerPreferences +import ca.gosyer.jui.domain.settings.interactor.GetSettings +import ca.gosyer.jui.domain.settings.interactor.SetSettings +import ca.gosyer.jui.domain.settings.model.SetSettingsInput +import ca.gosyer.jui.domain.settings.model.Settings import ca.gosyer.jui.i18n.MR +import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties import ca.gosyer.jui.ui.base.navigation.Toolbar import ca.gosyer.jui.ui.base.prefs.ChoicePreference import ca.gosyer.jui.ui.base.prefs.EditTextPreference import ca.gosyer.jui.ui.base.prefs.PreferenceRow +import ca.gosyer.jui.ui.base.prefs.SwitchPreference import ca.gosyer.jui.ui.main.components.bottomNav import ca.gosyer.jui.ui.viewModel import ca.gosyer.jui.uicore.components.VerticalScrollbar +import ca.gosyer.jui.uicore.components.keyboardHandler import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.scrollbarPadding import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow @@ -51,10 +83,24 @@ import ca.gosyer.jui.uicore.vm.ViewModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey +import com.vanpra.composematerialdialogs.MaterialDialog +import com.vanpra.composematerialdialogs.MaterialDialogState +import com.vanpra.composematerialdialogs.datetime.time.timepicker +import com.vanpra.composematerialdialogs.listItems +import com.vanpra.composematerialdialogs.rememberMaterialDialogState +import com.vanpra.composematerialdialogs.title import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.datetime.LocalTime +import kotlinx.datetime.format.char import me.tatarka.inject.annotations.Inject class SettingsServerScreen : Screen { @@ -80,6 +126,8 @@ class SettingsServerScreen : Screen { authChoices = connectionVM.getAuthChoices(), authUsername = connectionVM.authUsername, authPassword = connectionVM.authPassword, + serverSettings = connectionVM.serverSettings.collectAsState().value, + hosted = connectionVM.host.collectAsState().value, ) } } @@ -89,10 +137,238 @@ expect class SettingsServerHostViewModel : ViewModel @Composable expect fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit +private class ServerSettingMutableStateFlow( + parent: StateFlow, + getSetting: (Settings) -> T, + private val setSetting: (T) -> Unit, + scope: CoroutineScope, + private val state: MutableStateFlow = MutableStateFlow(getSetting(parent.value)), +) : MutableStateFlow by state { + + init { + parent + .onEach { state.value = getSetting(it) } + .launchIn(scope) + } + + override var value: T + get() = state.value + set(value) { + setSetting(value) + } +} + +@Stable +class ServerSettings( + private val getSettings: GetSettings, + private val setSettings: SetSettings, + private val scope: CoroutineScope, + initial: Settings, + private val onError: (String) -> Unit +) { + val settings = MutableStateFlow(initial) + + val autoDownloadNewChapters = getServerFlow( + getSetting = { it.autoDownloadNewChapters }, + getInput = { SetSettingsInput(autoDownloadNewChapters = it) } + ) + val autoDownloadNewChaptersLimit = getServerFlow( + getSetting = { it.autoDownloadNewChaptersLimit.toString() }, + getInput = { SetSettingsInput(autoDownloadNewChaptersLimit = it.toIntOrNull()) } + ) + val backupInterval = getServerFlow( + getSetting = { it.backupInterval.toString() }, + getInput = { SetSettingsInput(backupInterval = it.toIntOrNull()) } + ) + val backupPath = getServerFlow( + getSetting = { it.backupPath }, + getInput = { SetSettingsInput(backupPath = it) } + ) + val backupTTL = getServerFlow( + getSetting = { it.backupTTL.toString() }, + getInput = { SetSettingsInput(backupTTL = it.toIntOrNull()) } + ) + val backupTime = getServerFlow( + getSetting = { it.backupTime }, + getInput = { SetSettingsInput(backupTime = it) } + ) + val basicAuthEnabled = getServerFlow( + getSetting = { it.basicAuthEnabled }, + getInput = { SetSettingsInput(basicAuthEnabled = it) } + ) + val basicAuthPassword = getServerFlow( + getSetting = { it.basicAuthPassword }, + getInput = { SetSettingsInput(basicAuthPassword = it) } + ) + val basicAuthUsername = getServerFlow( + getSetting = { it.basicAuthUsername }, + getInput = { SetSettingsInput(basicAuthUsername = it) } + ) + val debugLogsEnabled = getServerFlow( + getSetting = { it.debugLogsEnabled }, + getInput = { SetSettingsInput(debugLogsEnabled = it) } + ) + val downloadAsCbz = getServerFlow( + getSetting = { it.downloadAsCbz }, + getInput = { SetSettingsInput(downloadAsCbz = it) } + ) + val downloadsPath = getServerFlow( + getSetting = { it.downloadsPath }, + getInput = { SetSettingsInput(downloadsPath = it) } + ) + val electronPath = getServerFlow( + getSetting = { it.electronPath }, + getInput = { SetSettingsInput(electronPath = it) } + ) + val excludeCompleted = getServerFlow( + getSetting = { it.excludeCompleted }, + getInput = { SetSettingsInput(excludeCompleted = it) } + ) + val excludeEntryWithUnreadChapters = getServerFlow( + getSetting = { it.excludeEntryWithUnreadChapters }, + getInput = { SetSettingsInput(excludeEntryWithUnreadChapters = it) } + ) + val excludeNotStarted = getServerFlow( + getSetting = { it.excludeNotStarted }, + getInput = { SetSettingsInput(excludeNotStarted = it) } + ) + val excludeUnreadChapters = getServerFlow( + getSetting = { it.excludeUnreadChapters }, + getInput = { SetSettingsInput(excludeUnreadChapters = it) } + ) + val extensionRepos = getServerFlow( + getSetting = { it.extensionRepos }, + getInput = { SetSettingsInput(extensionRepos = it) } + ) + val flareSolverrEnabled = getServerFlow( + getSetting = { it.flareSolverrEnabled }, + getInput = { SetSettingsInput(flareSolverrEnabled = it) } + ) + val flareSolverrSessionName = getServerFlow( + getSetting = { it.flareSolverrSessionName }, + getInput = { SetSettingsInput(flareSolverrSessionName = it) } + ) + val flareSolverrSessionTtl = getServerFlow( + getSetting = { it.flareSolverrSessionTtl.toString() }, + getInput = { SetSettingsInput(flareSolverrSessionTtl = it.toIntOrNull()) } + ) + val flareSolverrTimeout = getServerFlow( + getSetting = { it.flareSolverrTimeout.toString() }, + getInput = { SetSettingsInput(flareSolverrTimeout = it.toIntOrNull()) } + ) + val flareSolverrUrl = getServerFlow( + getSetting = { it.flareSolverrUrl }, + getInput = { SetSettingsInput(flareSolverrUrl = it) } + ) + val globalUpdateInterval = getServerFlow( + getSetting = { it.globalUpdateInterval.toString() }, + getInput = { SetSettingsInput(globalUpdateInterval = it.toDoubleOrNull()?.takeIf { it !in 0.01..5.99 }) } + ) + val gqlDebugLogsEnabled = getServerFlow( + getSetting = { it.gqlDebugLogsEnabled }, + getInput = { SetSettingsInput(gqlDebugLogsEnabled = it) } + ) + val initialOpenInBrowserEnabled = getServerFlow( + getSetting = { it.initialOpenInBrowserEnabled }, + getInput = { SetSettingsInput(initialOpenInBrowserEnabled = it) } + ) + val ip = getServerFlow( + getSetting = { it.ip }, + getInput = { SetSettingsInput(ip = it) } + ) + val localSourcePath = getServerFlow( + getSetting = { it.localSourcePath }, + getInput = { SetSettingsInput(localSourcePath = it) } + ) + val maxSourcesInParallel = getServerFlow( + getSetting = { it.maxSourcesInParallel.toString() }, + getInput = { SetSettingsInput(maxSourcesInParallel = it.toIntOrNull()) } + ) + val port = getServerFlow( + getSetting = { it.port.toString() }, + getInput = { SetSettingsInput(port = it.toIntOrNull()) } + ) + val socksProxyEnabled = getServerFlow( + getSetting = { it.socksProxyEnabled }, + getInput = { SetSettingsInput(socksProxyEnabled = it) } + ) + val socksProxyHost = getServerFlow( + getSetting = { it.socksProxyHost }, + getInput = { SetSettingsInput(socksProxyHost = it) } + ) + val socksProxyPassword = getServerFlow( + getSetting = { it.socksProxyPassword }, + getInput = { SetSettingsInput(socksProxyPassword = it) } + ) + val socksProxyPort = getServerFlow( + getSetting = { it.socksProxyPort }, + getInput = { SetSettingsInput(socksProxyPort = it) } + ) + val socksProxyUsername = getServerFlow( + getSetting = { it.socksProxyUsername }, + getInput = { SetSettingsInput(socksProxyUsername = it) } + ) + val socksProxyVersion = getServerFlow( + getSetting = { it.socksProxyVersion }, + getInput = { SetSettingsInput(socksProxyVersion = it) } + ) + val systemTrayEnabled = getServerFlow( + getSetting = { it.systemTrayEnabled }, + getInput = { SetSettingsInput(systemTrayEnabled = it) } + ) + val updateMangas = getServerFlow( + getSetting = { it.updateMangas }, + getInput = { SetSettingsInput(updateMangas = it) } + ) + val webUIChannel = getServerFlow( + getSetting = { it.webUIChannel }, + getInput = { SetSettingsInput(webUIChannel = it) } + ) + val webUIFlavor = getServerFlow( + getSetting = { it.webUIFlavor }, + getInput = { SetSettingsInput(webUIFlavor = it) } + ) + val webUIInterface = getServerFlow( + getSetting = { it.webUIInterface }, + getInput = { SetSettingsInput(webUIInterface = it) } + ) + val webUIUpdateCheckInterval = getServerFlow( + getSetting = { it.webUIUpdateCheckInterval }, + getInput = { SetSettingsInput(webUIUpdateCheckInterval = it) } + ) + + private fun getServerFlow( + getSetting: (Settings) -> T, + getInput: (T) -> SetSettingsInput, + ): MutableStateFlow { + return ServerSettingMutableStateFlow( + parent = settings, + getSetting = getSetting, + setSetting = { + scope.launch { + val input = getInput(it) + setSettings.await( + input, + onError = { onError(it.message.orEmpty()) } + ) + val response = getSettings.await(onError = { onError(it.message.orEmpty()) }) + if (response != null) { + settings.value = response + } + } + }, + scope = scope, + ) + } +} + class SettingsServerViewModel @Inject constructor( + private val getSettings: GetSettings, + private val setSettings: SetSettings, serverPreferences: ServerPreferences, + serverHostPreferences: ServerHostPreferences, contextWrapper: ContextWrapper, ) : ViewModel(contextWrapper) { val serverUrl = serverPreferences.server().asStateIn(scope) @@ -101,6 +377,8 @@ class SettingsServerViewModel val proxy = serverPreferences.proxy().asStateIn(scope) + val host = serverHostPreferences.host().asStateIn(scope) + @Composable fun getProxyChoices(): ImmutableMap = persistentMapOf( @@ -127,11 +405,22 @@ class SettingsServerViewModel val authUsername = serverPreferences.authUsername().asStateIn(scope) val authPassword = serverPreferences.authPassword().asStateIn(scope) - private val _serverSettingChanged = MutableStateFlow(false) - val serverSettingChanged = _serverSettingChanged.asStateFlow() + private val _serverSettings = MutableStateFlow(null) + val serverSettings = _serverSettings.asStateFlow() - fun serverSettingChanged() { - _serverSettingChanged.value = true + init { + scope.launchIO { + val initialSettings = getSettings.await(onError = { toast(it.message.orEmpty()) }) + if (initialSettings != null) { + _serverSettings.value = ServerSettings( + getSettings, + setSettings, + scope, + initialSettings, + onError = { toast(it) }, + ) + } + } } } @@ -153,6 +442,8 @@ fun SettingsServerScreenContent( authChoices: ImmutableMap, authUsername: PreferenceMutableStateFlow, authPassword: PreferenceMutableStateFlow, + hosted: Boolean, + serverSettings: ServerSettings?, ) { Scaffold( modifier = Modifier.windowInsetsPadding( @@ -260,6 +551,18 @@ fun SettingsServerScreenContent( ) } } + item { + Divider() + } + if (serverSettings != null) { + ServerSettingsItems(hosted, serverSettings) + } else { + item { + Box(Modifier.fillMaxWidth().height(48.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + } } VerticalScrollbar( rememberScrollbarAdapter(state), @@ -277,3 +580,433 @@ fun SettingsServerScreenContent( } } } + +fun LazyListScope.ServerSettingsItems( + hosted: Boolean, + serverSettings: ServerSettings, +) { + item { + PreferenceRow( + stringResource(MR.strings.host_settings), + Icons.Rounded.Info, + subtitle = stringResource(MR.strings.server_settings_sub), + ) + } + item { + val ipValue by serverSettings.ip.collectAsState() + EditTextPreference( + preference = serverSettings.ip, + title = stringResource(MR.strings.host_ip), + subtitle = stringResource(MR.strings.host_ip_sub, ipValue), + enabled = !hosted + ) + } + item { + val portValue by serverSettings.port.collectAsState() + EditTextPreference( + preference = serverSettings.port, + title = stringResource(MR.strings.host_port), + subtitle = stringResource(MR.strings.host_port_sub, portValue), + enabled = !hosted + ) + } + item { + val dialog = rememberMaterialDialogState() + PreferenceRow( + stringResource(MR.strings.extension_repos), + subtitle = stringResource(MR.strings.extension_repos_sub), + onClick = dialog::show + ) + val repos by serverSettings.extensionRepos.collectAsState() + ExtensionReposDialog( + dialog, + repos, + onSetRepos = { + serverSettings.extensionRepos.value = it + } + ) + } + item { + SwitchPreference( + preference = serverSettings.socksProxyEnabled, + title = stringResource(MR.strings.host_socks_enabled), + ) + } + item { + val socksProxyEnabled by serverSettings.socksProxyEnabled.collectAsState() + val proxyHost by serverSettings.socksProxyHost.collectAsState() + EditTextPreference( + preference = serverSettings.socksProxyHost, + title = stringResource(MR.strings.host_socks_host), + subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost), + enabled = socksProxyEnabled, + ) + } + item { + val socksProxyEnabled by serverSettings.socksProxyEnabled.collectAsState() + val proxyPort by serverSettings.socksProxyPort.collectAsState() + EditTextPreference( + preference = serverSettings.socksProxyPort, + title = stringResource(MR.strings.host_socks_port), + subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort), + enabled = socksProxyEnabled, + ) + } + item { + val socksProxyEnabled by serverSettings.socksProxyEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.socksProxyUsername, + title = stringResource(MR.strings.host_socks_username), + enabled = socksProxyEnabled, + ) + } + item { + val socksProxyEnabled by serverSettings.socksProxyEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.socksProxyPassword, + title = stringResource(MR.strings.host_socks_password), + visualTransformation = PasswordVisualTransformation(), + enabled = socksProxyEnabled, + ) + } + item { + val socksProxyEnabled by serverSettings.socksProxyEnabled.collectAsState() + ChoicePreference( + preference = serverSettings.socksProxyVersion, + choices = mapOf( + 4 to "SOCKS4", + 5 to "SOCKS5" + ).toImmutableMap(), + title = stringResource(MR.strings.host_socks_version), + enabled = socksProxyEnabled, + ) + } + item { + EditTextPreference( + preference = serverSettings.globalUpdateInterval, + title = stringResource(MR.strings.global_update_interval), + subtitle = stringResource(MR.strings.global_update_interval_sub) + ) + } + item { + SwitchPreference( + preference = serverSettings.updateMangas, + title = stringResource(MR.strings.update_manga_info), + subtitle = stringResource(MR.strings.update_manga_info_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.excludeCompleted, + title = stringResource(MR.strings.exclude_completed), + subtitle = stringResource(MR.strings.exclude_completed_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.excludeUnreadChapters, + title = stringResource(MR.strings.exclude_unread), + subtitle = stringResource(MR.strings.exclude_unread_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.excludeNotStarted, + title = stringResource(MR.strings.exclude_not_started), + subtitle = stringResource(MR.strings.exclude_not_started_sub), + ) + } + item { + EditTextPreference( + preference = serverSettings.maxSourcesInParallel, + title = stringResource(MR.strings.max_sources_parallel), + subtitle = stringResource(MR.strings.max_sources_parallel_sub) + ) + } + + item { + SwitchPreference( + preference = serverSettings.downloadAsCbz, + title = stringResource(MR.strings.host_download_as_cbz), + subtitle = stringResource(MR.strings.host_download_as_cbz_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.autoDownloadNewChapters, + title = stringResource(MR.strings.download_new_chapters), + ) + } + item { + EditTextPreference( + preference = serverSettings.autoDownloadNewChaptersLimit, + title = stringResource(MR.strings.download_chapter_limit), + subtitle = stringResource(MR.strings.download_chapter_limit_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.excludeEntryWithUnreadChapters, + title = stringResource(MR.strings.ignore_unread_entries), + ) + } + + item { + SwitchPreference( + preference = serverSettings.debugLogsEnabled, + title = stringResource(MR.strings.host_debug_logging), + subtitle = stringResource(MR.strings.host_debug_logging_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.gqlDebugLogsEnabled, + title = stringResource(MR.strings.graphql_debug_logs), + subtitle = stringResource(MR.strings.graphql_debug_logs_sub), + ) + } + item { + SwitchPreference( + preference = serverSettings.systemTrayEnabled, + title = stringResource(MR.strings.host_system_tray), + subtitle = stringResource(MR.strings.host_system_tray_sub), + ) + } + item { + // val webUIEnabledValue by serverSettings.webUIEnabled.collectAsState() + SwitchPreference( + preference = serverSettings.initialOpenInBrowserEnabled, + title = stringResource(MR.strings.host_open_in_browser), + subtitle = stringResource(MR.strings.host_open_in_browser_sub), + enabled = !hosted, //webUIEnabledValue, + ) + } + + item { + EditTextPreference( + preference = serverSettings.backupInterval, + title = stringResource(MR.strings.backup_interval), + subtitle = stringResource(MR.strings.backup_interval_sub), + ) + } + item { + EditTextPreference( + preference = serverSettings.backupTTL, + title = stringResource(MR.strings.backup_ttl), + subtitle = stringResource(MR.strings.backup_ttl_sub), + ) + } + item { + val dialog = rememberMaterialDialogState() + val backupTime by serverSettings.backupTime.collectAsState() + PreferenceRow( + title = stringResource(MR.strings.backup_time), + subtitle = stringResource(MR.strings.backup_time_sub), + onClick = dialog::show + ) + BackupTimeDialog( + dialog, + backupTime, + onSetTime = { + serverSettings.backupTime.value = it + } + ) + } + + item { + SwitchPreference( + preference = serverSettings.basicAuthEnabled, + title = stringResource(MR.strings.basic_auth), + subtitle = stringResource(MR.strings.host_basic_auth_sub), + enabled = !hosted + ) + } + + item { + val basicAuthEnabledValue by serverSettings.basicAuthEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.basicAuthUsername, + title = stringResource(MR.strings.host_basic_auth_username), + enabled = basicAuthEnabledValue && !hosted, + ) + } + item { + val basicAuthEnabledValue by serverSettings.basicAuthEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.basicAuthPassword, + title = stringResource(MR.strings.host_basic_auth_password), + visualTransformation = PasswordVisualTransformation(), + enabled = basicAuthEnabledValue && !hosted, + ) + } + item { + SwitchPreference( + preference = serverSettings.flareSolverrEnabled, + title = "FlareSolverr enabled", + subtitle = "Use a FlareSolverr instance to bypass CloudFlare. Manual setup required", + ) + } + item { + val flareSolverrEnabled by serverSettings.flareSolverrEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.flareSolverrUrl, + title = stringResource(MR.strings.flaresolverr_url), + enabled = flareSolverrEnabled, + ) + } + item { + val flareSolverrEnabled by serverSettings.flareSolverrEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.flareSolverrTimeout, + title = stringResource(MR.strings.flaresolverr_timeout), + enabled = flareSolverrEnabled, + ) + } + item { + val flareSolverrEnabled by serverSettings.flareSolverrEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.flareSolverrSessionName, + title = stringResource(MR.strings.flaresolverr_session_name), + enabled = flareSolverrEnabled, + ) + } + item { + val flareSolverrEnabled by serverSettings.flareSolverrEnabled.collectAsState() + EditTextPreference( + preference = serverSettings.flareSolverrSessionTtl, + title = stringResource(MR.strings.flaresolverr_session_ttl), + enabled = flareSolverrEnabled, + ) + } + + item { + Divider() + } +} + +private val repoRegex = + ( + "https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" + + "\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?" + ).toRegex() + +@Composable +fun ExtensionReposDialog( + state: MaterialDialogState, + extensionRepos: List, + onSetRepos: (List) -> Unit +) { + val repos = remember(state.showing) { + extensionRepos.toMutableStateList() + } + var newRepo by remember(state.showing) { mutableStateOf("") } + MaterialDialog( + state, + properties = getMaterialDialogProperties(), + buttons = { + negativeButton(stringResource(MR.strings.action_cancel)) + positiveButton(stringResource(MR.strings.action_ok), onClick = { onSetRepos(repos.toList()) }) + }, + ) { + title(stringResource(MR.strings.extension_repos)) + Row( + Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + val repoMatches by derivedStateOf { + newRepo.matches(repoRegex) + } + OutlinedTextField( + value = newRepo, + onValueChange = { newRepo = it }, + modifier = Modifier.weight(4f) + .keyboardHandler( + singleLine = true, + enterAction = { + if (repoMatches) { + repos.add(newRepo) + newRepo = "" + } + } + ), + isError = newRepo.isNotBlank() && !repoMatches, + ) + IconButton( + onClick = { + repos.add(newRepo) + newRepo = "" + }, + enabled = repoMatches, + modifier = Modifier.weight(1f, fill = false) + ) { + Icon( + Icons.Rounded.Add, + contentDescription = stringResource(MR.strings.action_add) + ) + } + } + + listItems( + modifier = Modifier.padding(bottom = 8.dp), + list = repos, + closeOnClick = false, + ) { _, item -> + Row( + Modifier.fillMaxWidth().padding(vertical = 12.dp, horizontal = 24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + item, + color = MaterialTheme.colors.onSurface, + style = MaterialTheme.typography.body1, + modifier = Modifier + .weight(4f) + .wrapContentWidth(Alignment.Start) + ) + IconButton( + onClick = { repos.remove(item) }, + modifier = Modifier.weight(1f, fill = false) + ) { + Icon( + Icons.Rounded.Delete, + contentDescription = stringResource(MR.strings.action_delete) + ) + } + } + } + } +} + +val formatter = LocalTime.Format { + hour() + char(':') + minute() +} +@Composable +fun BackupTimeDialog( + state: MaterialDialogState, + backupTime: String, + onSetTime: (String) -> Unit +) { + val time = remember(state.showing) { + LocalTime.parse(backupTime, formatter) + } + MaterialDialog( + state, + properties = getMaterialDialogProperties(), + buttons = { + negativeButton(stringResource(MR.strings.action_cancel)) + positiveButton(stringResource(MR.strings.action_ok)) + }, + ) { + timepicker( + time, + title = stringResource(MR.strings.backup_time), + onTimeChange = { + onSetTime(formatter.format(it)) + }, + ) + } +} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt index 097f1a80..0443013f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt @@ -82,7 +82,6 @@ fun SourceSettingsScreenContent(settings: ImmutableList ).asPaddingValues(), ) { items(settings, { it.props.hashCode() }) { - @Suppress("UNCHECKED_CAST") when (it) { is CheckBox, is Switch -> { TwoStatePreference(it as TwoState, it is CheckBox) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt index 67b2cf73..cec61110 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt @@ -24,7 +24,6 @@ import ca.gosyer.jui.ui.base.prefs.EditTextPreference import ca.gosyer.jui.ui.base.prefs.PreferenceRow import ca.gosyer.jui.ui.base.prefs.SwitchPreference import ca.gosyer.jui.ui.util.system.folderPicker -import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.jui.uicore.prefs.asStateIn import ca.gosyer.jui.uicore.prefs.asStringStateIn import ca.gosyer.jui.uicore.resources.stringResource @@ -56,15 +55,9 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie host = serverVm.host, ip = serverVm.ip, port = serverVm.port, - socksProxyEnabled = serverVm.socksProxyEnabled, - socksProxyHost = serverVm.socksProxyHost, - socksProxyPort = serverVm.socksProxyPort, - debugLogsEnabled = serverVm.debugLogsEnabled, - systemTrayEnabled = serverVm.systemTrayEnabled, downloadPath = serverVm.downloadPath, - downloadAsCbz = serverVm.downloadAsCbz, - webUIEnabled = serverVm.webUIEnabled, - openInBrowserEnabled = serverVm.openInBrowserEnabled, + backupPath = serverVm.backupPath, + localSourcePath = serverVm.localSourcePath, basicAuthEnabled = serverVm.basicAuthEnabled, basicAuthUsername = serverVm.basicAuthUsername, basicAuthPassword = serverVm.basicAuthPassword, @@ -84,22 +77,12 @@ actual class SettingsServerHostViewModel val ip = serverHostPreferences.ip().asStateIn(scope) val port = serverHostPreferences.port().asStringStateIn(scope) - // Proxy - val socksProxyEnabled = serverHostPreferences.socksProxyEnabled().asStateIn(scope) - val socksProxyHost = serverHostPreferences.socksProxyHost().asStateIn(scope) - val socksProxyPort = serverHostPreferences.socksProxyPort().asStringStateIn(scope) - - // Misc - val debugLogsEnabled = serverHostPreferences.debugLogsEnabled().asStateIn(scope) - val systemTrayEnabled = serverHostPreferences.systemTrayEnabled().asStateIn(scope) - // Downloader val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) - val downloadAsCbz = serverHostPreferences.downloadAsCbz().asStateIn(scope) - - // WebUI - val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) - val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope) + // Backup + val backupPath = serverHostPreferences.backupPath().asStateIn(scope) + // LocalSource + val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope) // Authentication val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) @@ -145,21 +128,15 @@ fun LazyListScope.ServerHostItems( hostValue: Boolean, basicAuthEnabledValue: Boolean, serverSettingChanged: () -> Unit, - host: PreferenceMutableStateFlow, - ip: PreferenceMutableStateFlow, - port: PreferenceMutableStateFlow, - socksProxyEnabled: PreferenceMutableStateFlow, - socksProxyHost: PreferenceMutableStateFlow, - socksProxyPort: PreferenceMutableStateFlow, - debugLogsEnabled: PreferenceMutableStateFlow, - systemTrayEnabled: PreferenceMutableStateFlow, - downloadPath: PreferenceMutableStateFlow, - downloadAsCbz: PreferenceMutableStateFlow, - webUIEnabled: PreferenceMutableStateFlow, - openInBrowserEnabled: PreferenceMutableStateFlow, - basicAuthEnabled: PreferenceMutableStateFlow, - basicAuthUsername: PreferenceMutableStateFlow, - basicAuthPassword: PreferenceMutableStateFlow, + host: MutableStateFlow, + ip: MutableStateFlow, + port: MutableStateFlow, + downloadPath: MutableStateFlow, + backupPath: MutableStateFlow, + localSourcePath: MutableStateFlow, + basicAuthEnabled: MutableStateFlow, + basicAuthUsername: MutableStateFlow, + basicAuthPassword: MutableStateFlow, ) { item { SwitchPreference(preference = host, title = stringResource(MR.strings.host_server)) @@ -190,47 +167,6 @@ fun LazyListScope.ServerHostItems( changeListener = serverSettingChanged, ) } - item { - SwitchPreference( - preference = socksProxyEnabled, - title = stringResource(MR.strings.host_socks_enabled), - changeListener = serverSettingChanged, - ) - } - item { - val proxyHost by socksProxyHost.collectAsState() - EditTextPreference( - preference = socksProxyHost, - title = stringResource(MR.strings.host_socks_host), - subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost), - changeListener = serverSettingChanged, - ) - } - item { - val proxyPort by socksProxyPort.collectAsState() - EditTextPreference( - preference = socksProxyPort, - title = stringResource(MR.strings.host_socks_port), - subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort), - changeListener = serverSettingChanged, - ) - } - item { - SwitchPreference( - preference = debugLogsEnabled, - title = stringResource(MR.strings.host_debug_logging), - subtitle = stringResource(MR.strings.host_debug_logging_sub), - changeListener = serverSettingChanged, - ) - } - item { - SwitchPreference( - preference = systemTrayEnabled, - title = stringResource(MR.strings.host_system_tray), - subtitle = stringResource(MR.strings.host_system_tray_sub), - changeListener = serverSettingChanged, - ) - } item { val downloadPathValue by downloadPath.collectAsState() PreferenceRow( @@ -253,29 +189,45 @@ fun LazyListScope.ServerHostItems( ) } item { - SwitchPreference( - preference = downloadAsCbz, - title = stringResource(MR.strings.host_download_as_cbz), - subtitle = stringResource(MR.strings.host_download_as_cbz_sub), - changeListener = serverSettingChanged, + val backupPathValue by backupPath.collectAsState() + PreferenceRow( + title = stringResource(MR.strings.host_backup_path), + subtitle = if (backupPathValue.isEmpty()) { + stringResource(MR.strings.host_backup_path_sub_empty) + } else { + stringResource(MR.strings.host_backup_path_sub, backupPathValue) + }, + onClick = { + folderPicker { + backupPath.value = it.toString() + serverSettingChanged() + } + }, + onLongClick = { + backupPath.value = "" + serverSettingChanged() + }, ) } item { - SwitchPreference( - preference = webUIEnabled, - title = stringResource(MR.strings.host_webui), - subtitle = stringResource(MR.strings.host_webui_sub), - changeListener = serverSettingChanged, - ) - } - item { - val webUIEnabledValue by webUIEnabled.collectAsState() - SwitchPreference( - preference = openInBrowserEnabled, - title = stringResource(MR.strings.host_open_in_browser), - subtitle = stringResource(MR.strings.host_open_in_browser_sub), - changeListener = serverSettingChanged, - enabled = webUIEnabledValue, + val localSourcePathValue by localSourcePath.collectAsState() + PreferenceRow( + title = stringResource(MR.strings.host_local_source_path), + subtitle = if (localSourcePathValue.isEmpty()) { + stringResource(MR.strings.host_local_source_path_sub_empty) + } else { + stringResource(MR.strings.host_local_source_path_sub, localSourcePathValue) + }, + onClick = { + folderPicker { + localSourcePath.value = it.toString() + serverSettingChanged() + } + }, + onLongClick = { + localSourcePath.value = "" + serverSettingChanged() + }, ) } item {