diff --git a/.gitignore b/.gitignore index 99232f52..be09476a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,6 @@ bin/ *.ipr *.iws /.idea/* -!/.idea/runConfigurations +!/.idea/runConfigurations/ out/ workspace.xml \ No newline at end of file diff --git a/.run/TachideskJUI [run].run.xml b/.run/TachideskJUI [run].run.xml new file mode 100644 index 00000000..1adcd27e --- /dev/null +++ b/.run/TachideskJUI [run].run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 1603e241..e0aab8d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,16 +3,17 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.4.31" - kotlin("plugin.serialization") version "1.4.31" - id("org.jetbrains.compose") version "0.4.0-build177" + kotlin("jvm") version "1.4.32" + kotlin("kapt") version "1.4.32" + kotlin("plugin.serialization") version "1.4.32" + id("org.jetbrains.compose") version "0.4.0-build184" + id("de.fuerstenau.buildconfig") version "1.1.8" } group = "ca.gosyer" version = "1.0.0" repositories { - jcenter() mavenCentral() maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } @@ -28,7 +29,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") // Dependency Injection - implementation("io.insert-koin:koin-core-ext:3.0.1-beta-1") + implementation("com.github.stephanenicolas.toothpick:ktp:3.1.0") + kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:3.1.0") // Http client val ktorVersion = "1.5.2" @@ -38,8 +40,10 @@ dependencies { implementation("io.ktor:ktor-client-logging:$ktorVersion") // Logging - implementation("ch.qos.logback:logback-classic:1.2.3") - //implementation("org.fusesource.jansi:jansi:1.18") + val log4jVersion = "2.14.1" + implementation("org.apache.logging.log4j:log4j-api:$log4jVersion") + implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") + implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion") implementation("io.github.microutils:kotlin-logging:2.0.5") // User storage @@ -105,4 +109,14 @@ compose.desktop { copyright = "Mozilla Public License v2.0" } } +} + +buildConfig { + appName = project.name + version = project.version.toString() + + clsName = "BuildConfig" + packageName = project.group.toString() + + buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString()) } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/Preference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/Preference.kt deleted file mode 100644 index 530c1e61..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/Preference.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.backend.preferences - -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors -import ca.gosyer.backend.preferences.impl.getBooleanPreference -import ca.gosyer.backend.preferences.impl.getJsonPreference -import ca.gosyer.backend.preferences.impl.getLongPreference -import ca.gosyer.backend.preferences.impl.getStringPreference -import ca.gosyer.ui.library.DisplayMode -import ca.gosyer.util.compose.color -import com.russhwolf.settings.JvmPreferencesSettings -import com.russhwolf.settings.ObservableSettings -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer -import org.koin.dsl.module -import java.util.prefs.Preferences - -class PreferenceHelper { - private val settings = JvmPreferencesSettings(Preferences.userRoot()) as ObservableSettings - val serverUrl = settings.getStringPreference("server_url", "http://localhost:4567") - val enabledLangs = settings.getJsonPreference("server_langs", listOf("all", "en"), ListSerializer(String.serializer())) - val libraryDisplay = settings.getJsonPreference("library_display", DisplayMode.CompactGrid, DisplayMode.serializer()) - - val lightTheme = settings.getBooleanPreference("light_theme", true) - val lightPrimary = settings.getLongPreference("light_color_primary", 0xFF00a2ff) - val lightPrimaryVariant = settings.getLongPreference("light_color_primary_variant", 0xFF0091EA) - val lightSecondary = settings.getLongPreference("light_color_secondary", 0xFFF44336) - val lightSecondaryVaraint = settings.getLongPreference("light_color_secondary_variant", 0xFFE53935) - - fun getTheme() = when (lightTheme.get()) { - true -> lightColors( - lightPrimary.get().color, - lightPrimaryVariant.get().color, - lightSecondary.get().color, - lightSecondaryVaraint.get().color - ) - false -> darkColors() - } -} - -val preferencesModule = module { - single { PreferenceHelper() } -} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/BooleanPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/BooleanPreference.kt deleted file mode 100644 index db30d014..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/BooleanPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getBooleanFlow -import com.russhwolf.settings.coroutines.getBooleanOrNullFlow -import com.russhwolf.settings.set - -class BooleanPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: Boolean -): DefaultPreference { - override fun get() = settings.getBoolean(key, default) - override fun asFLow() = settings.getBooleanFlow(key, default) - override fun set(value: Boolean) { - settings[key] = value - } -} - -class BooleanNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getBooleanOrNull(key) - override fun asFLow() = settings.getBooleanOrNullFlow(key) - override fun set(value: Boolean?) { - settings[key] = value - } -} - -fun ObservableSettings.getBooleanPreference(key: String, default: Boolean) = BooleanPreference( - this, - key, - default -) - -fun ObservableSettings.getBooleanPreference(key: String) = BooleanNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/DoublePreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/DoublePreference.kt deleted file mode 100644 index eb8137e7..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/DoublePreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getDoubleFlow -import com.russhwolf.settings.coroutines.getDoubleOrNullFlow -import com.russhwolf.settings.set - -class DoublePreference( - override val settings: ObservableSettings, - override val key: String, - override val default: Double -): DefaultPreference { - override fun get() = settings.getDouble(key, default) - override fun asFLow() = settings.getDoubleFlow(key, default) - override fun set(value: Double) { - settings[key] = value - } -} - -class DoubleNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getDoubleOrNull(key) - override fun asFLow() = settings.getDoubleOrNullFlow(key) - override fun set(value: Double?) { - settings[key] = value - } -} - -fun ObservableSettings.getDoublePreference(key: String, default: Double) = DoublePreference( - this, - key, - default -) - -fun ObservableSettings.getDoublePreference(key: String) = DoubleNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/FloatPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/FloatPreference.kt deleted file mode 100644 index 817ed8a5..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/FloatPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getFloatFlow -import com.russhwolf.settings.coroutines.getFloatOrNullFlow -import com.russhwolf.settings.set - -class FloatPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: Float -): DefaultPreference { - override fun get() = settings.getFloat(key, default) - override fun asFLow() = settings.getFloatFlow(key, default) - override fun set(value: Float) { - settings[key] = value - } -} - -class FloatNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getFloatOrNull(key) - override fun asFLow() = settings.getFloatOrNullFlow(key) - override fun set(value: Float?) { - settings[key] = value - } -} - -fun ObservableSettings.getFloatPreference(key: String, default: Float) = FloatPreference( - this, - key, - default -) - -fun ObservableSettings.getFloatPreference(key: String) = FloatNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/IntPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/IntPreference.kt deleted file mode 100644 index af7bcd1f..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/IntPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getIntFlow -import com.russhwolf.settings.coroutines.getIntOrNullFlow -import com.russhwolf.settings.set - -class IntPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: Int -): DefaultPreference { - override fun get() = settings.getInt(key, default) - override fun asFLow() = settings.getIntFlow(key, default) - override fun set(value: Int) { - settings[key] = value - } -} - -class IntNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getIntOrNull(key) - override fun asFLow() = settings.getIntOrNullFlow(key) - override fun set(value: Int?) { - settings[key] = value - } -} - -fun ObservableSettings.getIntPreference(key: String, default: Int) = IntPreference( - this, - key, - default -) - -fun ObservableSettings.getIntPreference(key: String) = IntNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/JsonPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/JsonPreference.kt deleted file mode 100644 index 67a25b70..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/JsonPreference.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.Settings -import com.russhwolf.settings.get -import com.russhwolf.settings.serialization.decodeValue -import com.russhwolf.settings.serialization.decodeValueOrNull -import com.russhwolf.settings.serialization.encodeValue -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.serialization.KSerializer - -class JsonPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: T, - private val serializer: KSerializer -): DefaultPreference { - override fun get() = settings.decodeValue(serializer, key, default) - override fun asFLow() = settings.createFlow(key, default) { key, default -> - decodeValue(serializer, key, default) - } - override fun set(value: T) { - settings.encodeValue(serializer, key, value) - } - - fun getJson(): String? { - return settings[key] - } -} - -class JsonNullPreference( - override val settings: ObservableSettings, - override val key: String, - private val serializer: KSerializer -): NullPreference { - override fun get() = settings.decodeValueOrNull(serializer, key) - override fun asFLow() = settings.createFlow(key, null) { key, _ -> - decodeValueOrNull(serializer, key) - } - override fun set(value: T?) { - if (value != null) { - settings.encodeValue(serializer, key, value) - } else { - settings.remove(key) - } - } - - fun getJson(): String? { - return settings[key] - } -} - -fun ObservableSettings.getJsonPreference(key: String, default: T, serializer: KSerializer) = JsonPreference( - this, - key, - default, - serializer -) - -fun ObservableSettings.getJsonPreference(key: String, serializer: KSerializer) = JsonNullPreference( - this, - key, - serializer -) - -private inline fun ObservableSettings.createFlow( - key: String, - defaultValue: T, - crossinline getter: Settings.(String, T) -> T -): Flow = callbackFlow { - offer(getter(key, defaultValue)) - val listener = addListener(key) { - offer(getter(key, defaultValue)) - } - awaitClose { - listener.deactivate() - } -} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/LongPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/LongPreference.kt deleted file mode 100644 index 37c7f129..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/LongPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getLongFlow -import com.russhwolf.settings.coroutines.getLongOrNullFlow -import com.russhwolf.settings.set - -class LongPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: Long -): DefaultPreference { - override fun get() = settings.getLong(key, default) - override fun asFLow() = settings.getLongFlow(key, default) - override fun set(value: Long) { - settings[key] = value - } -} - -class LongNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getLongOrNull(key) - override fun asFLow() = settings.getLongOrNullFlow(key) - override fun set(value: Long?) { - settings[key] = value - } -} - -fun ObservableSettings.getLongPreference(key: String, default: Long) = LongPreference( - this, - key, - default -) - -fun ObservableSettings.getLongPreference(key: String) = LongNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/Preference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/Preference.kt deleted file mode 100644 index 36cfd12e..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/Preference.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.backend.preferences.impl - -import ca.gosyer.util.system.asStateFlow -import com.russhwolf.settings.ObservableSettings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -interface Preference { - /** - * The settings to watch for this preference. Must be a instance of [ObservableSettings] - * so that we can watch changes - */ - val settings: ObservableSettings - - /** - * The key for this preference - */ - val key: String -} - - -interface NullPreference : Preference { - /** - * Returns the value stored at [key] as [T], or `null` if no value was stored. If a value of a different - * type was stored at `key`, the behavior is not defined. - */ - fun get(): T? - - /** - * Create a new [Flow], based on observing the given [key]. This flow will immediately emit the - * current value and then emit any subsequent values when the underlying `Settings` changes. When no value is present, - * `null` will be emitted instead. - */ - fun asFLow(): Flow - - /** - * See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting - */ - fun asStateFlow(scope: CoroutineScope): StateFlow = asFLow().asStateFlow(get(), scope, true) - - /** - * Stores a [T] value at [key], or remove what's there if [value] is null. - */ - fun set(value: T?) -} - -interface DefaultPreference : Preference { - val default: T - - /** - * Returns the value stored at [key] as [T], or [default] if no value was stored. If a value of a different - * type was stored at `key`, the behavior is not defined. - */ - fun get(): T - - /** - * Create a new [Flow], based on observing the given [key]. This flow will immediately emit the - * current value and then emit any subsequent values when the underlying `Settings` changes. When no value is present, - * [default] will be emitted instead. - */ - fun asFLow(): Flow - - /** - * See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting - */ - fun asStateFlow(scope: CoroutineScope): StateFlow = asFLow().asStateFlow(get(), scope, true) - /** - * Stores the [T] [value] at [key]. - */ - fun set(value: T) -} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/preferences/impl/StringPreference.kt b/src/main/kotlin/ca/gosyer/backend/preferences/impl/StringPreference.kt deleted file mode 100644 index 49e26364..00000000 --- a/src/main/kotlin/ca/gosyer/backend/preferences/impl/StringPreference.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.backend.preferences.impl - -import com.russhwolf.settings.ObservableSettings -import com.russhwolf.settings.coroutines.getStringFlow -import com.russhwolf.settings.coroutines.getStringOrNullFlow -import com.russhwolf.settings.set - -class StringPreference( - override val settings: ObservableSettings, - override val key: String, - override val default: String -): DefaultPreference { - override fun get() = settings.getString(key, default) - override fun asFLow() = settings.getStringFlow(key, default) - override fun set(value: String) { - settings[key] = value - } -} - -class StringNullPreference( - override val settings: ObservableSettings, - override val key: String, -): NullPreference { - override fun get() = settings.getStringOrNull(key) - override fun asFLow() = settings.getStringOrNullFlow(key) - override fun set(value: String?) { - settings[key] = value - } -} - -fun ObservableSettings.getStringPreference(key: String, default: String) = StringPreference( - this, - key, - default -) - -fun ObservableSettings.getStringPreference(key: String) = StringNullPreference( - this, - key -) \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/di/AppScope.kt b/src/main/kotlin/ca/gosyer/common/di/AppScope.kt new file mode 100644 index 00000000..cf36804e --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/di/AppScope.kt @@ -0,0 +1,31 @@ +/* + * 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.common.di + +import toothpick.Scope +import toothpick.ktp.KTP + +/** + * The global scope for dependency injection that will provide all the application level components. + */ +object AppScope : Scope by KTP.openRootScope() { + + /** + * Returns a new subscope inheriting the root scope. + */ + fun subscope(any: Any): Scope { + return openSubScope(any) + } + + /** + * Returns an instance of [T] from the root scope. + */ + inline fun getInstance(): T { + return getInstance(T::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt b/src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt new file mode 100644 index 00000000..29399d41 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt @@ -0,0 +1,17 @@ +/* + * 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.common.di + +import toothpick.Scope +import javax.inject.Provider + +class GenericsProvider(private val cls: Class, val scope: Scope = AppScope) : Provider { + + override fun get(): T { + return scope.getInstance(cls) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt b/src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt new file mode 100644 index 00000000..b96685c5 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.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.common.di + +import toothpick.config.Module + +/** + * Binds the given [instance] to its class. + */ +inline fun Module.bindInstance(instance: B) { + bind(B::class.java).toInstance(instance) +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt b/src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt new file mode 100644 index 00000000..23694c63 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt @@ -0,0 +1,54 @@ +/* + * 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.common.io + +import ca.gosyer.common.util.decodeBase64 +import okio.Buffer +import okio.Source +import okio.Timeout + +class DataUriStringSource(private val data: String) : Source { + + private val timeout = Timeout() + + private val headers = data.substringBefore(",") + + private var pos = headers.length + 1 + + private val decoder: (Buffer, String) -> Long = if ("base64" in headers) { + { sink, bytes -> + val decoded = bytes.decodeBase64() + sink.write(decoded) + decoded.size.toLong() + } + } else { + { sink, bytes -> + val decoded = bytes.toByteArray() + sink.write(decoded) + decoded.size.toLong() + } + } + + override fun read(sink: Buffer, byteCount: Long): Long { + if (pos >= data.length) return -1 + + val charsToRead = minOf(data.length - pos, byteCount.toInt()) + val nextChars = data.substring(pos, pos + charsToRead) + + pos += charsToRead + + return decoder(sink, nextChars) + } + + override fun timeout(): Timeout { + return timeout + } + + override fun close() { + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt b/src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt new file mode 100644 index 00000000..81c469ea --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt @@ -0,0 +1,32 @@ +/* + * 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.common.io + + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.BufferedSink +import okio.Source +import okio.buffer +import okio.sink +import java.io.File + +suspend fun Source.saveTo(file: File) { + withContext(Dispatchers.IO) { + use { source -> + file.sink().buffer().use { it.writeAll(source) } + } + } +} + +suspend fun Source.copyTo(sink: BufferedSink) { + withContext(Dispatchers.IO) { + use { source -> + sink.use { it.writeAll(source) } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/prefs/LazyPreferenceStore.kt b/src/main/kotlin/ca/gosyer/common/prefs/LazyPreferenceStore.kt new file mode 100644 index 00000000..7f8ac5ed --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/prefs/LazyPreferenceStore.kt @@ -0,0 +1,83 @@ +/* + * 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.common.prefs + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.modules.SerializersModule + +/** + * An implementation of a [PreferenceStore] which is initialized on first access. Useful when + * providing preference instances to classes that may not use them at all. + */ +class LazyPreferenceStore( + private val lazyStore: Lazy +) : PreferenceStore { + + /** + * Returns an [String] preference for this [key]. + */ + override fun getString(key: String, defaultValue: String): Preference { + return lazyStore.value.getString(key, defaultValue) + } + + /** + * Returns a [Long] preference for this [key]. + */ + override fun getLong(key: String, defaultValue: Long): Preference { + return lazyStore.value.getLong(key, defaultValue) + } + + /** + * Returns an [Int] preference for this [key]. + */ + override fun getInt(key: String, defaultValue: Int): Preference { + return lazyStore.value.getInt(key, defaultValue) + } + + /** + * Returns a [Float] preference for this [key]. + */ + override fun getFloat(key: String, defaultValue: Float): Preference { + return lazyStore.value.getFloat(key, defaultValue) + } + + /** + * Returns a [Boolean] preference for this [key]. + */ + override fun getBoolean(key: String, defaultValue: Boolean): Preference { + return lazyStore.value.getBoolean(key, defaultValue) + } + + /** + * Returns a [Set] preference for this [key]. + */ + override fun getStringSet(key: String, defaultValue: Set): Preference> { + return lazyStore.value.getStringSet(key, defaultValue) + } + + /** + * Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function + * must be provided. + */ + override fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T + ): Preference { + return lazyStore.value.getObject(key, defaultValue, serializer, deserializer) + } + + override fun getJsonObject( + key: String, + defaultValue: T, + serializer: KSerializer, + serializersModule: SerializersModule + ): Preference { + return lazyStore.value.getJsonObject(key, defaultValue, serializer) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/prefs/Preference.kt b/src/main/kotlin/ca/gosyer/common/prefs/Preference.kt new file mode 100644 index 00000000..25c489f4 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/prefs/Preference.kt @@ -0,0 +1,61 @@ +/* + * 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.common.prefs + + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** + * A wrapper around application preferences without knowing implementation details. Instances of + * this interface must be provided through a [PreferenceStore]. + */ +interface Preference { + + /** + * Returns the key of this preference. + */ + fun key(): String + + /** + * Returns the current value of this preference. + */ + fun get(): T + + /** + * Sets a new [value] for this preference. + */ + fun set(value: T) + + /** + * Returns whether there's an existing entry for this preference. + */ + fun isSet(): Boolean + + /** + * Deletes the entry of this preference. + */ + fun delete() + + /** + * Returns the default value of this preference. + */ + fun defaultValue(): T + + /** + * Returns a cold [Flow] of this preference to receive updates when its value changes. + */ + fun changes(): Flow + + /** + * Returns a hot [StateFlow] of this preference bound to the given [scope], allowing to read the + * current value and receive preference updates. + */ + fun stateIn(scope: CoroutineScope): StateFlow + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt b/src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt new file mode 100644 index 00000000..9e972ebb --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt @@ -0,0 +1,87 @@ +/* + * 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.common.prefs + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +/** + * A wrapper around an application preferences store. Implementations of this interface should + * persist these preferences on disk. + */ +interface PreferenceStore { + + /** + * Returns an [String] preference for this [key]. + */ + fun getString(key: String, defaultValue: String = ""): Preference + + /** + * Returns a [Long] preference for this [key]. + */ + fun getLong(key: String, defaultValue: Long = 0): Preference + + /** + * Returns an [Int] preference for this [key]. + */ + fun getInt(key: String, defaultValue: Int = 0): Preference + + /** + * Returns a [Float] preference for this [key]. + */ + fun getFloat(key: String, defaultValue: Float = 0f): Preference + + /** + * Returns a [Boolean] preference for this [key]. + */ + fun getBoolean(key: String, defaultValue: Boolean = false): Preference + + /** + * Returns a [Set] preference for this [key]. + */ + fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> + + /** + * Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function + * must be provided. + */ + fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T + ): Preference + + + /** + * Returns preference of type [T] for this [key]. The [serializer] must be provided. + */ + fun getJsonObject( + key: String, + defaultValue: T, + serializer: KSerializer, + serializersModule: SerializersModule = EmptySerializersModule + ): Preference + +} + +/** + * Returns an enum preference of type [T] for this [key]. + */ +inline fun > PreferenceStore.getEnum( + key: String, + defaultValue: T +): Preference { + return getObject(key, defaultValue, { it.name }, { + try { + enumValueOf(it) + } catch (e: IllegalArgumentException) { + defaultValue + } + }) +} diff --git a/src/main/kotlin/ca/gosyer/util/system/Koin.kt b/src/main/kotlin/ca/gosyer/common/util/Codec.kt similarity index 50% rename from src/main/kotlin/ca/gosyer/util/system/Koin.kt rename to src/main/kotlin/ca/gosyer/common/util/Codec.kt index 4d32ca5f..40a1b5d1 100644 --- a/src/main/kotlin/ca/gosyer/util/system/Koin.kt +++ b/src/main/kotlin/ca/gosyer/common/util/Codec.kt @@ -4,10 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.util.system +package ca.gosyer.common.util -import org.koin.core.context.GlobalContext +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.encode -inline fun get() = GlobalContext.get().get() +fun String.decodeBase64() = decodeBase64()!! -inline fun inject() = GlobalContext.get().inject() \ No newline at end of file +fun String.md5() = encode().md5().hex() \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/util/CollectionExtensions.kt b/src/main/kotlin/ca/gosyer/common/util/CollectionExtensions.kt new file mode 100644 index 00000000..bcf3b524 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/util/CollectionExtensions.kt @@ -0,0 +1,44 @@ +/* + * 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.common.util + +/** + * Returns a new list that replaces the item at the given [position] with [newItem]. + */ +fun List.replace(position: Int, newItem: T): List { + val newList = toMutableList() + newList[position] = newItem + return newList +} + +/** + * Returns a new list that replaces the first occurrence that matches the given [predicate] with + * [newItem]. If no item matches the predicate, the same list is returned (and unmodified). + */ +inline fun List.replaceFirst(predicate: (T) -> Boolean, newItem: T): List { + forEachIndexed { index, element -> + if (predicate(element)) { + return replace(index, newItem) + } + } + return this +} + +/** + * Removes the first item of this collection that matches the given [predicate]. + */ +inline fun MutableCollection.removeFirst(predicate: (T) -> Boolean): T? { + val iter = iterator() + while (iter.hasNext()) { + val element = iter.next() + if (predicate(element)) { + iter.remove() + return element + } + } + return null +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt b/src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt new file mode 100644 index 00000000..e08a95f0 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt @@ -0,0 +1,43 @@ +/* + * 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.common.util + +object ImageUtil { + + private val jpgMagic = charByteArrayOf(0xFF, 0xD8, 0xFF) + private val pngMagic = charByteArrayOf(0x89, 0x50, 0x4E, 0x47) + private val gifMagic = "GIF8".toByteArray() + private val webpMagic = "RIFF".toByteArray() + + fun findType(bytes: ByteArray): ImageType? { + return when { + bytes.compareWith(jpgMagic) -> ImageType.JPG + bytes.compareWith(pngMagic) -> ImageType.PNG + bytes.compareWith(gifMagic) -> ImageType.GIF + bytes.compareWith(webpMagic) -> ImageType.WEBP + else -> null + } + } + + private fun ByteArray.compareWith(magic: ByteArray): Boolean { + for (i in magic.indices) { + if (this[i] != magic[i]) return false + } + return true + } + + private fun charByteArrayOf(vararg bytes: Int): ByteArray { + return ByteArray(bytes.size) { pos -> bytes[pos].toByte() } + } + + enum class ImageType(val mime: String, val extension: String) { + JPG("image/jpeg", "jpg"), + PNG("image/png", "png"), + GIF("image/gif", "gif"), + WEBP("image/webp", "webp") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt new file mode 100644 index 00000000..a1634547 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt @@ -0,0 +1,99 @@ +/* + * 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.core.prefs + +import ca.gosyer.common.prefs.Preference +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.contains +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.stateIn + +internal class JvmPreference( + private val preferences: ObservableSettings, + private val key: String, + private val defaultValue: T, + private val adapter: Adapter +) : Preference { + + interface Adapter { + fun get(key: String, preferences: ObservableSettings): T + + fun set(key: String, value: T, editor: ObservableSettings) + } + + /** + * Returns the key of this preference. + */ + override fun key(): String { + return key + } + + /** + * Returns the current value of this preference. + */ + override fun get(): T { + return if (!preferences.contains(key)) { + defaultValue + } else { + adapter.get(key, preferences) + } + } + + /** + * Sets a new [value] for this preference. + */ + override fun set(value: T) { + adapter.set(key, value, preferences) + } + + /** + * Returns whether there's an existing entry for this preference. + */ + override fun isSet(): Boolean { + return preferences.contains(key) + } + + /** + * Deletes the entry of this preference. + */ + override fun delete() { + preferences.remove(key) + } + + /** + * Returns the default value of this preference + */ + override fun defaultValue(): T { + return defaultValue + } + + /** + * Returns a cold [Flow] of this preference to receive updates when its value changes. + */ + override fun changes(): Flow { + return callbackFlow { + val listener = preferences.addListener(key) { + offer(get()) + } + awaitClose { listener.deactivate() } + } + } + + /** + * Returns a hot [StateFlow] of this preference bound to the given [scope], allowing to read the + * current value and receive preference updates. + */ + override fun stateIn(scope: CoroutineScope): StateFlow { + return changes().stateIn(scope, SharingStarted.Eagerly, get()) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt new file mode 100644 index 00000000..9d6a0180 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt @@ -0,0 +1,107 @@ +/* + * 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.core.prefs + +import com.russhwolf.settings.ObservableSettings +import com.russhwolf.settings.serialization.decodeValue +import com.russhwolf.settings.serialization.encodeValue +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +internal object StringAdapter : JvmPreference.Adapter { + override fun get(key: String, preferences: ObservableSettings): String { + return preferences.getString(key) // Not called unless key is present. + } + + override fun set(key: String, value: String, editor: ObservableSettings) { + editor.putString(key, value) + } +} + +internal object LongAdapter : JvmPreference.Adapter { + override fun get(key: String, preferences: ObservableSettings): Long { + return preferences.getLong(key, 0) + } + + override fun set(key: String, value: Long, editor: ObservableSettings) { + editor.putLong(key, value) + } +} + +internal object IntAdapter : JvmPreference.Adapter { + override fun get(key: String, preferences: ObservableSettings): Int { + return preferences.getInt(key, 0) + } + + override fun set(key: String, value: Int, editor: ObservableSettings) { + editor.putInt(key, value) + } +} + +internal object FloatAdapter : JvmPreference.Adapter { + override fun get(key: String, preferences: ObservableSettings): Float { + return preferences.getFloat(key, 0f) + } + + override fun set(key: String, value: Float, editor: ObservableSettings) { + editor.putFloat(key, value) + } +} + +internal object BooleanAdapter : JvmPreference.Adapter { + override fun get(key: String, preferences: ObservableSettings): Boolean { + return preferences.getBoolean(key, false) + } + + override fun set(key: String, value: Boolean, editor: ObservableSettings) { + editor.putBoolean(key, value) + } +} + +internal object StringSetAdapter : JvmPreference.Adapter> { + override fun get(key: String, preferences: ObservableSettings): Set { + return preferences.decodeValue(SetSerializer(String.serializer()), key, emptySet()) // Not called unless key is present. + } + + override fun set(key: String, value: Set, editor: ObservableSettings) { + editor.encodeValue(SetSerializer(String.serializer()), key, value) + } +} + +internal class ObjectAdapter( + private val serializer: (T) -> String, + private val deserializer: (String) -> T +) : JvmPreference.Adapter { + + override fun get(key: String, preferences: ObservableSettings): T { + return deserializer(preferences.getString(key)) // Not called unless key is present. + } + + override fun set(key: String, value: T, editor: ObservableSettings) { + editor.putString(key, serializer(value)) + } + +} + +internal class JsonObjectAdapter( + private val defaultValue: T, + private val serializer: KSerializer, + private val serializersModule: SerializersModule = EmptySerializersModule +) : JvmPreference.Adapter { + + override fun get(key: String, preferences: ObservableSettings): T { + return preferences.decodeValue(serializer, key, defaultValue, serializersModule) // Not called unless key is present. + } + + override fun set(key: String, value: T, editor: ObservableSettings) { + editor.encodeValue(serializer, key, value, serializersModule) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt new file mode 100644 index 00000000..6561f422 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt @@ -0,0 +1,86 @@ +/* + * 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.core.prefs + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore +import com.russhwolf.settings.ObservableSettings +import kotlinx.serialization.KSerializer +import kotlinx.serialization.modules.SerializersModule + +class JvmPreferenceStore(private val preferences: ObservableSettings) : PreferenceStore { + + /** + * Returns an [String] preference for this [key]. + */ + override fun getString(key: String, defaultValue: String): Preference { + return JvmPreference(preferences, key, defaultValue, StringAdapter) + } + + /** + * Returns a [Long] preference for this [key]. + */ + override fun getLong(key: String, defaultValue: Long): Preference { + return JvmPreference(preferences, key, defaultValue, LongAdapter) + } + + /** + * Returns an [Int] preference for this [key]. + */ + override fun getInt(key: String, defaultValue: Int): Preference { + return JvmPreference(preferences, key, defaultValue, IntAdapter) + } + + /** + * Returns a [Float] preference for this [key]. + */ + override fun getFloat(key: String, defaultValue: Float): Preference { + return JvmPreference(preferences, key, defaultValue, FloatAdapter) + } + + /** + * Returns a [Boolean] preference for this [key]. + */ + override fun getBoolean(key: String, defaultValue: Boolean): Preference { + return JvmPreference(preferences, key, defaultValue, BooleanAdapter) + } + + /** + * Returns a [Set] preference for this [key]. + */ + override fun getStringSet(key: String, defaultValue: Set): Preference> { + return JvmPreference(preferences, key, defaultValue, StringSetAdapter) + } + + /** + * Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function + * must be provided. + */ + override fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T + ): Preference { + val adapter = ObjectAdapter(serializer, deserializer) + return JvmPreference(preferences, key, defaultValue, adapter) + } + + /** + * Returns preference of type [T] for this [key]. The [serializer] must be provided. + */ + override fun getJsonObject( + key: String, + defaultValue: T, + serializer: KSerializer, + serializersModule: SerializersModule + ): Preference { + val adapter = JsonObjectAdapter(defaultValue, serializer, serializersModule) + return JvmPreference(preferences, key, defaultValue, adapter) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/core/prefs/PreferenceStoreProvider.kt b/src/main/kotlin/ca/gosyer/core/prefs/PreferenceStoreProvider.kt new file mode 100644 index 00000000..7eea4447 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/core/prefs/PreferenceStoreProvider.kt @@ -0,0 +1,24 @@ +/* + * 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.core.prefs + +import ca.gosyer.common.prefs.PreferenceStore +import com.russhwolf.settings.JvmPreferencesSettings +import java.util.prefs.Preferences + +class PreferenceStoreFactory { + + fun create(name: String? = null): PreferenceStore { + val jvmPreferences = if (!name.isNullOrBlank()) { + JvmPreferencesSettings(Preferences.userRoot().node(name)) + } else { + JvmPreferencesSettings(Preferences.userRoot()) + } + return JvmPreferenceStore(jvmPreferences) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/DataModule.kt b/src/main/kotlin/ca/gosyer/data/DataModule.kt new file mode 100644 index 00000000..8a432f8e --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/DataModule.kt @@ -0,0 +1,67 @@ +/* + * 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.data + +import ca.gosyer.core.prefs.PreferenceStoreFactory +import ca.gosyer.data.catalog.CatalogPreferences +import ca.gosyer.data.extension.ExtensionPreferences +import ca.gosyer.data.library.LibraryPreferences +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.HttpProvider +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.CategoryInteractionHandler +import ca.gosyer.data.server.interactions.ChapterInteractionHandler +import ca.gosyer.data.server.interactions.ExtensionInteractionHandler +import ca.gosyer.data.server.interactions.LibraryInteractionHandler +import ca.gosyer.data.server.interactions.MangaInteractionHandler +import ca.gosyer.data.server.interactions.SourceInteractionHandler +import ca.gosyer.data.ui.UiPreferences +import io.ktor.client.HttpClient +import toothpick.ktp.binding.bind +import toothpick.ktp.binding.module + +@Suppress("FunctionName") +val DataModule = module { + val preferenceFactory = PreferenceStoreFactory() + + bind() + .toProviderInstance { ServerPreferences(preferenceFactory.create("server")) } + .providesSingleton() + + bind() + .toProviderInstance { ExtensionPreferences(preferenceFactory.create("extension")) } + .providesSingleton() + + bind() + .toProviderInstance { CatalogPreferences(preferenceFactory.create("catalog")) } + .providesSingleton() + + bind() + .toProviderInstance { LibraryPreferences(preferenceFactory.create("library")) } + .providesSingleton() + + bind() + .toProviderInstance { UiPreferences(preferenceFactory.create("ui")) } + .providesSingleton() + + bind() + .toProvider(HttpProvider::class) + .providesSingleton() + + bind() + .toClass() + bind() + .toClass() + bind() + .toClass() + bind() + .toClass() + bind() + .toClass() + bind() + .toClass() +} diff --git a/src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt b/src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt new file mode 100644 index 00000000..876c5149 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.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.data.catalog + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore + +class CatalogPreferences(private val preferenceStore: PreferenceStore) { + fun languages(): Preference> { + return preferenceStore.getStringSet("enabled_langs", setOf("en")) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/extension/ExtensionPreferences.kt b/src/main/kotlin/ca/gosyer/data/extension/ExtensionPreferences.kt new file mode 100644 index 00000000..ddeae81d --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/extension/ExtensionPreferences.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.data.extension + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore + +class ExtensionPreferences(private val preferenceStore: PreferenceStore) { + fun languages(): Preference> { + return preferenceStore.getStringSet("enabled_langs", setOf("all", "en")) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt b/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt new file mode 100644 index 00000000..5a08110e --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt @@ -0,0 +1,18 @@ +/* + * 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.data.library + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore +import ca.gosyer.data.library.model.DisplayMode + +class LibraryPreferences(private val preferenceStore: PreferenceStore) { + + fun displayMode(): Preference { + return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt b/src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt new file mode 100644 index 00000000..ee8c1d85 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt @@ -0,0 +1,20 @@ +/* + * 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.data.library.model + +import kotlinx.serialization.Serializable + +@Serializable +enum class DisplayMode { + CompactGrid, + ComfortableGrid, + List; + + companion object { + val values = values() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/models/Category.kt b/src/main/kotlin/ca/gosyer/data/models/Category.kt similarity index 91% rename from src/main/kotlin/ca/gosyer/backend/models/Category.kt rename to src/main/kotlin/ca/gosyer/data/models/Category.kt index 6c1df4e3..3259a4ec 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Category.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Category.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/models/Chapter.kt b/src/main/kotlin/ca/gosyer/data/models/Chapter.kt similarity index 87% rename from src/main/kotlin/ca/gosyer/backend/models/Chapter.kt rename to src/main/kotlin/ca/gosyer/data/models/Chapter.kt index 0c8419e2..1bc919af 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Chapter.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Chapter.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -21,4 +21,6 @@ data class Chapter( val scanlator: String?, val mangaId: Long, val pageCount: Int? = null, + val chapterIndex: Int, + val chapterCount: Int ) diff --git a/src/main/kotlin/ca/gosyer/backend/models/Extension.kt b/src/main/kotlin/ca/gosyer/data/models/Extension.kt similarity index 94% rename from src/main/kotlin/ca/gosyer/backend/models/Extension.kt rename to src/main/kotlin/ca/gosyer/data/models/Extension.kt index a6e981f2..bb386ad2 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Extension.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Extension.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/models/Manga.kt b/src/main/kotlin/ca/gosyer/data/models/Manga.kt similarity index 95% rename from src/main/kotlin/ca/gosyer/backend/models/Manga.kt rename to src/main/kotlin/ca/gosyer/data/models/Manga.kt index 934cbd1f..9407dc16 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Manga.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Manga.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/models/MangaPage.kt b/src/main/kotlin/ca/gosyer/data/models/MangaPage.kt similarity index 91% rename from src/main/kotlin/ca/gosyer/backend/models/MangaPage.kt rename to src/main/kotlin/ca/gosyer/data/models/MangaPage.kt index 3b879848..1c9804a7 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/MangaPage.kt +++ b/src/main/kotlin/ca/gosyer/data/models/MangaPage.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/models/Page.kt b/src/main/kotlin/ca/gosyer/data/models/Page.kt similarity index 91% rename from src/main/kotlin/ca/gosyer/backend/models/Page.kt rename to src/main/kotlin/ca/gosyer/data/models/Page.kt index bfa705ce..a492db72 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Page.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Page.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/models/Source.kt b/src/main/kotlin/ca/gosyer/data/models/Source.kt similarity index 93% rename from src/main/kotlin/ca/gosyer/backend/models/Source.kt rename to src/main/kotlin/ca/gosyer/data/models/Source.kt index 65368800..db0efdbe 100644 --- a/src/main/kotlin/ca/gosyer/backend/models/Source.kt +++ b/src/main/kotlin/ca/gosyer/data/models/Source.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.models +package ca.gosyer.data.models import kotlinx.serialization.Serializable diff --git a/src/main/kotlin/ca/gosyer/backend/network/HttpClient.kt b/src/main/kotlin/ca/gosyer/data/server/HttpClient.kt similarity index 69% rename from src/main/kotlin/ca/gosyer/backend/network/HttpClient.kt rename to src/main/kotlin/ca/gosyer/data/server/HttpClient.kt index 0f200114..139b5770 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/HttpClient.kt +++ b/src/main/kotlin/ca/gosyer/data/server/HttpClient.kt @@ -4,18 +4,21 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network +package ca.gosyer.data.server import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.logging.LogLevel import io.ktor.client.features.logging.Logging -import org.koin.dsl.module +import javax.inject.Inject +import javax.inject.Provider -val networkModule = module { - single { - HttpClient(OkHttp) { +typealias Http = HttpClient + +internal class HttpProvider @Inject constructor() : Provider { + override fun get(): Http { + return HttpClient(OkHttp) { install(JsonFeature) install(Logging) { level = LogLevel.INFO diff --git a/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt b/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt new file mode 100644 index 00000000..bc5dac1f --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.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.data.server + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore + +class ServerPreferences(private val preferenceStore: PreferenceStore) { + fun server(): Preference { + return preferenceStore.getString("server_url", "http://localhost:4567") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/BaseInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/BaseInteractionHandler.kt similarity index 82% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/BaseInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/BaseInteractionHandler.kt index 10647e2c..687f843d 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/BaseInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/BaseInteractionHandler.kt @@ -4,12 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions import androidx.compose.ui.graphics.ImageBitmap -import ca.gosyer.backend.preferences.PreferenceHelper -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.delete import io.ktor.client.request.forms.submitForm @@ -19,11 +18,14 @@ import io.ktor.client.request.post import io.ktor.http.Parameters import kotlinx.coroutines.CancellationException -open class BaseInteractionHandler { - val preferences: PreferenceHelper by inject() - val serverUrl get() = preferences.serverUrl.get() +open class BaseInteractionHandler( + protected val client: Http, + serverPreferences: ServerPreferences +) { + private val _serverUrl = serverPreferences.server() + val serverUrl get() = _serverUrl.get() - protected suspend inline fun HttpClient.getRepeat( + protected suspend inline fun Http.getRepeat( urlString: String, block: HttpRequestBuilder.() -> Unit = {} ): T { @@ -41,7 +43,7 @@ open class BaseInteractionHandler { throw lastException } - protected suspend inline fun HttpClient.deleteRepeat( + protected suspend inline fun Http.deleteRepeat( urlString: String, block: HttpRequestBuilder.() -> Unit = {} ): T { @@ -59,7 +61,7 @@ open class BaseInteractionHandler { throw lastException } - protected suspend inline fun HttpClient.patchRepeat( + protected suspend inline fun Http.patchRepeat( urlString: String, block: HttpRequestBuilder.() -> Unit = {} ): T { @@ -77,7 +79,7 @@ open class BaseInteractionHandler { throw lastException } - protected suspend inline fun HttpClient.postRepeat( + protected suspend inline fun Http.postRepeat( urlString: String, block: HttpRequestBuilder.() -> Unit = {} ): T { @@ -95,7 +97,7 @@ open class BaseInteractionHandler { throw lastException } - protected suspend inline fun HttpClient.submitFormRepeat( + protected suspend inline fun Http.submitFormRepeat( urlString: String, formParameters: Parameters = Parameters.Empty, encodeInQuery: Boolean = false, @@ -115,7 +117,7 @@ open class BaseInteractionHandler { throw lastException } - suspend fun imageFromUrl(client: HttpClient, imageUrl: String): ImageBitmap { + suspend fun imageFromUrl(client: Http, imageUrl: String): ImageBitmap { var attempt = 1 var lastException: Exception do { diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/CategoryInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt similarity index 81% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/CategoryInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt index 5093866f..870e1ca1 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/CategoryInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/CategoryInteractionHandler.kt @@ -4,27 +4,32 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.Category -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.requests.addMangaToCategoryQuery -import ca.gosyer.backend.network.requests.categoryDeleteRequest -import ca.gosyer.backend.network.requests.categoryModifyRequest -import ca.gosyer.backend.network.requests.categoryReorderRequest -import ca.gosyer.backend.network.requests.createCategoryRequest -import ca.gosyer.backend.network.requests.getCategoriesQuery -import ca.gosyer.backend.network.requests.getMangaCategoriesQuery -import ca.gosyer.backend.network.requests.getMangaInCategoryQuery -import ca.gosyer.backend.network.requests.removeMangaFromCategoryRequest -import io.ktor.client.HttpClient +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.addMangaToCategoryQuery +import ca.gosyer.data.server.requests.categoryDeleteRequest +import ca.gosyer.data.server.requests.categoryModifyRequest +import ca.gosyer.data.server.requests.categoryReorderRequest +import ca.gosyer.data.server.requests.createCategoryRequest +import ca.gosyer.data.server.requests.getCategoriesQuery +import ca.gosyer.data.server.requests.getMangaCategoriesQuery +import ca.gosyer.data.server.requests.getMangaInCategoryQuery +import ca.gosyer.data.server.requests.removeMangaFromCategoryRequest import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpMethod import io.ktor.http.Parameters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class CategoryInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class CategoryInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getMangaCategories(mangaId: Long) = withContext(Dispatchers.IO) { client.getRepeat>( diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/ChapterInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt similarity index 75% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/ChapterInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt index 39571cf3..75f1af7e 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/ChapterInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/ChapterInteractionHandler.kt @@ -4,18 +4,23 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.Chapter -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.requests.getChapterQuery -import ca.gosyer.backend.network.requests.getMangaChaptersQuery -import ca.gosyer.backend.network.requests.getPageQuery -import io.ktor.client.HttpClient +import ca.gosyer.data.models.Chapter +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.getChapterQuery +import ca.gosyer.data.server.requests.getMangaChaptersQuery +import ca.gosyer.data.server.requests.getPageQuery import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class ChapterInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class ChapterInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) { client.getRepeat>( diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/ExtensionInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt similarity index 65% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/ExtensionInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt index 659fa184..656a970e 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/ExtensionInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/ExtensionInteractionHandler.kt @@ -4,19 +4,24 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.Extension -import ca.gosyer.backend.network.requests.apkIconQuery -import ca.gosyer.backend.network.requests.apkInstallQuery -import ca.gosyer.backend.network.requests.apkUninstallQuery -import ca.gosyer.backend.network.requests.extensionListQuery -import io.ktor.client.HttpClient +import ca.gosyer.data.models.Extension +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.apkIconQuery +import ca.gosyer.data.server.requests.apkInstallQuery +import ca.gosyer.data.server.requests.apkUninstallQuery +import ca.gosyer.data.server.requests.extensionListQuery import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class ExtensionInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class ExtensionInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getExtensionList() = withContext(Dispatchers.IO) { client.getRepeat>( diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/LibraryInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt similarity index 66% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/LibraryInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt index 1e36aa6b..6df4b211 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/LibraryInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/LibraryInteractionHandler.kt @@ -4,18 +4,23 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.requests.addMangaToLibraryQuery -import ca.gosyer.backend.network.requests.getLibraryQuery -import ca.gosyer.backend.network.requests.removeMangaFromLibraryRequest -import io.ktor.client.HttpClient +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.addMangaToLibraryQuery +import ca.gosyer.data.server.requests.getLibraryQuery +import ca.gosyer.data.server.requests.removeMangaFromLibraryRequest import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class LibraryInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class LibraryInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getLibraryManga() = withContext(Dispatchers.IO) { client.getRepeat>( diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/MangaInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt similarity index 58% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/MangaInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt index 4976c71f..2e350ef2 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/MangaInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/MangaInteractionHandler.kt @@ -4,16 +4,21 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.requests.mangaQuery -import ca.gosyer.backend.network.requests.mangaThumbnailQuery -import io.ktor.client.HttpClient +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.mangaQuery +import ca.gosyer.data.server.requests.mangaThumbnailQuery import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class MangaInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class MangaInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) { client.getRepeat( diff --git a/src/main/kotlin/ca/gosyer/backend/network/interactions/SourceInteractionHandler.kt b/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt similarity index 74% rename from src/main/kotlin/ca/gosyer/backend/network/interactions/SourceInteractionHandler.kt rename to src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt index 9d234014..65162da4 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/interactions/SourceInteractionHandler.kt +++ b/src/main/kotlin/ca/gosyer/data/server/interactions/SourceInteractionHandler.kt @@ -4,23 +4,28 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.interactions +package ca.gosyer.data.server.interactions -import ca.gosyer.backend.models.MangaPage -import ca.gosyer.backend.models.Source -import ca.gosyer.backend.network.requests.getFilterListQuery -import ca.gosyer.backend.network.requests.globalSearchQuery -import ca.gosyer.backend.network.requests.sourceInfoQuery -import ca.gosyer.backend.network.requests.sourceLatestQuery -import ca.gosyer.backend.network.requests.sourceListQuery -import ca.gosyer.backend.network.requests.sourcePopularQuery -import ca.gosyer.backend.network.requests.sourceSearchQuery -import io.ktor.client.HttpClient +import ca.gosyer.data.models.MangaPage +import ca.gosyer.data.models.Source +import ca.gosyer.data.server.Http +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.requests.getFilterListQuery +import ca.gosyer.data.server.requests.globalSearchQuery +import ca.gosyer.data.server.requests.sourceInfoQuery +import ca.gosyer.data.server.requests.sourceLatestQuery +import ca.gosyer.data.server.requests.sourceListQuery +import ca.gosyer.data.server.requests.sourcePopularQuery +import ca.gosyer.data.server.requests.sourceSearchQuery import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class SourceInteractionHandler(private val client: HttpClient): BaseInteractionHandler() { +class SourceInteractionHandler @Inject constructor( + client: Http, + serverPreferences: ServerPreferences +): BaseInteractionHandler(client, serverPreferences) { suspend fun getSourceList() = withContext(Dispatchers.IO) { client.getRepeat>( diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Category.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Category.kt similarity index 96% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Category.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Category.kt index 9a9171c1..e8926875 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Category.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Category.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun getMangaCategoriesQuery(mangaId: Long) = diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Chapters.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Chapters.kt similarity index 92% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Chapters.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Chapters.kt index a2d4322d..7250ebbf 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Chapters.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Chapters.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun getMangaChaptersQuery(mangaId: Long) = diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Extensions.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Extensions.kt similarity index 92% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Extensions.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Extensions.kt index df7af2da..842cefa8 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Extensions.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Extensions.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun extensionListQuery() = diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Library.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Library.kt similarity index 91% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Library.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Library.kt index 95eb7989..22bc1c90 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Library.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Library.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun addMangaToLibraryQuery(mangaId: Long) = diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Manga.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt similarity index 89% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Manga.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt index 169f5be1..95067720 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Manga.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Manga.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun mangaQuery(mangaId: Long) = diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/RestRequests.kt b/src/main/kotlin/ca/gosyer/data/server/requests/RestRequests.kt similarity index 87% rename from src/main/kotlin/ca/gosyer/backend/network/requests/RestRequests.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/RestRequests.kt index 53289bf5..1ee59f63 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/RestRequests.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/RestRequests.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests annotation class Get diff --git a/src/main/kotlin/ca/gosyer/backend/network/requests/Sources.kt b/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt similarity index 95% rename from src/main/kotlin/ca/gosyer/backend/network/requests/Sources.kt rename to src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt index fc3e0013..b87961f3 100644 --- a/src/main/kotlin/ca/gosyer/backend/network/requests/Sources.kt +++ b/src/main/kotlin/ca/gosyer/data/server/requests/Sources.kt @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package ca.gosyer.backend.network.requests +package ca.gosyer.data.server.requests @Get fun sourceListQuery() = diff --git a/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt b/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt new file mode 100644 index 00000000..f97e71ea --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt @@ -0,0 +1,64 @@ +/* + * 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.data.ui + +import ca.gosyer.common.prefs.Preference +import ca.gosyer.common.prefs.PreferenceStore +import ca.gosyer.common.prefs.getEnum +import ca.gosyer.data.ui.model.ThemeMode + +class UiPreferences(private val preferenceStore: PreferenceStore) { + + fun themeMode(): Preference { + return preferenceStore.getEnum("theme_mode", ThemeMode.System) + } + + fun lightTheme(): Preference { + return preferenceStore.getInt("theme_light", 0) + } + + fun darkTheme(): Preference { + return preferenceStore.getInt("theme_dark", 0) + } + + fun colorPrimaryLight(): Preference { + return preferenceStore.getInt("color_primary_light", 0) + } + + fun colorPrimaryDark(): Preference { + return preferenceStore.getInt("color_primary_dark", 0) + } + + fun colorSecondaryLight(): Preference { + return preferenceStore.getInt("color_secondary_light", 0) + } + + fun colorSecondaryDark(): Preference { + return preferenceStore.getInt("color_secondary_dark", 0) + } + + fun colorBarsLight(): Preference { + return preferenceStore.getInt("color_bar_light", 0) + } + + fun colorBarsDark(): Preference { + return preferenceStore.getInt("color_bar_dark", 0) + } + + fun confirmExit(): Preference { + return preferenceStore.getBoolean("confirm_exit", false) + } + + fun language(): Preference { + return preferenceStore.getString("language", "") + } + + fun dateFormat(): Preference { + return preferenceStore.getString("date_format", "") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt b/src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt new file mode 100644 index 00000000..2d6b8c05 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.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.data.ui.model + +enum class ThemeMode { + System, + Light, + Dark, +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt b/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt index 265ee280..beca5491 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.sp @@ -22,7 +23,8 @@ import kotlin.random.Random fun ErrorScreen(errorMessage: String? = null) { Box(Modifier.fillMaxSize()) { Column(modifier = Modifier.align(Alignment.Center)) { - Text(getRandomErrorFace(), fontSize = 36.sp, color = MaterialTheme.colors.onBackground) + val errorFace = remember { getRandomErrorFace() } + Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground) if (errorMessage != null) { Text(errorMessage, color = MaterialTheme.colors.onBackground) } diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/KtorImage.kt b/src/main/kotlin/ca/gosyer/ui/base/components/KtorImage.kt index def59e9e..eea7d9b0 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/KtorImage.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/KtorImage.kt @@ -20,8 +20,9 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.layout.ContentScale +import ca.gosyer.common.di.AppScope +import ca.gosyer.data.server.Http import ca.gosyer.util.compose.imageFromUrl -import io.ktor.client.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.GlobalScope @@ -29,7 +30,6 @@ import kotlinx.coroutines.launch @Composable fun KtorImage( - client: HttpClient, imageUrl: String, imageModifier: Modifier = Modifier.fillMaxSize(), loadingModifier: Modifier = imageModifier, @@ -38,14 +38,18 @@ fun KtorImage( contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null, - retries: Int = 3 + retries: Int = 3, + httpClient: Http? = null ) { + val client = remember { httpClient ?: AppScope.getInstance() } BoxWithConstraints { val drawable: MutableState = remember { mutableStateOf(null) } val loading: MutableState = remember { mutableStateOf(true) } + val error: MutableState = remember { mutableStateOf(null) } DisposableEffect(imageUrl) { - val handler = CoroutineExceptionHandler { _, _ -> + val handler = CoroutineExceptionHandler { _, throwable -> loading.value = false + error.value = throwable.message } val job = GlobalScope.launch(handler) { if (drawable.value == null) { @@ -72,12 +76,12 @@ fun KtorImage( colorFilter = colorFilter ) } else { - LoadingScreen(loading.value, loadingModifier) + LoadingScreen(loading.value, loadingModifier, error.value) } } } -private suspend fun getImage(client: HttpClient, imageUrl: String, retries: Int = 3): ImageBitmap { +private suspend fun getImage(client: Http, imageUrl: String, retries: Int = 3): ImageBitmap { var attempt = 1 var lastException: Exception do { diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt b/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt index 0bdd04e8..b73f607d 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.min @@ -23,7 +24,10 @@ fun LoadingScreen( ) { BoxWithConstraints(modifier) { if (isLoading) { - CircularProgressIndicator(Modifier.align(Alignment.Center).size(min(maxHeight, maxWidth) / 2)) + val size = remember(maxHeight, maxWidth) { + min(maxHeight, maxWidth) / 2 + } + CircularProgressIndicator(Modifier.align(Alignment.Center).size(size)) } else { ErrorScreen(errorMessage) } diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt b/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt index 91be7e09..2968c4e8 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ca.gosyer.util.system.get @Composable fun MangaGridItem( @@ -53,7 +52,7 @@ fun MangaGridItem( ) { Box(modifier = Modifier.fillMaxSize()) { if (cover != null) { - KtorImage(get(), cover, contentScale = ContentScale.Crop) + KtorImage(cover, contentScale = ContentScale.Crop) } Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Text( diff --git a/src/main/kotlin/ca/gosyer/ui/base/vm/ComposeViewModel.kt b/src/main/kotlin/ca/gosyer/ui/base/vm/ComposeViewModel.kt index 870ef08a..c9ab6311 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/vm/ComposeViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/vm/ComposeViewModel.kt @@ -7,14 +7,18 @@ package ca.gosyer.ui.base.vm import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember -import org.koin.core.context.GlobalContext +import ca.gosyer.common.di.AppScope +import toothpick.Toothpick +import toothpick.ktp.binding.module +import toothpick.ktp.extension.getInstance @Composable -inline fun composeViewModel(): VM { +inline fun viewModel(): VM { val viewModel = remember { - GlobalContext.get().get() + AppScope.getInstance() } DisposableEffect(viewModel) { onDispose { @@ -23,3 +27,26 @@ inline fun composeViewModel(): VM { } return viewModel } + +@Composable +inline fun viewModel( + crossinline binding: @DisallowComposableCalls () -> Any, +): VM { + val (viewModel, submodule) = remember { + val submodule = module { + binding().let { bind(it.javaClass).toInstance(it) } + } + val subscope = AppScope.subscope(submodule).also { + it.installModules(submodule) + } + val viewModel = subscope.getInstance() + Pair(viewModel, submodule) + } + DisposableEffect(viewModel) { + onDispose { + viewModel.destroy() + Toothpick.closeScope(submodule) + } + } + return viewModel +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/base/vm/ViewModelModule.kt b/src/main/kotlin/ca/gosyer/ui/base/vm/ViewModelModule.kt deleted file mode 100644 index fc4a24ad..00000000 --- a/src/main/kotlin/ca/gosyer/ui/base/vm/ViewModelModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.ui.base.vm - -import ca.gosyer.ui.categories.CategoriesMenuViewModel -import ca.gosyer.ui.extensions.ExtensionsMenuViewModel -import ca.gosyer.ui.library.LibraryScreenViewModel -import ca.gosyer.ui.main.MainViewModel -import ca.gosyer.ui.manga.MangaMenuViewModel -import ca.gosyer.ui.sources.SourcesMenuViewModel -import ca.gosyer.ui.sources.components.SourceScreenViewModel -import org.koin.dsl.module - -val viewModelModule = module { - factory { MainViewModel() } - factory { ExtensionsMenuViewModel() } - factory { SourcesMenuViewModel() } - factory { SourceScreenViewModel() } - factory { MangaMenuViewModel() } - factory { LibraryScreenViewModel() } - factory { CategoriesMenuViewModel() } -} diff --git a/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt b/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt index 351ad45d..4139ecd7 100644 --- a/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenu.kt @@ -39,7 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.util.compose.ThemedWindow fun openCategoriesMenu() { @@ -51,7 +51,7 @@ fun openCategoriesMenu() { @Composable fun CategoriesMenu(windowEvents: WindowEvents) { - val vm = composeViewModel() + val vm = viewModel() val categories by vm.categories.collectAsState() remember { windowEvents.onClose = { vm.updateCategories() } diff --git a/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt index 8fff950c..874ad1a0 100644 --- a/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/categories/CategoriesMenuViewModel.kt @@ -6,11 +6,9 @@ package ca.gosyer.ui.categories -import ca.gosyer.backend.models.Category -import ca.gosyer.backend.network.interactions.CategoryInteractionHandler +import ca.gosyer.data.models.Category +import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.GlobalScope @@ -18,9 +16,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import mu.KotlinLogging +import javax.inject.Inject -class CategoriesMenuViewModel : ViewModel() { - private val httpClient: HttpClient by inject() +class CategoriesMenuViewModel @Inject constructor( + private val categoryHandler: CategoryInteractionHandler +) : ViewModel() { private val logger = KotlinLogging.logger {} private var originalCategories = emptyList() private val _categories = MutableStateFlow(emptyList()) @@ -38,7 +38,7 @@ class CategoriesMenuViewModel : ViewModel() { _categories.value = emptyList() _isLoading.value = true try { - _categories.value = CategoryInteractionHandler(httpClient).getCategories() + _categories.value = categoryHandler.getCategories() .sortedBy { it.order } .also { originalCategories = it } .map { it.toMenuCategory() } @@ -58,22 +58,22 @@ class CategoriesMenuViewModel : ViewModel() { val categories = _categories.value val newCategories = categories.filter { it.id == null } newCategories.forEach { - CategoryInteractionHandler(httpClient).createCategory(it.name) + categoryHandler.createCategory(it.name) } originalCategories.forEach { originalCategory -> val category = categories.find { it.id == originalCategory.id } if (category == null) { - CategoryInteractionHandler(httpClient).deleteCategory(originalCategory) + categoryHandler.deleteCategory(originalCategory) } else if (category.name != originalCategory.name) { - CategoryInteractionHandler(httpClient).modifyCategory(originalCategory, category.name) + categoryHandler.modifyCategory(originalCategory, category.name) } } - val updatedCategories = CategoryInteractionHandler(httpClient).getCategories() + val updatedCategories = categoryHandler.getCategories() updatedCategories.forEach { updatedCategory -> val category = categories.find { it.id == updatedCategory.id || it.name == updatedCategory.name } ?: return@forEach if (category.order != updatedCategory.order) { logger.debug { "${category.order} to ${updatedCategory.order}" } - CategoryInteractionHandler(httpClient).reorderCategory(updatedCategory, category.order, updatedCategory.order) + categoryHandler.reorderCategory(updatedCategory, category.order, updatedCategory.order) } } diff --git a/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt b/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt index a83f23cd..79d9280c 100644 --- a/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenu.kt @@ -40,12 +40,11 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ca.gosyer.backend.models.Extension +import ca.gosyer.data.models.Extension import ca.gosyer.ui.base.components.KtorImage import ca.gosyer.ui.base.components.LoadingScreen -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.system.get fun openExtensionsMenu() { ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) { @@ -56,7 +55,7 @@ fun openExtensionsMenu() { @OptIn(ExperimentalFoundationApi::class) @Composable fun ExtensionsMenu() { - val vm = composeViewModel() + val vm = viewModel() val extensions by vm.extensions.collectAsState() val isLoading by vm.isLoading.collectAsState() val serverUrl by vm.serverUrl.collectAsState() @@ -107,7 +106,7 @@ fun ExtensionItem( Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) { Row(verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.width(4.dp)) - KtorImage(get(), extension.iconUrl(serverUrl), Modifier.size(60.dp)) + KtorImage(extension.iconUrl(serverUrl), Modifier.size(60.dp)) Spacer(Modifier.width(8.dp)) Column { val title = buildAnnotatedString { diff --git a/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt index 93109b77..22ccdee0 100644 --- a/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/extensions/ExtensionsMenuViewModel.kt @@ -6,24 +6,26 @@ package ca.gosyer.ui.extensions -import ca.gosyer.backend.models.Extension -import ca.gosyer.backend.network.interactions.ExtensionInteractionHandler -import ca.gosyer.backend.preferences.PreferenceHelper +import ca.gosyer.data.extension.ExtensionPreferences +import ca.gosyer.data.models.Extension +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.ExtensionInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import mu.KotlinLogging +import javax.inject.Inject -class ExtensionsMenuViewModel: ViewModel() { - private val preferences: PreferenceHelper by inject() - private val httpClient: HttpClient by inject() +class ExtensionsMenuViewModel @Inject constructor( + private val extensionHandler: ExtensionInteractionHandler, + serverPreferences: ServerPreferences, + private val extensionPreferences: ExtensionPreferences +): ViewModel() { private val logger = KotlinLogging.logger {} - val serverUrl = preferences.serverUrl.asStateFlow(scope) + val serverUrl = serverPreferences.server().stateIn(scope) private val _extensions = MutableStateFlow(emptyList()) val extensions = _extensions.asStateFlow() @@ -41,8 +43,8 @@ class ExtensionsMenuViewModel: ViewModel() { private suspend fun getExtensions() { try { _isLoading.value = true - val enabledLangs = preferences.enabledLangs.get() - val extensions = ExtensionInteractionHandler(httpClient).getExtensionList() + val enabledLangs = extensionPreferences.languages().get() + val extensions = extensionHandler.getExtensionList() _extensions.value = extensions.filter { it.lang in enabledLangs }.sortedWith(compareBy({ it.lang }, { it.pkgName })) } catch (e: Exception) { if (e is CancellationException) throw e @@ -55,7 +57,7 @@ class ExtensionsMenuViewModel: ViewModel() { logger.info { "Install clicked" } scope.launch { try { - ExtensionInteractionHandler(httpClient).installExtension(extension) + extensionHandler.installExtension(extension) } catch (e: Exception) { if (e is CancellationException) throw e } @@ -67,7 +69,7 @@ class ExtensionsMenuViewModel: ViewModel() { logger.info { "Uninstall clicked" } scope.launch { try { - ExtensionInteractionHandler(httpClient).uninstallExtension(extension) + extensionHandler.uninstallExtension(extension) } catch (e: Exception) { if (e is CancellationException) throw e } diff --git a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt index 8ec57328..c1c2550f 100644 --- a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt @@ -23,15 +23,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp -import ca.gosyer.backend.models.Category -import ca.gosyer.backend.models.Manga +import ca.gosyer.data.library.model.DisplayMode +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.Pager import ca.gosyer.ui.base.components.PagerState -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.util.compose.ThemedWindow -import kotlinx.serialization.Serializable fun openLibraryMenu() { ThemedWindow { @@ -42,7 +42,7 @@ fun openLibraryMenu() { @OptIn(ExperimentalMaterialApi::class) @Composable fun LibraryScreen() { - val vm = composeViewModel() + val vm = viewModel() val categories by vm.categories.collectAsState() val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState() val displayMode by vm.displayMode.collectAsState() @@ -169,14 +169,4 @@ private fun LibraryPager( else -> Box {} } } -} - -@Serializable -sealed class DisplayMode { - @Serializable - object List : DisplayMode() - @Serializable - object CompactGrid : DisplayMode() - @Serializable - object ComfortableGrid : DisplayMode() } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt index 8d74749f..5855195f 100644 --- a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreenViewModel.kt @@ -6,20 +6,20 @@ package ca.gosyer.ui.library -import ca.gosyer.backend.models.Category -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.interactions.CategoryInteractionHandler -import ca.gosyer.backend.network.interactions.LibraryInteractionHandler -import ca.gosyer.backend.preferences.PreferenceHelper +import ca.gosyer.data.library.LibraryPreferences +import ca.gosyer.data.models.Category +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.CategoryInteractionHandler +import ca.gosyer.data.server.interactions.LibraryInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject private typealias LibraryMap = MutableMap>> private data class Library(val categories: MutableStateFlow>, val mangaMap: LibraryMap) @@ -30,11 +30,13 @@ private fun LibraryMap.setManga(order: Int, manga: List) { getManga(order).value = manga } -class LibraryScreenViewModel: ViewModel() { - private val preferences: PreferenceHelper by inject() - private val httpClient: HttpClient by inject() - - val serverUrl = preferences.serverUrl.asStateFlow(scope) +class LibraryScreenViewModel @Inject constructor( + private val libraryHandler: LibraryInteractionHandler, + private val categoryHandler: CategoryInteractionHandler, + libraryPreferences: LibraryPreferences, + serverPreferences: ServerPreferences, +): ViewModel() { + val serverUrl = serverPreferences.server().stateIn(scope) private val library = Library(MutableStateFlow(emptyList()), mutableMapOf()) val categories = library.categories.asStateFlow() @@ -42,7 +44,7 @@ class LibraryScreenViewModel: ViewModel() { private val _selectedCategoryIndex = MutableStateFlow(0) val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() - val displayMode = preferences.libraryDisplay.asStateFlow(scope) + val displayMode = libraryPreferences.displayMode().stateIn(scope) private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() @@ -55,19 +57,19 @@ class LibraryScreenViewModel: ViewModel() { scope.launch { _isLoading.value = true try { - val categories = CategoryInteractionHandler(httpClient).getCategories() + val categories = categoryHandler.getCategories() if (categories.isEmpty()) { library.categories.value = listOf(defaultCategory) - library.mangaMap.setManga(defaultCategory.order, LibraryInteractionHandler(httpClient).getLibraryManga()) + library.mangaMap.setManga(defaultCategory.order, libraryHandler.getLibraryManga()) } else { library.categories.value = listOf(defaultCategory) + categories.sortedBy { it.order } categories.map { async { - library.mangaMap.setManga(it.order, CategoryInteractionHandler(httpClient).getMangaFromCategory(it)) + library.mangaMap.setManga(it.order, categoryHandler.getMangaFromCategory(it)) } }.awaitAll() val mangaInCategories = library.mangaMap.flatMap { it.value.value }.map { it.id }.distinct() - library.mangaMap.setManga(defaultCategory.order, LibraryInteractionHandler(httpClient).getLibraryManga().filterNot { it.id in mangaInCategories }) + library.mangaMap.setManga(defaultCategory.order, libraryHandler.getLibraryManga().filterNot { it.id in mangaInCategories }) } } catch (e: Exception) { } finally { diff --git a/src/main/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt b/src/main/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt index e2dfc0f8..4cdbb4e0 100644 --- a/src/main/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt +++ b/src/main/kotlin/ca/gosyer/ui/library/MangaCompactGrid.kt @@ -32,9 +32,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ca.gosyer.backend.models.Manga +import ca.gosyer.data.models.Manga import ca.gosyer.ui.base.components.KtorImage -import ca.gosyer.util.system.get @Composable fun LibraryMangaCompactGrid( @@ -78,7 +77,7 @@ private fun LibraryMangaCompactGridItem( .clickable(onClick = onClick) ) { if (cover != null) { - KtorImage(get(), cover, contentScale = ContentScale.Crop) + KtorImage(cover, contentScale = ContentScale.Crop) } Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Text( diff --git a/src/main/kotlin/ca/gosyer/ui/main/MainViewModel.kt b/src/main/kotlin/ca/gosyer/ui/main/MainViewModel.kt index 8ec91538..28f3e45f 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/MainViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/MainViewModel.kt @@ -7,5 +7,6 @@ package ca.gosyer.ui.main import ca.gosyer.ui.base.vm.ViewModel +import javax.inject.Inject -class MainViewModel : ViewModel() \ No newline at end of file +class MainViewModel @Inject constructor(): ViewModel() \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/main/main.kt b/src/main/kotlin/ca/gosyer/ui/main/main.kt index dbad2ed9..20a42d3d 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/main.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/main.kt @@ -6,17 +6,15 @@ package ca.gosyer.ui.main -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.ui.Modifier -import ca.gosyer.backend.network.networkModule -import ca.gosyer.backend.preferences.preferencesModule -import ca.gosyer.ui.base.vm.composeViewModel -import ca.gosyer.ui.base.vm.viewModelModule +import ca.gosyer.BuildConfig +import ca.gosyer.data.DataModule +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.categories.openCategoriesMenu import ca.gosyer.ui.extensions.openExtensionsMenu import ca.gosyer.ui.library.openLibraryMenu @@ -26,11 +24,21 @@ import ca.gosyer.util.system.userDataDir import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import mu.KotlinLogging -import org.koin.core.context.startKoin -import kotlin.concurrent.thread +import org.apache.logging.log4j.core.config.Configurator +import toothpick.configuration.Configuration +import toothpick.ktp.KTP +import java.io.BufferedReader import java.io.File +import kotlin.concurrent.thread fun main() { + val clazz = MainViewModel::class.java + Configurator.initialize( + null, + clazz.classLoader, + clazz.getResource("log4j2.xml")?.toURI() + ) + GlobalScope.launch { val logger = KotlinLogging.logger("Server") val runtime = Runtime.getRuntime() @@ -38,19 +46,30 @@ fun main() { val jarFile = File(userDataDir,"Tachidesk.jar") if (!jarFile.exists()) { logger.info { "Copying server to resources" } - javaClass.getResourceAsStream("/Tachidesk.jar").buffered().use { input -> + javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input -> jarFile.outputStream().use { output -> input.copyTo(output) } } } - logger.info { "Starting server" } - val process = runtime.exec("""java -jar "${jarFile.absolutePath}"""") + val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator) + val javaExeFile = File(javaLibraryPath, "java.exe") + val javaUnixFile = File(javaLibraryPath, "java") + val javaExePath = when { + javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"' + javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"' + else -> "java" + } + + logger.info { "Starting server with $javaExePath" } + val reader: BufferedReader + val process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also { + reader = it.inputStream.bufferedReader() + } runtime.addShutdownHook(thread(start = false) { process?.destroy() }) - val reader = process.inputStream.reader().buffered() logger.info { "Server started successfully" } var line: String? while (reader.readLine().also { line = it } != null) { @@ -61,37 +80,48 @@ fun main() { logger.info { "Process exitValue: $exitVal" } } - startKoin { - modules( - preferencesModule, - networkModule, - viewModelModule - ) + if (BuildConfig.DEBUG) { + System.setProperty("kotlinx.coroutines.debug", "on") } + KTP.setConfiguration( + if (BuildConfig.DEBUG) { + Configuration.forDevelopment() + } else { + Configuration.forProduction() + } + ) + + KTP.openRootScope() + .installModules( + DataModule + ) + ThemedWindow(title = "TachideskJUI") { - val vm = composeViewModel() - Column(Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { - Button( - onClick = ::openExtensionsMenu - ) { - Text("Extensions") - } - Button( - onClick = ::openSourcesMenu - ) { - Text("Sources") - } - Button( - onClick = ::openLibraryMenu - ) { - Text("Library") - } - Button( - onClick = ::openCategoriesMenu - ) { - Text("Categories") + val vm = viewModel() + Surface { + Column(Modifier.fillMaxSize()) { + Button( + onClick = ::openExtensionsMenu + ) { + Text("Extensions") + } + Button( + onClick = ::openSourcesMenu + ) { + Text("Sources") + } + Button( + onClick = ::openLibraryMenu + ) { + Text("Library") + } + Button( + onClick = ::openCategoriesMenu + ) { + Text("Categories") + } } } } diff --git a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt index b47e82b7..23af4e0d 100644 --- a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt @@ -39,14 +39,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import ca.gosyer.backend.models.Chapter -import ca.gosyer.backend.models.Manga +import ca.gosyer.data.models.Chapter +import ca.gosyer.data.models.Manga import ca.gosyer.ui.base.components.KtorImage import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.mangaAspectRatio -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.util.compose.ThemedWindow -import ca.gosyer.util.system.get fun openMangaMenu(mangaId: Long) { ThemedWindow("TachideskJUI") { @@ -62,7 +61,7 @@ fun openMangaMenu(manga: Manga) { @Composable fun MangaMenu(mangaId: Long) { - val vm = composeViewModel() + val vm = viewModel() remember(mangaId) { vm.init(mangaId) } @@ -71,7 +70,7 @@ fun MangaMenu(mangaId: Long) { @Composable fun MangaMenu(manga: Manga) { - val vm = composeViewModel() + val vm = viewModel() remember(manga) { vm.init(manga) } @@ -161,7 +160,7 @@ private fun Cover(manga: Manga, serverUrl: String, modifier: Modifier = Modifier Box(modifier = Modifier.fillMaxSize()) { manga.cover(serverUrl).let { if (it != null) { - KtorImage(get(), it) + KtorImage(it) } } } diff --git a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt index 3acd7bf5..a0d51d9b 100644 --- a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt @@ -6,15 +6,13 @@ package ca.gosyer.ui.manga -import ca.gosyer.backend.models.Chapter -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.network.interactions.ChapterInteractionHandler -import ca.gosyer.backend.network.interactions.LibraryInteractionHandler -import ca.gosyer.backend.network.interactions.MangaInteractionHandler -import ca.gosyer.backend.preferences.PreferenceHelper +import ca.gosyer.data.models.Chapter +import ca.gosyer.data.models.Manga +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.ChapterInteractionHandler +import ca.gosyer.data.server.interactions.LibraryInteractionHandler +import ca.gosyer.data.server.interactions.MangaInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -22,12 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import javax.inject.Inject -class MangaMenuViewModel : ViewModel() { - private val preferences: PreferenceHelper by inject() - private val httpClient: HttpClient by inject() - - val serverUrl = preferences.serverUrl.asStateFlow(scope) +class MangaMenuViewModel @Inject constructor( + private val mangaHandler: MangaInteractionHandler, + private val chapterHandler: ChapterInteractionHandler, + private val libraryHandler: LibraryInteractionHandler, + serverPreferences: ServerPreferences +) : ViewModel() { + val serverUrl = serverPreferences.server().stateIn(scope) private val _manga = MutableStateFlow(null) val manga = _manga.asStateFlow() @@ -53,7 +54,7 @@ class MangaMenuViewModel : ViewModel() { private suspend fun refreshMangaAsync(mangaId: Long) = withContext(Dispatchers.IO) { async { try { - _manga.value = MangaInteractionHandler(httpClient).getManga(mangaId) + _manga.value = mangaHandler.getManga(mangaId) } catch (e: Exception) { if (e is CancellationException) throw e } @@ -63,7 +64,7 @@ class MangaMenuViewModel : ViewModel() { suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) { async { try { - _chapters.value = ChapterInteractionHandler(httpClient).getChapters(mangaId) + _chapters.value = chapterHandler.getChapters(mangaId) } catch (e: Exception) { if (e is CancellationException) throw e } @@ -74,9 +75,9 @@ class MangaMenuViewModel : ViewModel() { scope.launch { manga.value?.let { if (it.inLibrary) { - LibraryInteractionHandler(httpClient).removeMangaFromLibrary(it) + libraryHandler.removeMangaFromLibrary(it) } else { - LibraryInteractionHandler(httpClient).addMangaToLibrary(it) + libraryHandler.addMangaToLibrary(it) } refreshMangaAsync(it.id).await() diff --git a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt index 9e1aed37..30fe92dd 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt @@ -14,8 +14,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import ca.gosyer.backend.models.Source -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.data.models.Source +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.sources.components.SourceHomeScreen import ca.gosyer.ui.sources.components.SourceScreen import ca.gosyer.ui.sources.components.SourceTopBar @@ -29,7 +29,7 @@ fun openSourcesMenu() { @Composable fun SourcesMenu() { - val vm = composeViewModel() + val vm = viewModel() val isLoading by vm.isLoading.collectAsState() val sources by vm.sources.collectAsState() val sourceTabs by vm.sourceTabs.collectAsState() diff --git a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt index 1b06fd13..83f50347 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt @@ -6,24 +6,28 @@ package ca.gosyer.ui.sources -import ca.gosyer.backend.models.Source -import ca.gosyer.backend.network.interactions.SourceInteractionHandler -import ca.gosyer.backend.preferences.PreferenceHelper +import ca.gosyer.data.catalog.CatalogPreferences +import ca.gosyer.data.models.Source +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import mu.KotlinLogging +import javax.inject.Inject -class SourcesMenuViewModel: ViewModel() { - private val preferences: PreferenceHelper by inject() - private val httpClient: HttpClient by inject() +class SourcesMenuViewModel @Inject constructor( + private val sourceHandler: SourceInteractionHandler, + serverPreferences: ServerPreferences, + catalogPreferences: CatalogPreferences +): ViewModel() { private val logger = KotlinLogging.logger {} - val serverUrl = preferences.serverUrl.asStateFlow(scope) + val serverUrl = serverPreferences.server().stateIn(scope) + + private val languages = catalogPreferences.languages().stateIn(scope) private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() @@ -44,9 +48,9 @@ class SourcesMenuViewModel: ViewModel() { private fun getSources() { scope.launch { try { - val sources = SourceInteractionHandler(httpClient).getSourceList() + val sources = sourceHandler.getSourceList() logger.info { sources } - _sources.value = sources//.filter { it.lang in Preferences.enabledLangs } + _sources.value = sources.filter { it.lang in languages.value } logger.info { _sources.value } } catch (e: Exception) { if (e is CancellationException) throw e diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt index 084ca90f..4fce9e0a 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceHomeScreen.kt @@ -29,10 +29,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ca.gosyer.backend.models.Source +import ca.gosyer.data.models.Source import ca.gosyer.ui.base.components.KtorImage import ca.gosyer.ui.base.components.LoadingScreen -import ca.gosyer.util.system.get @Composable fun SourceHomeScreen( @@ -110,7 +109,7 @@ fun SourceItem( }, horizontalAlignment = Alignment.CenterHorizontally ) { - KtorImage(get(), source.iconUrl(serverUrl), Modifier.size(96.dp)) + KtorImage(source.iconUrl(serverUrl), Modifier.size(96.dp)) Spacer(Modifier.height(4.dp)) Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground) } diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt index a15c3ae7..ddc74252 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt @@ -21,18 +21,18 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.models.Source +import ca.gosyer.data.models.Manga +import ca.gosyer.data.models.Source import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.MangaGridItem -import ca.gosyer.ui.base.vm.composeViewModel +import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.manga.openMangaMenu @Composable fun SourceScreen( source: Source ) { - val vm = composeViewModel() + val vm = viewModel() remember(source) { vm.init(source) } diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt index e94e9de4..08190be7 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt @@ -6,26 +6,24 @@ package ca.gosyer.ui.sources.components -import ca.gosyer.backend.models.Manga -import ca.gosyer.backend.models.MangaPage -import ca.gosyer.backend.models.Source -import ca.gosyer.backend.network.interactions.SourceInteractionHandler -import ca.gosyer.backend.preferences.PreferenceHelper +import ca.gosyer.data.models.Manga +import ca.gosyer.data.models.MangaPage +import ca.gosyer.data.models.Source +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.ui.base.vm.ViewModel -import ca.gosyer.util.system.asStateFlow -import ca.gosyer.util.system.inject -import io.ktor.client.HttpClient import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import javax.inject.Inject -class SourceScreenViewModel: ViewModel() { +class SourceScreenViewModel @Inject constructor( + private val sourceHandler: SourceInteractionHandler, + serverPreferences: ServerPreferences +): ViewModel() { private lateinit var source: Source - private val preferences: PreferenceHelper by inject() - private val httpClient: HttpClient by inject() - val serverUrl = preferences.serverUrl.asFLow() - .asStateFlow(preferences.serverUrl.get(),scope, true) + val serverUrl = serverPreferences.server().stateIn(scope) private val _mangas = MutableStateFlow(emptyList()) val mangas = _mangas.asStateFlow() @@ -49,6 +47,7 @@ class SourceScreenViewModel: ViewModel() { _mangas.value = emptyList() _hasNextPage.value = false _pageNum.value = 1 + _isLatest.value = source.supportsLatest val page = getPage() _mangas.value += page.mangaList _hasNextPage.value = page.hasNextPage @@ -76,9 +75,9 @@ class SourceScreenViewModel: ViewModel() { private suspend fun getPage(): MangaPage { return if (isLatest.value) { - SourceInteractionHandler(httpClient).getLatestManga(source, pageNum.value) + sourceHandler.getLatestManga(source, pageNum.value) } else { - SourceInteractionHandler(httpClient).getPopularManga(source, pageNum.value) + sourceHandler.getPopularManga(source, pageNum.value) } } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceTabs.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceTabs.kt index ff129de8..6bd06a5e 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceTabs.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceTabs.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import ca.gosyer.backend.models.Source +import ca.gosyer.data.models.Source @Composable fun SourceTopBar( diff --git a/src/main/kotlin/ca/gosyer/util/compose/Flow.kt b/src/main/kotlin/ca/gosyer/util/compose/Flow.kt new file mode 100644 index 00000000..2b45d9da --- /dev/null +++ b/src/main/kotlin/ca/gosyer/util/compose/Flow.kt @@ -0,0 +1,19 @@ +/* + * 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.util.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import kotlinx.coroutines.flow.StateFlow +import kotlin.reflect.KProperty + +@Composable +operator fun StateFlow.getValue(thisObj: Any?, property: KProperty<*>): T { + val item by collectAsState() + return item +} diff --git a/src/main/kotlin/ca/gosyer/util/compose/Image.kt b/src/main/kotlin/ca/gosyer/util/compose/Image.kt index 549fec2e..8cb53ba4 100644 --- a/src/main/kotlin/ca/gosyer/util/compose/Image.kt +++ b/src/main/kotlin/ca/gosyer/util/compose/Image.kt @@ -8,7 +8,7 @@ package ca.gosyer.util.compose import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import io.ktor.client.HttpClient +import ca.gosyer.data.server.Http import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.readBytes @@ -19,6 +19,6 @@ fun imageFromFile(file: File): ImageBitmap { return Image.makeFromEncoded(file.readBytes()).asImageBitmap() } -suspend fun imageFromUrl(client: HttpClient, url: String): ImageBitmap { +suspend fun imageFromUrl(client: Http, url: String): ImageBitmap { return Image.makeFromEncoded(client.get(url).readBytes()).asImageBitmap() } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/util/compose/Theme.kt b/src/main/kotlin/ca/gosyer/util/compose/Theme.kt index caa7106f..64eb6e95 100644 --- a/src/main/kotlin/ca/gosyer/util/compose/Theme.kt +++ b/src/main/kotlin/ca/gosyer/util/compose/Theme.kt @@ -13,8 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.MenuBar -import ca.gosyer.backend.preferences.PreferenceHelper -import ca.gosyer.util.system.get import java.awt.image.BufferedImage fun ThemedWindow( @@ -31,7 +29,7 @@ fun ThemedWindow( content: @Composable () -> Unit = { } ) { Window(title, size, location, centered, icon, menuBar, undecorated, resizable, events, onDismissRequest) { - DesktopMaterialTheme(get().getTheme()) { + DesktopMaterialTheme { content() } } diff --git a/src/main/resources/Log4j-config.xsd b/src/main/resources/Log4j-config.xsd new file mode 100644 index 00000000..1b8ef64c --- /dev/null +++ b/src/main/resources/Log4j-config.xsd @@ -0,0 +1,1341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The Advertiser plugin name which will be used to advertise individual FileAppender or SocketAppender configurations. The only + Advertiser plugin provided is 'multicastdns". + + + + + + Either "err" for stderr, "out" for stdout, a file path, or a URL. + + + + + The minimum amount of time, in seconds, that must elapse before the file configuration is checked for changes. + + + + + The name of the configuration. + + + + + A comma separated list of package names to search for plugins. Plugins are only loaded once per classloader so changing this value + may not have any effect upon reconfiguration. + + + + + + Identifies the location for the classloader to located the XML Schema to use to validate the configuration. Only valid when strict + is set to true. If not set no schema validation will take place. + + + + + + Specifies whether or not Log4j should automatically shutdown when the JVM shuts down. The shutdown hook is enabled by default but + may be disabled by setting this attribute to "disable". + + + + + + + + + + + Specifies how many milliseconds appenders and background tasks will get to shutdown when the JVM shuts down. Default is zero which + mean that each appender uses its default timeout, and don't wait for background tasks. + + + + + + The level of internal Log4j events that should be logged to the console. Valid values for this attribute are "trace", "debug", + "info", "warn", "error" and "fatal". + + + + + + + + + + + Enables the use of the strict XML format. Not supported in JSON configurations. + + + + + Enables diagnostic information while loading plugins. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Action to take when the filter matches. May be ACCEPT, DENY or NEUTRAL. + + + + + Action to take when the filter does not match. May be ACCEPT, DENY or NEUTRAL. + + + + + + + + + + + + + + + + + + + + + + + One or more KeyValuePair elements that define the matching value for the key and the Level to evaluate when the key matches. + + + + + + + + + + + + + + The average number of events per second to allow. + + + + + The maximum number of events that can occur before events are filtered for exceeding the average rate. The default is 10 times + the rate. + + + + + + Level of messages to be filtered. Anything at or below this level will be filtered out if maxBurst has been exceeded. The + default is WARN meaning any messages that are higher than warn will be logged regardless of the size of a burst. + + + + + + + + + + + + + + + + The filter type, see https://logging.apache.org/log4j/2.x/manual/filters.html + + + + + + + + + + + + + The name of the item in the ThreadContext Map to compare. + + + + + + Level of messages to be filtered. The default threshold only applies if the log event contains the specified ThreadContext Map + item and its value does not match any key in the key/value pairs. + + + + + + + + + + + + + + If the operator is "or" then a match by any one of the key/value pairs will be considered to be a match, otherwise all the + key/value pairs must match. + + + + + + + + + + + + + + + + + + + + + + + + + + + The regular expression. + + + + + If true the unformatted message will be used, otherwise the formatted message will be used. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If the operator is "or" then a match by any one of the key/value pairs will be considered to be a match, otherwise all the + key/value pairs must match. + + + + + + + + + + + + + A valid Level name to match on. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + If true exceptions are always written even if the pattern contains no exception conversions. + + + + + The character set to use when converting the syslog String to a byte array. + + + + + If true, do not output ANSI escape codes. + + + + + Footer string to include at the bottom of each log file. + + + + + Header string to include at the top of each log file. + + + + + If true and System.console() is null, do not output ANSI escape codes. + + + + + + + + + + + + + + + + + + + + One or more KeyValuePair elements that define custom field in the output. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One or more KeyValuePair elements that define custom field in the output. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The name of the appender. + + + + + + The default is true, causing exceptions encountered while appending events to be internally logged and then ignored. + When set to false + exceptions will be propagated to the caller, instead. + You must set this to false when wrapping this Appender in a FailoverAppender. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Write directly to java.io.FileDescriptor and bypass java.lang.System.out/.err. Can give up to 10x performance boost when the + output is redirected to file or other process. Cannot be used with Jansi on Windows. Cannot be used with follow. + + + + + + + Identifies whether the appender honors reassignments of System.out or System.err via System.setOut or System.setErr made after + configuration. Note that the follow attribute cannot be used with Jansi on Windows. Cannot be used with direct. + + + + + + Default is SYSTEM_OUT. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One or more KeyValuePair elements that define the matching value for the key and the Level to evaluate when the + key + matches. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + When true, records will be appended to the end of the file. + When set to false, the file will be cleared before new records are + written. + + + + + + + + + The name of the file to write to. If the file, or any of its parent directories, do not exist, they will be created. + + + + + + + When set to true, each write will be followed by a flush. + This will guarantee the data is written to disk but could impact + performance. + + + + + + + + + + + + + + + When true, records will be written to a buffer and the data will be written to disk when the buffer is full or, if + immediateFlush is set, when the record is written. + File locking cannot be used with bufferedIO. + + + + + + When bufferedIO is true, this is the buffer size, the default is 8192 bytes. + + + + + + + + + + + + + + The appender creates the file on-demand. The appender only creates the file when a log event passes all filters and is routed + to this appender. + + + + + + + File attribute permissions in POSIX format to apply whenever the file is created. + Underlying files system shall support POSIX + file attribute view. + + + + + + + File owner to define whenever the file is created. + Underlying files system shall support POSIX file attribute view. + + + + + + + File group to define whenever the file is created. + Underlying files system shall support POSIX file attribute view. + + + + + + + + + + + + + + The length of the mapped region, defaults to 32 MB (32 * 1024 * 1024 bytes). This parameter must be a value between 256 and + 1,073,741,824 (1 GB or 2^30); values outside this range will be adjusted to the closest valid value. Log4j will round the specified value + up to the nearest power of two. + + + + + + + + + + + + + + + + + + + + + + The pattern of the file name of the archived log file. + The format of the pattern is dependent on the RolloverPolicy that is + used. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The cron expression. The expression is the same as what is allowed in the Quartz scheduler. + + + + + + On startup the cron expression will be evaluated against the file's last modification timestamp. If the cron expression indicates a + rollover should have occurred between that time and the current time the file will be immediately rolled over. + + + + + + + + + + The minimum size the file must have to roll over. A size of zero will cause a roll over no matter what the file size is. The default + value is 1, which will prevent rolling over an empty file. + + + + + + + + + + The size can be specified in bytes, with the suffix KB, MB or GB, for example 20MB. + + + + + + + + + + How often a rollover should occur based on the most specific time unit in the date pattern. + + + + + + + Indicates whether the interval should be adjusted to cause the next rollover to occur on the interval boundary. + + + + + + + Indicates the maximum number of seconds to randomly delay a rollover. By default, this is 0 which indicates no delay. + + + + + + + + + + + + + + + Sets the compression level, 0-9, where 0 = none, 1 = best speed, through 9 = best compression. Only implemented for ZIP files. + + + + + + + If set to "max", files with a higher index will be newer than files with a smaller index. + If set to "min", file renaming and the + counter will follow the Fixed Window strategy described above. + + + + + + + + + + + + + + The maximum value of the counter. Once this values is reached older archives will be deleted on subsequent rollovers. + + + + + + The minimum value of the counter. + + + + + + The pattern of the file name of the archived log file during compression. + + + + + + + + + + + + Sets the compression level, 0-9, where 0 = none, 1 = best speed, through 9 = best compression. Only implemented for ZIP files. + + + + + + + The maximum number of files to allow in the time period matching the file pattern. If the number of files is exceeded the oldest file + will be deleted. If specified, the value must be greater than 1. If the value is less than zero or omitted then the number of files will not be + limited. + + + + + + The pattern of the file name of the archived log file during compression. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Allow boolean values or variable place holders in the form of ${variablename} + + + + + + + + + Allow long values or variable place holders in the form of ${variablename} + + + + + + diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 00000000..95249865 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 47543e7b..00000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - %highlight(%d{HH:mm:ss.SSS} [%thread] %level/%logger{0}: %msg%n) - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/kotlin/ca/gosyer/backend/network/ExtensionInteractionTest.kt b/src/test/kotlin/ca/gosyer/data/server/ExtensionInteractionTest.kt similarity index 76% rename from src/test/kotlin/ca/gosyer/backend/network/ExtensionInteractionTest.kt rename to src/test/kotlin/ca/gosyer/data/server/ExtensionInteractionTest.kt index cf61a97f..c815a6e8 100644 --- a/src/test/kotlin/ca/gosyer/backend/network/ExtensionInteractionTest.kt +++ b/src/test/kotlin/ca/gosyer/data/server/ExtensionInteractionTest.kt @@ -1,4 +1,4 @@ -package ca.gosyer.backend.network +package ca.gosyer.data.server import kotlin.test.Test