Implement configuration of server settings during runtime

This commit is contained in:
Syer10
2024-03-30 16:22:50 -04:00
parent a04762842b
commit 52ea0f1c37
34 changed files with 1544 additions and 289 deletions

View File

@@ -31,6 +31,7 @@ dependencies {
implementation(libs.imageloader.core) implementation(libs.imageloader.core)
implementation(libs.imageloader.moko) implementation(libs.imageloader.moko)
implementation(libs.materialDialogs.core) implementation(libs.materialDialogs.core)
implementation(libs.materialDialogs.datetime)
// Android // Android
implementation(libs.androidx.core) implementation(libs.androidx.core)
@@ -111,6 +112,9 @@ android {
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
isMinifyEnabled = true
isShrinkResources = true
setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"))
} }
getByName("release") { getByName("release") {
isMinifyEnabled = true isMinifyEnabled = true

View File

@@ -1,5 +1,4 @@
query AllSettings { fragment SettingsTypeFragment on SettingsType {
settings {
autoDownloadNewChapters autoDownloadNewChapters
autoDownloadNewChaptersLimit autoDownloadNewChaptersLimit
backupInterval backupInterval
@@ -43,4 +42,105 @@ query AllSettings {
webUIInterface webUIInterface
webUIUpdateCheckInterval webUIUpdateCheckInterval
} }
query AllSettings {
settings {
...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
}
} }

View File

@@ -7,6 +7,7 @@
package ca.gosyer.jui.data package ca.gosyer.jui.data
import ca.gosyer.jui.core.lang.addSuffix 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.backup.service.BackupRepository
import ca.gosyer.jui.domain.category.service.CategoryRepository import ca.gosyer.jui.domain.category.service.CategoryRepository
import ca.gosyer.jui.domain.chapter.service.ChapterRepository 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.Http
import ca.gosyer.jui.domain.server.service.ServerPreferences import ca.gosyer.jui.domain.server.service.ServerPreferences
import ca.gosyer.jui.domain.settings.service.SettingsRepository 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.source.service.SourceRepository
import ca.gosyer.jui.domain.updates.service.UpdatesRepository import ca.gosyer.jui.domain.updates.service.UpdatesRepository
import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.ApolloClient
@@ -79,11 +81,15 @@ interface DataComponent {
fun mangaRepository(ktorfit: Ktorfit) = ktorfit.create<MangaRepository>() fun mangaRepository(ktorfit: Ktorfit) = ktorfit.create<MangaRepository>()
@Provides @Provides
fun settingsRepository(ktorfit: Ktorfit) = ktorfit.create<SettingsRepository>() fun settingsRepositoryOld(ktorfit: Ktorfit) = ktorfit.create<SettingsRepositoryOld>()
@Provides @Provides
fun sourceRepository(ktorfit: Ktorfit) = ktorfit.create<SourceRepository>() fun sourceRepository(ktorfit: Ktorfit) = ktorfit.create<SourceRepository>()
@Provides @Provides
fun updatesRepository(ktorfit: Ktorfit) = ktorfit.create<UpdatesRepository>() fun updatesRepository(ktorfit: Ktorfit) = ktorfit.create<UpdatesRepository>()
@Provides
fun settingsRepository(apolloClient: ApolloClient): SettingsRepository =
SettingsRepositoryImpl(apolloClient)
} }

View File

@@ -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<Settings> {
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<Unit> {
return apolloClient.mutation(input.toMutation())
.toFlow()
.map {
it.dataOrThrow()
Unit
}
.flowOn(Dispatchers.IO)
}
}

View File

@@ -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> T?.toOptional() = Optional.presentIfNotNull(this)

View File

@@ -39,6 +39,7 @@ dependencies {
implementation(libs.imageloader.core) implementation(libs.imageloader.core)
implementation(libs.imageloader.moko) implementation(libs.imageloader.moko)
implementation(libs.materialDialogs.core) implementation(libs.materialDialogs.core)
implementation(libs.materialDialogs.datetime)
// UI (Swing) // UI (Swing)
implementation(libs.darklaf) implementation(libs.darklaf)

View File

@@ -36,7 +36,6 @@ const val filePattern =
'$' + '$' +
"{LOG_EXCEPTION_CONVERSION_WORD:-%xEx}" "{LOG_EXCEPTION_CONVERSION_WORD:-%xEx}"
@Suppress("UPPER_BOUND_VIOLATED_WARNING")
fun initializeLogger(loggingLocation: Path) { fun initializeLogger(loggingLocation: Path) {
val ctx = LogManager.getContext(false) as LoggerContext val ctx = LogManager.getContext(false) as LoggerContext
val builder = ConfigurationBuilderFactory.newConfigurationBuilder() val builder = ConfigurationBuilderFactory.newConfigurationBuilder()

View File

@@ -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<Boolean> = object : Preference<Boolean> {
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<Boolean> = MutableStateFlow(false)
override fun stateIn(scope: CoroutineScope): StateFlow<Boolean> = MutableStateFlow(false)
override fun set(value: Boolean) {}
}
}

View File

@@ -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.reader.service.ReaderPreferences
import ca.gosyer.jui.domain.server.Http import ca.gosyer.jui.domain.server.Http
import ca.gosyer.jui.domain.server.httpClient 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.server.service.ServerPreferences
import ca.gosyer.jui.domain.source.service.CatalogPreferences import ca.gosyer.jui.domain.source.service.CatalogPreferences
import ca.gosyer.jui.domain.ui.service.UiPreferences import ca.gosyer.jui.domain.ui.service.UiPreferences
@@ -107,6 +108,11 @@ interface SharedDomainComponent : CoreComponent {
val updatePreferencesFactory: UpdatePreferences val updatePreferencesFactory: UpdatePreferences
get() = UpdatePreferences(preferenceFactory.create("update")) get() = UpdatePreferences(preferenceFactory.create("update"))
@get:AppScope
@get:Provides
val serverHostPreferencesFactory: ServerHostPreferences
get() = ServerHostPreferences(preferenceFactory.create("host"))
@get:AppScope @get:AppScope
@get:Provides @get:Provides
val libraryUpdateServiceFactory: LibraryUpdateService val libraryUpdateServiceFactory: LibraryUpdateService

View File

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

View File

@@ -6,7 +6,7 @@
package ca.gosyer.jui.domain.settings.interactor 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.catch
import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -15,7 +15,7 @@ import org.lighthousegames.logging.logging
class AboutServer class AboutServer
@Inject @Inject
constructor( constructor(
private val settingsRepository: SettingsRepository, private val settingsRepositoryOld: SettingsRepositoryOld,
) { ) {
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
asFlow() asFlow()
@@ -25,7 +25,7 @@ class AboutServer
} }
.singleOrNull() .singleOrNull()
fun asFlow() = settingsRepository.aboutServer() fun asFlow() = settingsRepositoryOld.aboutServer()
companion object { companion object {
private val log = logging() private val log = logging()

View File

@@ -6,7 +6,7 @@
package ca.gosyer.jui.domain.settings.interactor 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.catch
import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.singleOrNull
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
@@ -15,7 +15,7 @@ import org.lighthousegames.logging.logging
class CheckUpdate class CheckUpdate
@Inject @Inject
constructor( constructor(
private val settingsRepository: SettingsRepository, private val settingsRepositoryOld: SettingsRepositoryOld,
) { ) {
suspend fun await(onError: suspend (Throwable) -> Unit = {}) = suspend fun await(onError: suspend (Throwable) -> Unit = {}) =
asFlow() asFlow()
@@ -25,7 +25,7 @@ class CheckUpdate
} }
.singleOrNull() .singleOrNull()
fun asFlow() = settingsRepository.checkUpdate() fun asFlow() = settingsRepositoryOld.checkUpdate()
companion object { companion object {
private val log = logging() private val log = logging()

View File

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

View File

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

View File

@@ -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<String>? = 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,
)

View File

@@ -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<String>,
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,
)

View File

@@ -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__;
}

View File

@@ -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__;
}

View File

@@ -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__;
}

View File

@@ -6,16 +6,13 @@
package ca.gosyer.jui.domain.settings.service package ca.gosyer.jui.domain.settings.service
import ca.gosyer.jui.domain.settings.model.About import ca.gosyer.jui.domain.settings.model.SetSettingsInput
import de.jensklingenberg.ktorfit.http.GET import ca.gosyer.jui.domain.settings.model.Settings
import de.jensklingenberg.ktorfit.http.POST
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SettingsRepository { interface SettingsRepository {
@GET("api/v1/settings/about")
fun aboutServer(): Flow<About>
@POST("api/v1/settings/check-update") fun getSettings(): Flow<Settings>
fun checkUpdate(): Flow<HttpResponse>
fun setSettings(input: SetSettingsInput): Flow<Unit>
} }

View File

@@ -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<About>
@POST("api/v1/settings/check-update")
fun checkUpdate(): Flow<HttpResponse>
}

View File

@@ -18,11 +18,6 @@ actual interface DomainComponent : SharedDomainComponent {
val serverHostPreferences: ServerHostPreferences val serverHostPreferences: ServerHostPreferences
@get:AppScope
@get:Provides
val serverHostPreferencesFactory: ServerHostPreferences
get() = ServerHostPreferences(preferenceFactory.create("host"))
@get:AppScope @get:AppScope
@get:Provides @get:Provides
val serverServiceFactory: ServerService val serverServiceFactory: ServerService

View File

@@ -10,10 +10,10 @@ import ca.gosyer.jui.core.prefs.Preference
import ca.gosyer.jui.core.prefs.PreferenceStore import ca.gosyer.jui.core.prefs.PreferenceStore
import ca.gosyer.jui.domain.server.service.host.ServerHostPreference import ca.gosyer.jui.domain.server.service.host.ServerHostPreference
class ServerHostPreferences( actual class ServerHostPreferences actual constructor(
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun host(): Preference<Boolean> = preferenceStore.getBoolean("host", true) actual fun host(): Preference<Boolean> = preferenceStore.getBoolean("host", true)
private val ip = ServerHostPreference.IP(preferenceStore) private val ip = ServerHostPreference.IP(preferenceStore)
@@ -23,45 +23,20 @@ class ServerHostPreferences(
fun port(): Preference<Int> = port.preference() fun port(): Preference<Int> = port.preference()
// Proxy
private val socksProxyEnabled = ServerHostPreference.SocksProxyEnabled(preferenceStore)
fun socksProxyEnabled(): Preference<Boolean> = socksProxyEnabled.preference()
private val socksProxyHost = ServerHostPreference.SocksProxyHost(preferenceStore)
fun socksProxyHost(): Preference<String> = socksProxyHost.preference()
private val socksProxyPort = ServerHostPreference.SocksProxyPort(preferenceStore)
fun socksProxyPort(): Preference<Int> = socksProxyPort.preference()
// Misc
private val debugLogsEnabled = ServerHostPreference.DebugLogsEnabled(preferenceStore)
fun debugLogsEnabled(): Preference<Boolean> = debugLogsEnabled.preference()
private val systemTrayEnabled = ServerHostPreference.SystemTrayEnabled(preferenceStore)
fun systemTrayEnabled(): Preference<Boolean> = systemTrayEnabled.preference()
// Downloader // Downloader
private val downloadPath = ServerHostPreference.DownloadPath(preferenceStore) private val downloadPath = ServerHostPreference.DownloadPath(preferenceStore)
fun downloadPath(): Preference<String> = downloadPath.preference() fun downloadPath(): Preference<String> = downloadPath.preference()
private val downloadAsCbz = ServerHostPreference.DownloadAsCbz(preferenceStore) // Backup
private val backupPath = ServerHostPreference.BackupPath(preferenceStore)
fun downloadAsCbz(): Preference<Boolean> = downloadAsCbz.preference() fun backupPath(): Preference<String> = backupPath.preference()
// WebUI // LocalSource
private val webUIEnabled = ServerHostPreference.WebUIEnabled(preferenceStore) private val localSourcePath = ServerHostPreference.LocalSourcePath(preferenceStore)
fun webUIEnabled(): Preference<Boolean> = webUIEnabled.preference() fun localSourcePath(): Preference<String> = localSourcePath.preference()
private val openInBrowserEnabled = ServerHostPreference.OpenInBrowserEnabled(preferenceStore)
fun openInBrowserEnabled(): Preference<Boolean> = openInBrowserEnabled.preference()
// Authentication // Authentication
private val basicAuthEnabled = ServerHostPreference.BasicAuthEnabled(preferenceStore) private val basicAuthEnabled = ServerHostPreference.BasicAuthEnabled(preferenceStore)
@@ -80,19 +55,15 @@ class ServerHostPreferences(
listOf( listOf(
ip, ip,
port, port,
socksProxyEnabled,
socksProxyHost,
socksProxyPort,
debugLogsEnabled,
systemTrayEnabled,
downloadPath, downloadPath,
downloadAsCbz, backupPath,
webUIEnabled, localSourcePath,
openInBrowserEnabled,
basicAuthEnabled, basicAuthEnabled,
basicAuthUsername, basicAuthUsername,
basicAuthPassword, basicAuthPassword,
).mapNotNull { ).mapNotNull {
it.getProperty() it.getProperty()
}.toTypedArray() }.plus(
"-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false"
).toTypedArray()
} }

View File

@@ -79,49 +79,6 @@ sealed class ServerHostPreference<T : Any> {
4567, 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 // Downloader
class DownloadPath( class DownloadPath(
preferenceStore: PreferenceStore, preferenceStore: PreferenceStore,
@@ -131,31 +88,22 @@ sealed class ServerHostPreference<T : Any> {
"", "",
) )
class DownloadAsCbz( // Backup
class BackupPath(
preferenceStore: PreferenceStore, preferenceStore: PreferenceStore,
) : BooleanServerHostPreference( ) : StringServerHostPreference(
preferenceStore, preferenceStore,
"downloadAsCbz", "backupPath",
false, "",
) )
// WebUI // LocalSource
class WebUIEnabled( class LocalSourcePath(
preferenceStore: PreferenceStore, preferenceStore: PreferenceStore,
) : BooleanServerHostPreference( ) : StringServerHostPreference(
preferenceStore, preferenceStore,
"webUIEnabled", "localSourcePath",
false, "",
true,
)
class OpenInBrowserEnabled(
preferenceStore: PreferenceStore,
) : BooleanServerHostPreference(
preferenceStore,
"initialOpenInBrowserEnabled",
false,
true,
) )
// Authentication // Authentication

View File

@@ -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<Boolean> = object : Preference<Boolean> {
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<Boolean> = MutableStateFlow(false)
override fun stateIn(scope: CoroutineScope): StateFlow<Boolean> = MutableStateFlow(false)
override fun set(value: Boolean) {}
}
}

View File

@@ -99,6 +99,7 @@ accompanist-systemUIController = { module = "com.google.accompanist:accompanist-
imageloader-core = { module = "io.github.qdsfdhvh:image-loader", version.ref = "imageloader" } 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" } 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-core = { module = "ca.gosyer:compose-material-dialogs-core", version.ref = "materialDialogs" }
materialDialogs-datetime = { module = "ca.gosyer:compose-material-dialogs-datetime", version.ref = "materialDialogs" }
# Android # Android
androidx-core = { module = "androidx.core:core-ktx", version.ref = "core" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "core" }

View File

@@ -220,9 +220,9 @@
<!-- Backup Settings --> <!-- Backup Settings -->
<string name="backup_restore">Restore Backup</string> <string name="backup_restore">Restore Backup</string>
<string name="backup_restore_sub">Restore a backup into Tachidesk</string> <string name="backup_restore_sub">Restore a backup to your library</string>
<string name="backup_create">Create Backup</string> <string name="backup_create">Create Backup</string>
<string name="backup_create_sub">Create a backup from Tachidesk</string> <string name="backup_create_sub">Create a backup from your library</string>
<string name="missing_sources">Missing sources:</string> <string name="missing_sources">Missing sources:</string>
<!-- General Settings --> <!-- General Settings -->
@@ -282,17 +282,21 @@
<!-- Server Settings --> <!-- Server Settings -->
<string name="host_server">Host server inside Tachidesk-JUI</string> <string name="host_server">Host server inside Tachidesk-JUI</string>
<string name="host_settings">Tachidesk-Server settings</string> <string name="host_settings">Suwayomi-Server settings</string>
<string name="host_settings_sub">The below settings configure the internal Tachidesk-Server</string> <string name="host_settings_sub">The below settings configure the internal Suwayomi-Server</string>
<string name="host_ip">Server IP</string> <string name="host_ip">Server IP</string>
<string name="host_ip_sub">Current server IP: %1$s</string> <string name="host_ip_sub">Current server IP: %1$s</string>
<string name="host_port">Server PORT</string> <string name="host_port">Server PORT</string>
<string name="host_port_sub">Current server PORT: %1$s</string> <string name="host_port_sub">Current server PORT: %1$s</string>
<string name="host_socks_enabled">Server SOCKS5 Proxy</string> <string name="host_socks_enabled">Server SOCKS Proxy</string>
<string name="host_socks_host">Server SOCKS5 Proxy HOST</string> <string name="host_socks_host">Server SOCKS Proxy HOST</string>
<string name="host_socks_host_sub">Current Proxy HOST: %1$s</string> <string name="host_socks_host_sub">Current Proxy HOST: %1$s</string>
<string name="host_socks_port">Server SOCKS5 Proxy PORT</string> <string name="host_socks_port">Server SOCKS Proxy PORT</string>
<string name="host_socks_port_sub">Current Proxy PORT: %1$s</string> <string name="host_socks_port_sub">Current Proxy PORT: %1$s</string>
<string name="host_socks_username">Server SOCKS Proxy username</string>
<string name="host_socks_password">Server SOCKS Proxy password</string>
<string name="host_socks_version">Server SOCKS Proxy version</string>
<string name="host_debug_logging">Server debug logs</string> <string name="host_debug_logging">Server debug logs</string>
<string name="host_debug_logging_sub">Output debug logs from the server to JUI</string> <string name="host_debug_logging_sub">Output debug logs from the server to JUI</string>
<string name="host_system_tray">Server system tray icon</string> <string name="host_system_tray">Server system tray icon</string>
@@ -300,10 +304,16 @@
<string name="host_download_path">Download path</string> <string name="host_download_path">Download path</string>
<string name="host_download_path_sub">Current download path: %1$s</string> <string name="host_download_path_sub">Current download path: %1$s</string>
<string name="host_download_path_sub_empty">Using default download path</string> <string name="host_download_path_sub_empty">Using default download path</string>
<string name="host_backup_path">Backup path</string>
<string name="host_backup_path_sub">Current backup path: %1$s</string>
<string name="host_backup_path_sub_empty">Using default backup path</string>
<string name="host_local_source_path">Local source path</string>
<string name="host_local_source_path_sub">Current local source path: %1$s</string>
<string name="host_local_source_path_sub_empty">Using default local source path</string>
<string name="host_download_as_cbz">Download as CBZ</string> <string name="host_download_as_cbz">Download as CBZ</string>
<string name="host_download_as_cbz_sub">Download chapters into CBZ archives</string> <string name="host_download_as_cbz_sub">Download chapters into CBZ archives</string>
<string name="host_webui">Server WebUI</string> <string name="host_webui">Server WebUI</string>
<string name="host_webui_sub">Whether the server\'s default WebUI is enabled, makes you able to use Tachidesk in your browser</string> <string name="host_webui_sub">Whether the server\'s default WebUI is enabled, makes you able to use Suwayomi in your browser</string>
<string name="host_open_in_browser">Open Server WebUI on startup</string> <string name="host_open_in_browser">Open Server WebUI on startup</string>
<string name="host_open_in_browser_sub">Open the WebUI inside your browser on server startup. Requires the WebUI be enabled</string> <string name="host_open_in_browser_sub">Open the WebUI inside your browser on server startup. Requires the WebUI be enabled</string>
<string name="host_basic_auth_sub">Use basic auth to protect your library, requires username and password</string> <string name="host_basic_auth_sub">Use basic auth to protect your library, requires username and password</string>
@@ -327,6 +337,37 @@
<string name="digest_auth">Digest auth</string> <string name="digest_auth">Digest auth</string>
<string name="auth_username">Auth username</string> <string name="auth_username">Auth username</string>
<string name="auth_password">Auth password</string> <string name="auth_password">Auth password</string>
<string name="server_settings_sub">The below settings configure the connected Suwayomi-Server</string>
<string name="extension_repos">Extension repos</string>
<string name="extension_repos_sub">Configure your extension repos to allow extensions to be found</string>
<string name="global_update_interval">Global update interval</string>
<string name="global_update_interval_sub">Update interval in hours, 0 to disable, values under 6 hours will be ignored</string>
<string name="update_manga_info">Update manga info</string>
<string name="update_manga_info_sub">Update manga info alongside manga chapters in the library update</string>
<string name="exclude_completed">Exclude completed</string>
<string name="exclude_completed_sub">Exclude completed manga from library update</string>
<string name="exclude_unread">Exclude unread</string>
<string name="exclude_unread_sub">Exclude manga with unread chapters from library update</string>
<string name="exclude_not_started">Exclude not started</string>
<string name="exclude_not_started_sub">Exclude manga with no read chapters from library update</string>
<string name="max_sources_parallel">Max sources in parallel</string>
<string name="max_sources_parallel_sub">How many sources can be updated or downloaded in parallel</string>
<string name="graphql_debug_logs">Server GraphQL debug logs</string>
<string name="graphql_debug_logs_sub">Output GraphQL debug logs from the server to JUI</string>
<string name="download_new_chapters">Download new chapters</string>
<string name="download_chapter_limit">Chapter download limit</string>
<string name="download_chapter_limit_sub">Limit the amount of new chapters that are going to get downloaded</string>
<string name="ignore_unread_entries">Ignore entries with unread chapters</string>
<string name="backup_interval">Backup interval</string>
<string name="backup_interval_sub">Automatic backup interval in days, 0 to disable it</string>
<string name="backup_ttl">Backup TTL</string>
<string name="backup_ttl_sub">How long to keep a specific backup, in days, 0 to disable</string>
<string name="backup_time">Backup time</string>
<string name="backup_time_sub">When to run the automatic backup</string>
<string name="flaresolverr_url">FlareSolverr server url</string>
<string name="flaresolverr_timeout">FlareSolverr request timeout</string>
<string name="flaresolverr_session_name">FlareSolverr session name</string>
<string name="flaresolverr_session_ttl">FlareSolverr session TTL</string>
<!-- Advanced Settings --> <!-- Advanced Settings -->
<string name="update_checker">Check for updates</string> <string name="update_checker">Check for updates</string>

View File

@@ -60,6 +60,7 @@ kotlin {
implementation(libs.imageloader.core) implementation(libs.imageloader.core)
implementation(libs.imageloader.moko) implementation(libs.imageloader.moko)
implementation(libs.materialDialogs.core) implementation(libs.materialDialogs.core)
implementation(libs.materialDialogs.datetime)
// Threading // Threading
implementation(libs.coroutines.core) implementation(libs.coroutines.core)

View File

@@ -66,6 +66,7 @@ kotlin {
api(libs.voyager.transitions) api(libs.voyager.transitions)
api(libs.voyager.screenmodel) api(libs.voyager.screenmodel)
api(libs.materialDialogs.core) api(libs.materialDialogs.core)
api(libs.materialDialogs.datetime)
api(libs.accompanist.pager) api(libs.accompanist.pager)
api(libs.accompanist.pagerIndicators) api(libs.accompanist.pagerIndicators)
api(libs.accompanist.flowLayout) api(libs.accompanist.flowLayout)

View File

@@ -71,7 +71,6 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar
import ca.gosyer.jui.uicore.components.keyboardHandler import ca.gosyer.jui.uicore.components.keyboardHandler
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
import ca.gosyer.jui.uicore.components.scrollbarPadding import ca.gosyer.jui.uicore.components.scrollbarPadding
import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.jui.uicore.resources.stringResource import ca.gosyer.jui.uicore.resources.stringResource
import com.vanpra.composematerialdialogs.MaterialDialog import com.vanpra.composematerialdialogs.MaterialDialog
import com.vanpra.composematerialdialogs.MaterialDialogButtons import com.vanpra.composematerialdialogs.MaterialDialogButtons
@@ -85,6 +84,7 @@ import com.vanpra.composematerialdialogs.title
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun PreferenceRow( fun PreferenceRow(
@@ -151,7 +151,7 @@ fun PreferenceRow(
@Composable @Composable
fun SwitchPreference( fun SwitchPreference(
preference: PreferenceMutableStateFlow<Boolean>, preference: MutableStateFlow<Boolean>,
title: String, title: String,
subtitle: String? = null, subtitle: String? = null,
icon: ImageVector? = null, icon: ImageVector? = null,
@@ -176,7 +176,7 @@ fun SwitchPreference(
@Composable @Composable
fun EditTextPreference( fun EditTextPreference(
preference: PreferenceMutableStateFlow<String>, preference: MutableStateFlow<String>,
title: String, title: String,
subtitle: String? = null, subtitle: String? = null,
icon: ImageVector? = null, icon: ImageVector? = null,
@@ -221,7 +221,7 @@ fun EditTextPreference(
@Composable @Composable
fun <Key> ChoicePreference( fun <Key> ChoicePreference(
preference: PreferenceMutableStateFlow<Key>, preference: MutableStateFlow<Key>,
choices: ImmutableMap<Key, String>, choices: ImmutableMap<Key, String>,
title: String, title: String,
subtitle: String? = null, subtitle: String? = null,
@@ -344,7 +344,7 @@ fun <T> MultiSelectDialog(
@Composable @Composable
fun ColorPreference( fun ColorPreference(
preference: PreferenceMutableStateFlow<Color>, preference: MutableStateFlow<Color>,
title: String, title: String,
subtitle: String? = null, subtitle: String? = null,
enabled: Boolean = true, enabled: Boolean = true,

View File

@@ -121,7 +121,7 @@ class SettingsLibraryViewModel
@Composable @Composable
fun getDisplayModeChoices() = fun getDisplayModeChoices() =
DisplayMode.values() DisplayMode.entries
.associateWith { stringResource(it.res) } .associateWith { stringResource(it.res) }
.toImmutableMap() .toImmutableMap()
} }

View File

@@ -6,40 +6,72 @@
package ca.gosyer.jui.ui.settings package ca.gosyer.jui.ui.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize 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.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState 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.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons 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.material.icons.rounded.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation 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.Auth
import ca.gosyer.jui.domain.server.model.Proxy 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.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.i18n.MR
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
import ca.gosyer.jui.ui.base.navigation.Toolbar import ca.gosyer.jui.ui.base.navigation.Toolbar
import ca.gosyer.jui.ui.base.prefs.ChoicePreference import ca.gosyer.jui.ui.base.prefs.ChoicePreference
import ca.gosyer.jui.ui.base.prefs.EditTextPreference import ca.gosyer.jui.ui.base.prefs.EditTextPreference
import ca.gosyer.jui.ui.base.prefs.PreferenceRow 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.main.components.bottomNav
import ca.gosyer.jui.ui.viewModel import ca.gosyer.jui.ui.viewModel
import ca.gosyer.jui.uicore.components.VerticalScrollbar 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.rememberScrollbarAdapter
import ca.gosyer.jui.uicore.components.scrollbarPadding import ca.gosyer.jui.uicore.components.scrollbarPadding
import ca.gosyer.jui.uicore.prefs.PreferenceMutableStateFlow 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.Screen
import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey 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.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow 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 import me.tatarka.inject.annotations.Inject
class SettingsServerScreen : Screen { class SettingsServerScreen : Screen {
@@ -80,6 +126,8 @@ class SettingsServerScreen : Screen {
authChoices = connectionVM.getAuthChoices(), authChoices = connectionVM.getAuthChoices(),
authUsername = connectionVM.authUsername, authUsername = connectionVM.authUsername,
authPassword = connectionVM.authPassword, authPassword = connectionVM.authPassword,
serverSettings = connectionVM.serverSettings.collectAsState().value,
hosted = connectionVM.host.collectAsState().value,
) )
} }
} }
@@ -89,10 +137,238 @@ expect class SettingsServerHostViewModel : ViewModel
@Composable @Composable
expect fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit expect fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit
private class ServerSettingMutableStateFlow<T>(
parent: StateFlow<Settings>,
getSetting: (Settings) -> T,
private val setSetting: (T) -> Unit,
scope: CoroutineScope,
private val state: MutableStateFlow<T> = MutableStateFlow(getSetting(parent.value)),
) : MutableStateFlow<T> 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 <T> getServerFlow(
getSetting: (Settings) -> T,
getInput: (T) -> SetSettingsInput,
): MutableStateFlow<T> {
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 class SettingsServerViewModel
@Inject @Inject
constructor( constructor(
private val getSettings: GetSettings,
private val setSettings: SetSettings,
serverPreferences: ServerPreferences, serverPreferences: ServerPreferences,
serverHostPreferences: ServerHostPreferences,
contextWrapper: ContextWrapper, contextWrapper: ContextWrapper,
) : ViewModel(contextWrapper) { ) : ViewModel(contextWrapper) {
val serverUrl = serverPreferences.server().asStateIn(scope) val serverUrl = serverPreferences.server().asStateIn(scope)
@@ -101,6 +377,8 @@ class SettingsServerViewModel
val proxy = serverPreferences.proxy().asStateIn(scope) val proxy = serverPreferences.proxy().asStateIn(scope)
val host = serverHostPreferences.host().asStateIn(scope)
@Composable @Composable
fun getProxyChoices(): ImmutableMap<Proxy, String> = fun getProxyChoices(): ImmutableMap<Proxy, String> =
persistentMapOf( persistentMapOf(
@@ -127,11 +405,22 @@ class SettingsServerViewModel
val authUsername = serverPreferences.authUsername().asStateIn(scope) val authUsername = serverPreferences.authUsername().asStateIn(scope)
val authPassword = serverPreferences.authPassword().asStateIn(scope) val authPassword = serverPreferences.authPassword().asStateIn(scope)
private val _serverSettingChanged = MutableStateFlow(false) private val _serverSettings = MutableStateFlow<ServerSettings?>(null)
val serverSettingChanged = _serverSettingChanged.asStateFlow() val serverSettings = _serverSettings.asStateFlow()
fun serverSettingChanged() { init {
_serverSettingChanged.value = true 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<Auth, String>, authChoices: ImmutableMap<Auth, String>,
authUsername: PreferenceMutableStateFlow<String>, authUsername: PreferenceMutableStateFlow<String>,
authPassword: PreferenceMutableStateFlow<String>, authPassword: PreferenceMutableStateFlow<String>,
hosted: Boolean,
serverSettings: ServerSettings?,
) { ) {
Scaffold( Scaffold(
modifier = Modifier.windowInsetsPadding( 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( VerticalScrollbar(
rememberScrollbarAdapter(state), 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<String>,
onSetRepos: (List<String>) -> 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))
},
)
}
}

View File

@@ -82,7 +82,6 @@ fun SourceSettingsScreenContent(settings: ImmutableList<SourceSettingsView<*, *>
).asPaddingValues(), ).asPaddingValues(),
) { ) {
items(settings, { it.props.hashCode() }) { items(settings, { it.props.hashCode() }) {
@Suppress("UNCHECKED_CAST")
when (it) { when (it) {
is CheckBox, is Switch -> { is CheckBox, is Switch -> {
TwoStatePreference(it as TwoState, it is CheckBox) TwoStatePreference(it as TwoState, it is CheckBox)

View File

@@ -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.PreferenceRow
import ca.gosyer.jui.ui.base.prefs.SwitchPreference import ca.gosyer.jui.ui.base.prefs.SwitchPreference
import ca.gosyer.jui.ui.util.system.folderPicker 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.asStateIn
import ca.gosyer.jui.uicore.prefs.asStringStateIn import ca.gosyer.jui.uicore.prefs.asStringStateIn
import ca.gosyer.jui.uicore.resources.stringResource import ca.gosyer.jui.uicore.resources.stringResource
@@ -56,15 +55,9 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie
host = serverVm.host, host = serverVm.host,
ip = serverVm.ip, ip = serverVm.ip,
port = serverVm.port, port = serverVm.port,
socksProxyEnabled = serverVm.socksProxyEnabled,
socksProxyHost = serverVm.socksProxyHost,
socksProxyPort = serverVm.socksProxyPort,
debugLogsEnabled = serverVm.debugLogsEnabled,
systemTrayEnabled = serverVm.systemTrayEnabled,
downloadPath = serverVm.downloadPath, downloadPath = serverVm.downloadPath,
downloadAsCbz = serverVm.downloadAsCbz, backupPath = serverVm.backupPath,
webUIEnabled = serverVm.webUIEnabled, localSourcePath = serverVm.localSourcePath,
openInBrowserEnabled = serverVm.openInBrowserEnabled,
basicAuthEnabled = serverVm.basicAuthEnabled, basicAuthEnabled = serverVm.basicAuthEnabled,
basicAuthUsername = serverVm.basicAuthUsername, basicAuthUsername = serverVm.basicAuthUsername,
basicAuthPassword = serverVm.basicAuthPassword, basicAuthPassword = serverVm.basicAuthPassword,
@@ -84,22 +77,12 @@ actual class SettingsServerHostViewModel
val ip = serverHostPreferences.ip().asStateIn(scope) val ip = serverHostPreferences.ip().asStateIn(scope)
val port = serverHostPreferences.port().asStringStateIn(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 // Downloader
val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope)
val downloadAsCbz = serverHostPreferences.downloadAsCbz().asStateIn(scope) // Backup
val backupPath = serverHostPreferences.backupPath().asStateIn(scope)
// WebUI // LocalSource
val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) val localSourcePath = serverHostPreferences.localSourcePath().asStateIn(scope)
val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope)
// Authentication // Authentication
val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope)
@@ -145,21 +128,15 @@ fun LazyListScope.ServerHostItems(
hostValue: Boolean, hostValue: Boolean,
basicAuthEnabledValue: Boolean, basicAuthEnabledValue: Boolean,
serverSettingChanged: () -> Unit, serverSettingChanged: () -> Unit,
host: PreferenceMutableStateFlow<Boolean>, host: MutableStateFlow<Boolean>,
ip: PreferenceMutableStateFlow<String>, ip: MutableStateFlow<String>,
port: PreferenceMutableStateFlow<String>, port: MutableStateFlow<String>,
socksProxyEnabled: PreferenceMutableStateFlow<Boolean>, downloadPath: MutableStateFlow<String>,
socksProxyHost: PreferenceMutableStateFlow<String>, backupPath: MutableStateFlow<String>,
socksProxyPort: PreferenceMutableStateFlow<String>, localSourcePath: MutableStateFlow<String>,
debugLogsEnabled: PreferenceMutableStateFlow<Boolean>, basicAuthEnabled: MutableStateFlow<Boolean>,
systemTrayEnabled: PreferenceMutableStateFlow<Boolean>, basicAuthUsername: MutableStateFlow<String>,
downloadPath: PreferenceMutableStateFlow<String>, basicAuthPassword: MutableStateFlow<String>,
downloadAsCbz: PreferenceMutableStateFlow<Boolean>,
webUIEnabled: PreferenceMutableStateFlow<Boolean>,
openInBrowserEnabled: PreferenceMutableStateFlow<Boolean>,
basicAuthEnabled: PreferenceMutableStateFlow<Boolean>,
basicAuthUsername: PreferenceMutableStateFlow<String>,
basicAuthPassword: PreferenceMutableStateFlow<String>,
) { ) {
item { item {
SwitchPreference(preference = host, title = stringResource(MR.strings.host_server)) SwitchPreference(preference = host, title = stringResource(MR.strings.host_server))
@@ -190,47 +167,6 @@ fun LazyListScope.ServerHostItems(
changeListener = serverSettingChanged, 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 { item {
val downloadPathValue by downloadPath.collectAsState() val downloadPathValue by downloadPath.collectAsState()
PreferenceRow( PreferenceRow(
@@ -253,29 +189,45 @@ fun LazyListScope.ServerHostItems(
) )
} }
item { item {
SwitchPreference( val backupPathValue by backupPath.collectAsState()
preference = downloadAsCbz, PreferenceRow(
title = stringResource(MR.strings.host_download_as_cbz), title = stringResource(MR.strings.host_backup_path),
subtitle = stringResource(MR.strings.host_download_as_cbz_sub), subtitle = if (backupPathValue.isEmpty()) {
changeListener = serverSettingChanged, 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 { item {
SwitchPreference( val localSourcePathValue by localSourcePath.collectAsState()
preference = webUIEnabled, PreferenceRow(
title = stringResource(MR.strings.host_webui), title = stringResource(MR.strings.host_local_source_path),
subtitle = stringResource(MR.strings.host_webui_sub), subtitle = if (localSourcePathValue.isEmpty()) {
changeListener = serverSettingChanged, 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()
} }
item { },
val webUIEnabledValue by webUIEnabled.collectAsState() onLongClick = {
SwitchPreference( localSourcePath.value = ""
preference = openInBrowserEnabled, serverSettingChanged()
title = stringResource(MR.strings.host_open_in_browser), },
subtitle = stringResource(MR.strings.host_open_in_browser_sub),
changeListener = serverSettingChanged,
enabled = webUIEnabledValue,
) )
} }
item { item {