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