mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Rewrite backend
- Use Tachiyomi 1.x Preference backend - Switch DI from Koin to Toothpick - Use gradle BuildConfig library to move variables from gradle to the App - Switch from Logback to Log4j2 with slf4j implmenetation - Try to use the same java as the application for the server - Add Run Debug run configuration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,6 +19,6 @@ bin/
|
||||
*.ipr
|
||||
*.iws
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
!/.idea/runConfigurations/
|
||||
out/
|
||||
workspace.xml
|
||||
23
.run/TachideskJUI [run].run.xml
Normal file
23
.run/TachideskJUI [run].run.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="TachideskJUI Run Debug" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="-PdebugApp" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="run" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -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
|
||||
@@ -106,3 +110,13 @@ compose.desktop {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
appName = project.name
|
||||
version = project.version.toString()
|
||||
|
||||
clsName = "BuildConfig"
|
||||
packageName = project.group.toString()
|
||||
|
||||
buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString())
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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<Boolean> {
|
||||
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<Boolean> {
|
||||
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
|
||||
)
|
||||
@@ -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<Double> {
|
||||
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<Double> {
|
||||
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
|
||||
)
|
||||
@@ -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<Float> {
|
||||
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<Float> {
|
||||
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
|
||||
)
|
||||
@@ -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<Int> {
|
||||
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<Int> {
|
||||
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
|
||||
)
|
||||
@@ -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<T>(
|
||||
override val settings: ObservableSettings,
|
||||
override val key: String,
|
||||
override val default: T,
|
||||
private val serializer: KSerializer<T>
|
||||
): DefaultPreference<T> {
|
||||
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<T>(
|
||||
override val settings: ObservableSettings,
|
||||
override val key: String,
|
||||
private val serializer: KSerializer<T>
|
||||
): NullPreference<T> {
|
||||
override fun get() = settings.decodeValueOrNull(serializer, key)
|
||||
override fun asFLow() = settings.createFlow<T?>(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 <T> ObservableSettings.getJsonPreference(key: String, default: T, serializer: KSerializer<T>) = JsonPreference(
|
||||
this,
|
||||
key,
|
||||
default,
|
||||
serializer
|
||||
)
|
||||
|
||||
fun <T> ObservableSettings.getJsonPreference(key: String, serializer: KSerializer<T>) = JsonNullPreference(
|
||||
this,
|
||||
key,
|
||||
serializer
|
||||
)
|
||||
|
||||
private inline fun <T> ObservableSettings.createFlow(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
crossinline getter: Settings.(String, T) -> T
|
||||
): Flow<T> = callbackFlow {
|
||||
offer(getter(key, defaultValue))
|
||||
val listener = addListener(key) {
|
||||
offer(getter(key, defaultValue))
|
||||
}
|
||||
awaitClose {
|
||||
listener.deactivate()
|
||||
}
|
||||
}
|
||||
@@ -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<Long> {
|
||||
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<Long> {
|
||||
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
|
||||
)
|
||||
@@ -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 <T>: 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<T?>
|
||||
|
||||
/**
|
||||
* See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting
|
||||
*/
|
||||
fun asStateFlow(scope: CoroutineScope): StateFlow<T?> = 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 <T>: 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<T>
|
||||
|
||||
/**
|
||||
* See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting
|
||||
*/
|
||||
fun asStateFlow(scope: CoroutineScope): StateFlow<T> = asFLow().asStateFlow(get(), scope, true)
|
||||
/**
|
||||
* Stores the [T] [value] at [key].
|
||||
*/
|
||||
fun set(value: T)
|
||||
}
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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
|
||||
)
|
||||
31
src/main/kotlin/ca/gosyer/common/di/AppScope.kt
Normal file
31
src/main/kotlin/ca/gosyer/common/di/AppScope.kt
Normal file
@@ -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 <reified T> getInstance(): T {
|
||||
return getInstance(T::class.java)
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt
Normal file
17
src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt
Normal file
@@ -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<T>(private val cls: Class<T>, val scope: Scope = AppScope) : Provider<T> {
|
||||
|
||||
override fun get(): T {
|
||||
return scope.getInstance(cls)
|
||||
}
|
||||
}
|
||||
16
src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt
Normal file
16
src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.common.di
|
||||
|
||||
import toothpick.config.Module
|
||||
|
||||
/**
|
||||
* Binds the given [instance] to its class.
|
||||
*/
|
||||
inline fun <reified B> Module.bindInstance(instance: B) {
|
||||
bind(B::class.java).toInstance(instance)
|
||||
}
|
||||
54
src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt
Normal file
54
src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt
Normal file
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt
Normal file
32
src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt
Normal file
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
) : PreferenceStore {
|
||||
|
||||
/**
|
||||
* Returns an [String] preference for this [key].
|
||||
*/
|
||||
override fun getString(key: String, defaultValue: String): Preference<String> {
|
||||
return lazyStore.value.getString(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Long] preference for this [key].
|
||||
*/
|
||||
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
|
||||
return lazyStore.value.getLong(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Int] preference for this [key].
|
||||
*/
|
||||
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
|
||||
return lazyStore.value.getInt(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Float] preference for this [key].
|
||||
*/
|
||||
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
|
||||
return lazyStore.value.getFloat(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] preference for this [key].
|
||||
*/
|
||||
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
|
||||
return lazyStore.value.getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Set<String>] preference for this [key].
|
||||
*/
|
||||
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||
return lazyStore.value.getStringSet(key, defaultValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||
* must be provided.
|
||||
*/
|
||||
override fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T
|
||||
): Preference<T> {
|
||||
return lazyStore.value.getObject(key, defaultValue, serializer, deserializer)
|
||||
}
|
||||
|
||||
override fun <T> getJsonObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: KSerializer<T>,
|
||||
serializersModule: SerializersModule
|
||||
): Preference<T> {
|
||||
return lazyStore.value.getJsonObject(key, defaultValue, serializer)
|
||||
}
|
||||
}
|
||||
61
src/main/kotlin/ca/gosyer/common/prefs/Preference.kt
Normal file
61
src/main/kotlin/ca/gosyer/common/prefs/Preference.kt
Normal file
@@ -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<T> {
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
|
||||
/**
|
||||
* 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<T>
|
||||
|
||||
}
|
||||
87
src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt
Normal file
87
src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt
Normal file
@@ -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<String>
|
||||
|
||||
/**
|
||||
* Returns a [Long] preference for this [key].
|
||||
*/
|
||||
fun getLong(key: String, defaultValue: Long = 0): Preference<Long>
|
||||
|
||||
/**
|
||||
* Returns an [Int] preference for this [key].
|
||||
*/
|
||||
fun getInt(key: String, defaultValue: Int = 0): Preference<Int>
|
||||
|
||||
/**
|
||||
* Returns a [Float] preference for this [key].
|
||||
*/
|
||||
fun getFloat(key: String, defaultValue: Float = 0f): Preference<Float>
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] preference for this [key].
|
||||
*/
|
||||
fun getBoolean(key: String, defaultValue: Boolean = false): Preference<Boolean>
|
||||
|
||||
/**
|
||||
* Returns a [Set<String>] preference for this [key].
|
||||
*/
|
||||
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||
|
||||
/**
|
||||
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||
* must be provided.
|
||||
*/
|
||||
fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T
|
||||
): Preference<T>
|
||||
|
||||
|
||||
/**
|
||||
* Returns preference of type [T] for this [key]. The [serializer] must be provided.
|
||||
*/
|
||||
fun <T> getJsonObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: KSerializer<T>,
|
||||
serializersModule: SerializersModule = EmptySerializersModule
|
||||
): Preference<T>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an enum preference of type [T] for this [key].
|
||||
*/
|
||||
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||
key: String,
|
||||
defaultValue: T
|
||||
): Preference<T> {
|
||||
return getObject(key, defaultValue, { it.name }, {
|
||||
try {
|
||||
enumValueOf(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
defaultValue
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 <reified T: Any> get() = GlobalContext.get().get<T>()
|
||||
fun String.decodeBase64() = decodeBase64()!!
|
||||
|
||||
inline fun <reified T: Any> inject() = GlobalContext.get().inject<T>()
|
||||
fun String.md5() = encode().md5().hex()
|
||||
@@ -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 <T> List<T>.replace(position: Int, newItem: T): List<T> {
|
||||
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 <T> List<T>.replaceFirst(predicate: (T) -> Boolean, newItem: T): List<T> {
|
||||
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 <T> MutableCollection<T>.removeFirst(predicate: (T) -> Boolean): T? {
|
||||
val iter = iterator()
|
||||
while (iter.hasNext()) {
|
||||
val element = iter.next()
|
||||
if (predicate(element)) {
|
||||
iter.remove()
|
||||
return element
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
43
src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt
Normal file
43
src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
99
src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt
Normal file
99
src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt
Normal file
@@ -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<T>(
|
||||
private val preferences: ObservableSettings,
|
||||
private val key: String,
|
||||
private val defaultValue: T,
|
||||
private val adapter: Adapter<T>
|
||||
) : Preference<T> {
|
||||
|
||||
interface Adapter<T> {
|
||||
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<T> {
|
||||
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<T> {
|
||||
return changes().stateIn(scope, SharingStarted.Eagerly, get())
|
||||
}
|
||||
|
||||
}
|
||||
107
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt
Normal file
107
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt
Normal file
@@ -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<String> {
|
||||
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<Long> {
|
||||
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<Int> {
|
||||
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<Float> {
|
||||
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<Boolean> {
|
||||
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<Set<String>> {
|
||||
override fun get(key: String, preferences: ObservableSettings): Set<String> {
|
||||
return preferences.decodeValue(SetSerializer(String.serializer()), key, emptySet()) // Not called unless key is present.
|
||||
}
|
||||
|
||||
override fun set(key: String, value: Set<String>, editor: ObservableSettings) {
|
||||
editor.encodeValue(SetSerializer(String.serializer()), key, value)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ObjectAdapter<T>(
|
||||
private val serializer: (T) -> String,
|
||||
private val deserializer: (String) -> T
|
||||
) : JvmPreference.Adapter<T> {
|
||||
|
||||
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<T>(
|
||||
private val defaultValue: T,
|
||||
private val serializer: KSerializer<T>,
|
||||
private val serializersModule: SerializersModule = EmptySerializersModule
|
||||
) : JvmPreference.Adapter<T> {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
86
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt
Normal file
86
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt
Normal file
@@ -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<String> {
|
||||
return JvmPreference(preferences, key, defaultValue, StringAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Long] preference for this [key].
|
||||
*/
|
||||
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
|
||||
return JvmPreference(preferences, key, defaultValue, LongAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Int] preference for this [key].
|
||||
*/
|
||||
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
|
||||
return JvmPreference(preferences, key, defaultValue, IntAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Float] preference for this [key].
|
||||
*/
|
||||
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
|
||||
return JvmPreference(preferences, key, defaultValue, FloatAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] preference for this [key].
|
||||
*/
|
||||
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
|
||||
return JvmPreference(preferences, key, defaultValue, BooleanAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Set<String>] preference for this [key].
|
||||
*/
|
||||
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||
return JvmPreference(preferences, key, defaultValue, StringSetAdapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||
* must be provided.
|
||||
*/
|
||||
override fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T
|
||||
): Preference<T> {
|
||||
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 <T> getJsonObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: KSerializer<T>,
|
||||
serializersModule: SerializersModule
|
||||
): Preference<T> {
|
||||
val adapter = JsonObjectAdapter(defaultValue, serializer, serializersModule)
|
||||
return JvmPreference(preferences, key, defaultValue, adapter)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
67
src/main/kotlin/ca/gosyer/data/DataModule.kt
Normal file
67
src/main/kotlin/ca/gosyer/data/DataModule.kt
Normal file
@@ -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<ServerPreferences>()
|
||||
.toProviderInstance { ServerPreferences(preferenceFactory.create("server")) }
|
||||
.providesSingleton()
|
||||
|
||||
bind<ExtensionPreferences>()
|
||||
.toProviderInstance { ExtensionPreferences(preferenceFactory.create("extension")) }
|
||||
.providesSingleton()
|
||||
|
||||
bind<CatalogPreferences>()
|
||||
.toProviderInstance { CatalogPreferences(preferenceFactory.create("catalog")) }
|
||||
.providesSingleton()
|
||||
|
||||
bind<LibraryPreferences>()
|
||||
.toProviderInstance { LibraryPreferences(preferenceFactory.create("library")) }
|
||||
.providesSingleton()
|
||||
|
||||
bind<UiPreferences>()
|
||||
.toProviderInstance { UiPreferences(preferenceFactory.create("ui")) }
|
||||
.providesSingleton()
|
||||
|
||||
bind<Http>()
|
||||
.toProvider(HttpProvider::class)
|
||||
.providesSingleton()
|
||||
|
||||
bind<CategoryInteractionHandler>()
|
||||
.toClass<CategoryInteractionHandler>()
|
||||
bind<ChapterInteractionHandler>()
|
||||
.toClass<ChapterInteractionHandler>()
|
||||
bind<ExtensionInteractionHandler>()
|
||||
.toClass<ExtensionInteractionHandler>()
|
||||
bind<LibraryInteractionHandler>()
|
||||
.toClass<LibraryInteractionHandler>()
|
||||
bind<MangaInteractionHandler>()
|
||||
.toClass<MangaInteractionHandler>()
|
||||
bind<SourceInteractionHandler>()
|
||||
.toClass<SourceInteractionHandler>()
|
||||
}
|
||||
16
src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt
Normal file
16
src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.catalog
|
||||
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.common.prefs.PreferenceStore
|
||||
|
||||
class CatalogPreferences(private val preferenceStore: PreferenceStore) {
|
||||
fun languages(): Preference<Set<String>> {
|
||||
return preferenceStore.getStringSet("enabled_langs", setOf("en"))
|
||||
}
|
||||
}
|
||||
@@ -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<Set<String>> {
|
||||
return preferenceStore.getStringSet("enabled_langs", setOf("all", "en"))
|
||||
}
|
||||
}
|
||||
18
src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt
Normal file
18
src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt
Normal file
@@ -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<DisplayMode> {
|
||||
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
|
||||
}
|
||||
}
|
||||
20
src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt
Normal file
20
src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Http> {
|
||||
override fun get(): Http {
|
||||
return HttpClient(OkHttp) {
|
||||
install(JsonFeature)
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
16
src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt
Normal file
16
src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.server
|
||||
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.common.prefs.PreferenceStore
|
||||
|
||||
class ServerPreferences(private val preferenceStore: PreferenceStore) {
|
||||
fun server(): Preference<String> {
|
||||
return preferenceStore.getString("server_url", "http://localhost:4567")
|
||||
}
|
||||
}
|
||||
@@ -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 <reified T> HttpClient.getRepeat(
|
||||
protected suspend inline fun <reified T> Http.getRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
@@ -41,7 +43,7 @@ open class BaseInteractionHandler {
|
||||
throw lastException
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> HttpClient.deleteRepeat(
|
||||
protected suspend inline fun <reified T> Http.deleteRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
@@ -59,7 +61,7 @@ open class BaseInteractionHandler {
|
||||
throw lastException
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> HttpClient.patchRepeat(
|
||||
protected suspend inline fun <reified T> Http.patchRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
@@ -77,7 +79,7 @@ open class BaseInteractionHandler {
|
||||
throw lastException
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> HttpClient.postRepeat(
|
||||
protected suspend inline fun <reified T> Http.postRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
@@ -95,7 +97,7 @@ open class BaseInteractionHandler {
|
||||
throw lastException
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> HttpClient.submitFormRepeat(
|
||||
protected suspend inline fun <reified T> 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 {
|
||||
@@ -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<List<Category>>(
|
||||
@@ -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<List<Chapter>>(
|
||||
@@ -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<List<Extension>>(
|
||||
@@ -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<List<Manga>>(
|
||||
@@ -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<Manga>(
|
||||
@@ -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<List<Source>>(
|
||||
@@ -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) =
|
||||
@@ -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) =
|
||||
@@ -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() =
|
||||
@@ -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) =
|
||||
@@ -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) =
|
||||
@@ -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
|
||||
|
||||
@@ -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() =
|
||||
64
src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt
Normal file
64
src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt
Normal file
@@ -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<ThemeMode> {
|
||||
return preferenceStore.getEnum("theme_mode", ThemeMode.System)
|
||||
}
|
||||
|
||||
fun lightTheme(): Preference<Int> {
|
||||
return preferenceStore.getInt("theme_light", 0)
|
||||
}
|
||||
|
||||
fun darkTheme(): Preference<Int> {
|
||||
return preferenceStore.getInt("theme_dark", 0)
|
||||
}
|
||||
|
||||
fun colorPrimaryLight(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_primary_light", 0)
|
||||
}
|
||||
|
||||
fun colorPrimaryDark(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_primary_dark", 0)
|
||||
}
|
||||
|
||||
fun colorSecondaryLight(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_secondary_light", 0)
|
||||
}
|
||||
|
||||
fun colorSecondaryDark(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_secondary_dark", 0)
|
||||
}
|
||||
|
||||
fun colorBarsLight(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_bar_light", 0)
|
||||
}
|
||||
|
||||
fun colorBarsDark(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_bar_dark", 0)
|
||||
}
|
||||
|
||||
fun confirmExit(): Preference<Boolean> {
|
||||
return preferenceStore.getBoolean("confirm_exit", false)
|
||||
}
|
||||
|
||||
fun language(): Preference<String> {
|
||||
return preferenceStore.getString("language", "")
|
||||
}
|
||||
|
||||
fun dateFormat(): Preference<String> {
|
||||
return preferenceStore.getString("date_format", "")
|
||||
}
|
||||
|
||||
}
|
||||
13
src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt
Normal file
13
src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.data.ui.model
|
||||
|
||||
enum class ThemeMode {
|
||||
System,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<ImageBitmap?> = remember { mutableStateOf(null) }
|
||||
val loading: MutableState<Boolean> = remember { mutableStateOf(true) }
|
||||
val error: MutableState<String?> = 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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 <reified VM : ViewModel> composeViewModel(): VM {
|
||||
inline fun <reified VM : ViewModel> viewModel(): VM {
|
||||
val viewModel = remember {
|
||||
GlobalContext.get().get<VM>()
|
||||
AppScope.getInstance<VM>()
|
||||
}
|
||||
DisposableEffect(viewModel) {
|
||||
onDispose {
|
||||
@@ -23,3 +27,26 @@ inline fun <reified VM : ViewModel> composeViewModel(): VM {
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified VM : ViewModel> 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<VM>()
|
||||
Pair(viewModel, submodule)
|
||||
}
|
||||
DisposableEffect(viewModel) {
|
||||
onDispose {
|
||||
viewModel.destroy()
|
||||
Toothpick.closeScope(submodule)
|
||||
}
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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<CategoriesMenuViewModel>()
|
||||
val vm = viewModel<CategoriesMenuViewModel>()
|
||||
val categories by vm.categories.collectAsState()
|
||||
remember {
|
||||
windowEvents.onClose = { vm.updateCategories() }
|
||||
|
||||
@@ -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<Category>()
|
||||
private val _categories = MutableStateFlow(emptyList<MenuCategory>())
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ExtensionsMenuViewModel>()
|
||||
val vm = viewModel<ExtensionsMenuViewModel>()
|
||||
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 {
|
||||
|
||||
@@ -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<Extension>())
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<LibraryScreenViewModel>()
|
||||
val vm = viewModel<LibraryScreenViewModel>()
|
||||
val categories by vm.categories.collectAsState()
|
||||
val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState()
|
||||
val displayMode by vm.displayMode.collectAsState()
|
||||
@@ -170,13 +170,3 @@ private fun LibraryPager(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class DisplayMode {
|
||||
@Serializable
|
||||
object List : DisplayMode()
|
||||
@Serializable
|
||||
object CompactGrid : DisplayMode()
|
||||
@Serializable
|
||||
object ComfortableGrid : DisplayMode()
|
||||
}
|
||||
@@ -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<Int, MutableStateFlow<List<Manga>>>
|
||||
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap)
|
||||
@@ -30,11 +30,13 @@ private fun LibraryMap.setManga(order: Int, manga: List<Manga>) {
|
||||
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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
package ca.gosyer.ui.main
|
||||
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainViewModel : ViewModel()
|
||||
class MainViewModel @Inject constructor(): ViewModel()
|
||||
@@ -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<MainViewModel>()
|
||||
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<MainViewModel>()
|
||||
Surface {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Button(
|
||||
onClick = ::openExtensionsMenu
|
||||
) {
|
||||
Text("Extensions")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openSourcesMenu
|
||||
) {
|
||||
Text("Sources")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openLibraryMenu
|
||||
) {
|
||||
Text("Library")
|
||||
}
|
||||
Button(
|
||||
onClick = ::openCategoriesMenu
|
||||
) {
|
||||
Text("Categories")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MangaMenuViewModel>()
|
||||
val vm = viewModel<MangaMenuViewModel>()
|
||||
remember(mangaId) {
|
||||
vm.init(mangaId)
|
||||
}
|
||||
@@ -71,7 +70,7 @@ fun MangaMenu(mangaId: Long) {
|
||||
|
||||
@Composable
|
||||
fun MangaMenu(manga: Manga) {
|
||||
val vm = composeViewModel<MangaMenuViewModel>()
|
||||
val vm = viewModel<MangaMenuViewModel>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Manga?>(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()
|
||||
|
||||
@@ -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<SourcesMenuViewModel>()
|
||||
val vm = viewModel<SourcesMenuViewModel>()
|
||||
val isLoading by vm.isLoading.collectAsState()
|
||||
val sources by vm.sources.collectAsState()
|
||||
val sourceTabs by vm.sourceTabs.collectAsState()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<SourceScreenViewModel>()
|
||||
val vm = viewModel<SourceScreenViewModel>()
|
||||
remember(source) {
|
||||
vm.init(source)
|
||||
}
|
||||
|
||||
@@ -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<Manga>())
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
19
src/main/kotlin/ca/gosyer/util/compose/Flow.kt
Normal file
19
src/main/kotlin/ca/gosyer/util/compose/Flow.kt
Normal file
@@ -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 <T> StateFlow<T>.getValue(thisObj: Any?, property: KProperty<*>): T {
|
||||
val item by collectAsState()
|
||||
return item
|
||||
}
|
||||
@@ -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<HttpResponse>(url).readBytes()).asImageBitmap()
|
||||
}
|
||||
@@ -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<PreferenceHelper>().getTheme()) {
|
||||
DesktopMaterialTheme {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
1341
src/main/resources/Log4j-config.xsd
Normal file
1341
src/main/resources/Log4j-config.xsd
Normal file
File diff suppressed because it is too large
Load Diff
18
src/main/resources/log4j2.xml
Normal file
18
src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://logging.apache.org/log4j/2.0/config"
|
||||
xsi:noNamespaceSchemaLocation="Log4j-config.xsd">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout disableAnsi="false" pattern="%highlight{%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}} [%t] ${LOG_LEVEL_PATTERN:-%p}/%c{1}: %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%xEx}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=black, DEBUG=black, TRACE=black}" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="debug">
|
||||
<AppenderRef ref="Console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -1,38 +0,0 @@
|
||||
<configuration debug="true">
|
||||
|
||||
<property name="HOME_LOG" value="logs/app.log"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!--<withJansi>true</withJansi>-->
|
||||
<encoder>
|
||||
<pattern>%highlight(%d{HH:mm:ss.SSS} [%thread] %level/%logger{0}: %msg%n)</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!--<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>-->
|
||||
|
||||
<!--<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${HOME_LOG}</file>
|
||||
<append>true</append>
|
||||
<immediateFlush>true</immediateFlush>
|
||||
<encoder>
|
||||
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
|
||||
</encoder>
|
||||
</appender>-->
|
||||
|
||||
<!--<logger name="ca.gosyer" level="debug" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<!–<appender-ref ref="FILE"/>–>
|
||||
</logger>-->
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -1,4 +1,4 @@
|
||||
package ca.gosyer.backend.network
|
||||
package ca.gosyer.data.server
|
||||
|
||||
import kotlin.test.Test
|
||||
|
||||
Reference in New Issue
Block a user