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:
Syer10
2021-04-21 16:07:50 -04:00
parent f66951bbb5
commit 57ff9095a8
87 changed files with 2812 additions and 839 deletions

2
.gitignore vendored
View File

@@ -19,6 +19,6 @@ bin/
*.ipr *.ipr
*.iws *.iws
/.idea/* /.idea/*
!/.idea/runConfigurations !/.idea/runConfigurations/
out/ out/
workspace.xml workspace.xml

View 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>

View File

@@ -3,16 +3,17 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.4.31" kotlin("jvm") version "1.4.32"
kotlin("plugin.serialization") version "1.4.31" kotlin("kapt") version "1.4.32"
id("org.jetbrains.compose") version "0.4.0-build177" 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" group = "ca.gosyer"
version = "1.0.0" version = "1.0.0"
repositories { repositories {
jcenter()
mavenCentral() mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 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") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
// Dependency Injection // 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 // Http client
val ktorVersion = "1.5.2" val ktorVersion = "1.5.2"
@@ -38,8 +40,10 @@ dependencies {
implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion")
// Logging // Logging
implementation("ch.qos.logback:logback-classic:1.2.3") val log4jVersion = "2.14.1"
//implementation("org.fusesource.jansi:jansi:1.18") 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") implementation("io.github.microutils:kotlin-logging:2.0.5")
// User storage // 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())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}
}

View 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)
}
}

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.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)
}

View 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() {
}
}

View 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) }
}
}
}

View File

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

View 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>
}

View 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
}
})
}

View File

@@ -4,10 +4,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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()

View File

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

View 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")
}
}

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

View 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)
}
}

View 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)
}
}

View File

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

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

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.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"))
}
}

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.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"))
}
}

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

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

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -21,4 +21,6 @@ data class Chapter(
val scanlator: String?, val scanlator: String?,
val mangaId: Long, val mangaId: Long,
val pageCount: Int? = null, val pageCount: Int? = null,
val chapterIndex: Int,
val chapterCount: Int
) )

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 import kotlinx.serialization.Serializable

View File

@@ -4,18 +4,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.HttpClient
import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.logging.LogLevel import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logging import io.ktor.client.features.logging.Logging
import org.koin.dsl.module import javax.inject.Inject
import javax.inject.Provider
val networkModule = module { typealias Http = HttpClient
single {
HttpClient(OkHttp) { internal class HttpProvider @Inject constructor() : Provider<Http> {
override fun get(): Http {
return HttpClient(OkHttp) {
install(JsonFeature) install(JsonFeature)
install(Logging) { install(Logging) {
level = LogLevel.INFO level = LogLevel.INFO

View File

@@ -0,0 +1,16 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.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")
}
}

View File

@@ -4,12 +4,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 androidx.compose.ui.graphics.ImageBitmap
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.Http
import ca.gosyer.util.system.inject import ca.gosyer.data.server.ServerPreferences
import io.ktor.client.HttpClient
import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete import io.ktor.client.request.delete
import io.ktor.client.request.forms.submitForm import io.ktor.client.request.forms.submitForm
@@ -19,11 +18,14 @@ import io.ktor.client.request.post
import io.ktor.http.Parameters import io.ktor.http.Parameters
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
open class BaseInteractionHandler { open class BaseInteractionHandler(
val preferences: PreferenceHelper by inject() protected val client: Http,
val serverUrl get() = preferences.serverUrl.get() 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, urlString: String,
block: HttpRequestBuilder.() -> Unit = {} block: HttpRequestBuilder.() -> Unit = {}
): T { ): T {
@@ -41,7 +43,7 @@ open class BaseInteractionHandler {
throw lastException throw lastException
} }
protected suspend inline fun <reified T> HttpClient.deleteRepeat( protected suspend inline fun <reified T> Http.deleteRepeat(
urlString: String, urlString: String,
block: HttpRequestBuilder.() -> Unit = {} block: HttpRequestBuilder.() -> Unit = {}
): T { ): T {
@@ -59,7 +61,7 @@ open class BaseInteractionHandler {
throw lastException throw lastException
} }
protected suspend inline fun <reified T> HttpClient.patchRepeat( protected suspend inline fun <reified T> Http.patchRepeat(
urlString: String, urlString: String,
block: HttpRequestBuilder.() -> Unit = {} block: HttpRequestBuilder.() -> Unit = {}
): T { ): T {
@@ -77,7 +79,7 @@ open class BaseInteractionHandler {
throw lastException throw lastException
} }
protected suspend inline fun <reified T> HttpClient.postRepeat( protected suspend inline fun <reified T> Http.postRepeat(
urlString: String, urlString: String,
block: HttpRequestBuilder.() -> Unit = {} block: HttpRequestBuilder.() -> Unit = {}
): T { ): T {
@@ -95,7 +97,7 @@ open class BaseInteractionHandler {
throw lastException throw lastException
} }
protected suspend inline fun <reified T> HttpClient.submitFormRepeat( protected suspend inline fun <reified T> Http.submitFormRepeat(
urlString: String, urlString: String,
formParameters: Parameters = Parameters.Empty, formParameters: Parameters = Parameters.Empty,
encodeInQuery: Boolean = false, encodeInQuery: Boolean = false,
@@ -115,7 +117,7 @@ open class BaseInteractionHandler {
throw lastException throw lastException
} }
suspend fun imageFromUrl(client: HttpClient, imageUrl: String): ImageBitmap { suspend fun imageFromUrl(client: Http, imageUrl: String): ImageBitmap {
var attempt = 1 var attempt = 1
var lastException: Exception var lastException: Exception
do { do {

View File

@@ -4,27 +4,32 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.Category
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.backend.network.requests.addMangaToCategoryQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.categoryDeleteRequest import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.requests.categoryModifyRequest import ca.gosyer.data.server.requests.addMangaToCategoryQuery
import ca.gosyer.backend.network.requests.categoryReorderRequest import ca.gosyer.data.server.requests.categoryDeleteRequest
import ca.gosyer.backend.network.requests.createCategoryRequest import ca.gosyer.data.server.requests.categoryModifyRequest
import ca.gosyer.backend.network.requests.getCategoriesQuery import ca.gosyer.data.server.requests.categoryReorderRequest
import ca.gosyer.backend.network.requests.getMangaCategoriesQuery import ca.gosyer.data.server.requests.createCategoryRequest
import ca.gosyer.backend.network.requests.getMangaInCategoryQuery import ca.gosyer.data.server.requests.getCategoriesQuery
import ca.gosyer.backend.network.requests.removeMangaFromCategoryRequest import ca.gosyer.data.server.requests.getMangaCategoriesQuery
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.getMangaInCategoryQuery
import ca.gosyer.data.server.requests.removeMangaFromCategoryRequest
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.Parameters import io.ktor.http.Parameters
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getMangaCategories(mangaId: Long) = withContext(Dispatchers.IO) {
client.getRepeat<List<Category>>( client.getRepeat<List<Category>>(

View File

@@ -4,18 +4,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.Chapter
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.backend.network.requests.getChapterQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.getMangaChaptersQuery import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.requests.getPageQuery import ca.gosyer.data.server.requests.getChapterQuery
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.getMangaChaptersQuery
import ca.gosyer.data.server.requests.getPageQuery
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) {
client.getRepeat<List<Chapter>>( client.getRepeat<List<Chapter>>(

View File

@@ -4,19 +4,24 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.Extension
import ca.gosyer.backend.network.requests.apkIconQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.apkInstallQuery import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.requests.apkUninstallQuery import ca.gosyer.data.server.requests.apkIconQuery
import ca.gosyer.backend.network.requests.extensionListQuery import ca.gosyer.data.server.requests.apkInstallQuery
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.apkUninstallQuery
import ca.gosyer.data.server.requests.extensionListQuery
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getExtensionList() = withContext(Dispatchers.IO) {
client.getRepeat<List<Extension>>( client.getRepeat<List<Extension>>(

View File

@@ -4,18 +4,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.Manga
import ca.gosyer.backend.network.requests.addMangaToLibraryQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.getLibraryQuery import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.requests.removeMangaFromLibraryRequest import ca.gosyer.data.server.requests.addMangaToLibraryQuery
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.getLibraryQuery
import ca.gosyer.data.server.requests.removeMangaFromLibraryRequest
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getLibraryManga() = withContext(Dispatchers.IO) {
client.getRepeat<List<Manga>>( client.getRepeat<List<Manga>>(

View File

@@ -4,16 +4,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.Manga
import ca.gosyer.backend.network.requests.mangaQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.mangaThumbnailQuery import ca.gosyer.data.server.ServerPreferences
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.mangaQuery
import ca.gosyer.data.server.requests.mangaThumbnailQuery
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) {
client.getRepeat<Manga>( client.getRepeat<Manga>(

View File

@@ -4,23 +4,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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.data.models.MangaPage
import ca.gosyer.backend.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.backend.network.requests.getFilterListQuery import ca.gosyer.data.server.Http
import ca.gosyer.backend.network.requests.globalSearchQuery import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.requests.sourceInfoQuery import ca.gosyer.data.server.requests.getFilterListQuery
import ca.gosyer.backend.network.requests.sourceLatestQuery import ca.gosyer.data.server.requests.globalSearchQuery
import ca.gosyer.backend.network.requests.sourceListQuery import ca.gosyer.data.server.requests.sourceInfoQuery
import ca.gosyer.backend.network.requests.sourcePopularQuery import ca.gosyer.data.server.requests.sourceLatestQuery
import ca.gosyer.backend.network.requests.sourceSearchQuery import ca.gosyer.data.server.requests.sourceListQuery
import io.ktor.client.HttpClient import ca.gosyer.data.server.requests.sourcePopularQuery
import ca.gosyer.data.server.requests.sourceSearchQuery
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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) { suspend fun getSourceList() = withContext(Dispatchers.IO) {
client.getRepeat<List<Source>>( client.getRepeat<List<Source>>(

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun getMangaCategoriesQuery(mangaId: Long) = fun getMangaCategoriesQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun getMangaChaptersQuery(mangaId: Long) = fun getMangaChaptersQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun extensionListQuery() = fun extensionListQuery() =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun addMangaToLibraryQuery(mangaId: Long) = fun addMangaToLibraryQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun mangaQuery(mangaId: Long) = fun mangaQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 annotation class Get

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * 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 @Get
fun sourceListQuery() = fun sourceListQuery() =

View 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", "")
}
}

View File

@@ -0,0 +1,13 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.data.ui.model
enum class ThemeMode {
System,
Light,
Dark,
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -22,7 +23,8 @@ import kotlin.random.Random
fun ErrorScreen(errorMessage: String? = null) { fun ErrorScreen(errorMessage: String? = null) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Column(modifier = Modifier.align(Alignment.Center)) { 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) { if (errorMessage != null) {
Text(errorMessage, color = MaterialTheme.colors.onBackground) Text(errorMessage, color = MaterialTheme.colors.onBackground)
} }

View File

@@ -20,8 +20,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale 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 ca.gosyer.util.compose.imageFromUrl
import io.ktor.client.HttpClient
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -29,7 +30,6 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun KtorImage( fun KtorImage(
client: HttpClient,
imageUrl: String, imageUrl: String,
imageModifier: Modifier = Modifier.fillMaxSize(), imageModifier: Modifier = Modifier.fillMaxSize(),
loadingModifier: Modifier = imageModifier, loadingModifier: Modifier = imageModifier,
@@ -38,14 +38,18 @@ fun KtorImage(
contentScale: ContentScale = ContentScale.Fit, contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha, alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null, colorFilter: ColorFilter? = null,
retries: Int = 3 retries: Int = 3,
httpClient: Http? = null
) { ) {
val client = remember { httpClient ?: AppScope.getInstance() }
BoxWithConstraints { BoxWithConstraints {
val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) } val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) }
val loading: MutableState<Boolean> = remember { mutableStateOf(true) } val loading: MutableState<Boolean> = remember { mutableStateOf(true) }
val error: MutableState<String?> = remember { mutableStateOf(null) }
DisposableEffect(imageUrl) { DisposableEffect(imageUrl) {
val handler = CoroutineExceptionHandler { _, _ -> val handler = CoroutineExceptionHandler { _, throwable ->
loading.value = false loading.value = false
error.value = throwable.message
} }
val job = GlobalScope.launch(handler) { val job = GlobalScope.launch(handler) {
if (drawable.value == null) { if (drawable.value == null) {
@@ -72,12 +76,12 @@ fun KtorImage(
colorFilter = colorFilter colorFilter = colorFilter
) )
} else { } 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 attempt = 1
var lastException: Exception var lastException: Exception
do { do {

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.min import androidx.compose.ui.unit.min
@@ -23,7 +24,10 @@ fun LoadingScreen(
) { ) {
BoxWithConstraints(modifier) { BoxWithConstraints(modifier) {
if (isLoading) { 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 { } else {
ErrorScreen(errorMessage) ErrorScreen(errorMessage)
} }

View File

@@ -30,7 +30,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ca.gosyer.util.system.get
@Composable @Composable
fun MangaGridItem( fun MangaGridItem(
@@ -53,7 +52,7 @@ fun MangaGridItem(
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (cover != null) { if (cover != null) {
KtorImage(get(), cover, contentScale = ContentScale.Crop) KtorImage(cover, contentScale = ContentScale.Crop)
} }
Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
Text( Text(

View File

@@ -7,14 +7,18 @@
package ca.gosyer.ui.base.vm package ca.gosyer.ui.base.vm
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember 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 @Composable
inline fun <reified VM : ViewModel> composeViewModel(): VM { inline fun <reified VM : ViewModel> viewModel(): VM {
val viewModel = remember { val viewModel = remember {
GlobalContext.get().get<VM>() AppScope.getInstance<VM>()
} }
DisposableEffect(viewModel) { DisposableEffect(viewModel) {
onDispose { onDispose {
@@ -23,3 +27,26 @@ inline fun <reified VM : ViewModel> composeViewModel(): VM {
} }
return viewModel 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
}

View File

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

View File

@@ -39,7 +39,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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 import ca.gosyer.util.compose.ThemedWindow
fun openCategoriesMenu() { fun openCategoriesMenu() {
@@ -51,7 +51,7 @@ fun openCategoriesMenu() {
@Composable @Composable
fun CategoriesMenu(windowEvents: WindowEvents) { fun CategoriesMenu(windowEvents: WindowEvents) {
val vm = composeViewModel<CategoriesMenuViewModel>() val vm = viewModel<CategoriesMenuViewModel>()
val categories by vm.categories.collectAsState() val categories by vm.categories.collectAsState()
remember { remember {
windowEvents.onClose = { vm.updateCategories() } windowEvents.onClose = { vm.updateCategories() }

View File

@@ -6,11 +6,9 @@
package ca.gosyer.ui.categories package ca.gosyer.ui.categories
import ca.gosyer.backend.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.backend.network.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -18,9 +16,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import javax.inject.Inject
class CategoriesMenuViewModel : ViewModel() { class CategoriesMenuViewModel @Inject constructor(
private val httpClient: HttpClient by inject() private val categoryHandler: CategoryInteractionHandler
) : ViewModel() {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private var originalCategories = emptyList<Category>() private var originalCategories = emptyList<Category>()
private val _categories = MutableStateFlow(emptyList<MenuCategory>()) private val _categories = MutableStateFlow(emptyList<MenuCategory>())
@@ -38,7 +38,7 @@ class CategoriesMenuViewModel : ViewModel() {
_categories.value = emptyList() _categories.value = emptyList()
_isLoading.value = true _isLoading.value = true
try { try {
_categories.value = CategoryInteractionHandler(httpClient).getCategories() _categories.value = categoryHandler.getCategories()
.sortedBy { it.order } .sortedBy { it.order }
.also { originalCategories = it } .also { originalCategories = it }
.map { it.toMenuCategory() } .map { it.toMenuCategory() }
@@ -58,22 +58,22 @@ class CategoriesMenuViewModel : ViewModel() {
val categories = _categories.value val categories = _categories.value
val newCategories = categories.filter { it.id == null } val newCategories = categories.filter { it.id == null }
newCategories.forEach { newCategories.forEach {
CategoryInteractionHandler(httpClient).createCategory(it.name) categoryHandler.createCategory(it.name)
} }
originalCategories.forEach { originalCategory -> originalCategories.forEach { originalCategory ->
val category = categories.find { it.id == originalCategory.id } val category = categories.find { it.id == originalCategory.id }
if (category == null) { if (category == null) {
CategoryInteractionHandler(httpClient).deleteCategory(originalCategory) categoryHandler.deleteCategory(originalCategory)
} else if (category.name != originalCategory.name) { } 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 -> updatedCategories.forEach { updatedCategory ->
val category = categories.find { it.id == updatedCategory.id || it.name == updatedCategory.name } ?: return@forEach val category = categories.find { it.id == updatedCategory.id || it.name == updatedCategory.name } ?: return@forEach
if (category.order != updatedCategory.order) { if (category.order != updatedCategory.order) {
logger.debug { "${category.order} to ${updatedCategory.order}" } logger.debug { "${category.order} to ${updatedCategory.order}" }
CategoryInteractionHandler(httpClient).reorderCategory(updatedCategory, category.order, updatedCategory.order) categoryHandler.reorderCategory(updatedCategory, category.order, updatedCategory.order)
} }
} }

View File

@@ -40,12 +40,11 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.KtorImage
import ca.gosyer.ui.base.components.LoadingScreen 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.compose.ThemedWindow
import ca.gosyer.util.system.get
fun openExtensionsMenu() { fun openExtensionsMenu() {
ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) { ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) {
@@ -56,7 +55,7 @@ fun openExtensionsMenu() {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ExtensionsMenu() { fun ExtensionsMenu() {
val vm = composeViewModel<ExtensionsMenuViewModel>() val vm = viewModel<ExtensionsMenuViewModel>()
val extensions by vm.extensions.collectAsState() val extensions by vm.extensions.collectAsState()
val isLoading by vm.isLoading.collectAsState() val isLoading by vm.isLoading.collectAsState()
val serverUrl by vm.serverUrl.collectAsState() val serverUrl by vm.serverUrl.collectAsState()
@@ -107,7 +106,7 @@ fun ExtensionItem(
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) { Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.width(4.dp)) 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)) Spacer(Modifier.width(8.dp))
Column { Column {
val title = buildAnnotatedString { val title = buildAnnotatedString {

View File

@@ -6,24 +6,26 @@
package ca.gosyer.ui.extensions package ca.gosyer.ui.extensions
import ca.gosyer.backend.models.Extension import ca.gosyer.data.extension.ExtensionPreferences
import ca.gosyer.backend.network.interactions.ExtensionInteractionHandler import ca.gosyer.data.models.Extension
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import javax.inject.Inject
class ExtensionsMenuViewModel: ViewModel() { class ExtensionsMenuViewModel @Inject constructor(
private val preferences: PreferenceHelper by inject() private val extensionHandler: ExtensionInteractionHandler,
private val httpClient: HttpClient by inject() serverPreferences: ServerPreferences,
private val extensionPreferences: ExtensionPreferences
): ViewModel() {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
val serverUrl = preferences.serverUrl.asStateFlow(scope) val serverUrl = serverPreferences.server().stateIn(scope)
private val _extensions = MutableStateFlow(emptyList<Extension>()) private val _extensions = MutableStateFlow(emptyList<Extension>())
val extensions = _extensions.asStateFlow() val extensions = _extensions.asStateFlow()
@@ -41,8 +43,8 @@ class ExtensionsMenuViewModel: ViewModel() {
private suspend fun getExtensions() { private suspend fun getExtensions() {
try { try {
_isLoading.value = true _isLoading.value = true
val enabledLangs = preferences.enabledLangs.get() val enabledLangs = extensionPreferences.languages().get()
val extensions = ExtensionInteractionHandler(httpClient).getExtensionList() val extensions = extensionHandler.getExtensionList()
_extensions.value = extensions.filter { it.lang in enabledLangs }.sortedWith(compareBy({ it.lang }, { it.pkgName })) _extensions.value = extensions.filter { it.lang in enabledLangs }.sortedWith(compareBy({ it.lang }, { it.pkgName }))
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -55,7 +57,7 @@ class ExtensionsMenuViewModel: ViewModel() {
logger.info { "Install clicked" } logger.info { "Install clicked" }
scope.launch { scope.launch {
try { try {
ExtensionInteractionHandler(httpClient).installExtension(extension) extensionHandler.installExtension(extension)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
} }
@@ -67,7 +69,7 @@ class ExtensionsMenuViewModel: ViewModel() {
logger.info { "Uninstall clicked" } logger.info { "Uninstall clicked" }
scope.launch { scope.launch {
try { try {
ExtensionInteractionHandler(httpClient).uninstallExtension(extension) extensionHandler.uninstallExtension(extension)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
} }

View File

@@ -23,15 +23,15 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.backend.models.Category import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.backend.models.Manga 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.LoadingScreen
import ca.gosyer.ui.base.components.Pager import ca.gosyer.ui.base.components.Pager
import ca.gosyer.ui.base.components.PagerState 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.ui.manga.openMangaMenu
import ca.gosyer.util.compose.ThemedWindow import ca.gosyer.util.compose.ThemedWindow
import kotlinx.serialization.Serializable
fun openLibraryMenu() { fun openLibraryMenu() {
ThemedWindow { ThemedWindow {
@@ -42,7 +42,7 @@ fun openLibraryMenu() {
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun LibraryScreen() { fun LibraryScreen() {
val vm = composeViewModel<LibraryScreenViewModel>() val vm = viewModel<LibraryScreenViewModel>()
val categories by vm.categories.collectAsState() val categories by vm.categories.collectAsState()
val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState() val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState()
val displayMode by vm.displayMode.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()
}

View File

@@ -6,20 +6,20 @@
package ca.gosyer.ui.library package ca.gosyer.ui.library
import ca.gosyer.backend.models.Category import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Category
import ca.gosyer.backend.network.interactions.CategoryInteractionHandler import ca.gosyer.data.models.Manga
import ca.gosyer.backend.network.interactions.LibraryInteractionHandler import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
private typealias LibraryMap = MutableMap<Int, MutableStateFlow<List<Manga>>> private typealias LibraryMap = MutableMap<Int, MutableStateFlow<List<Manga>>>
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap) 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 getManga(order).value = manga
} }
class LibraryScreenViewModel: ViewModel() { class LibraryScreenViewModel @Inject constructor(
private val preferences: PreferenceHelper by inject() private val libraryHandler: LibraryInteractionHandler,
private val httpClient: HttpClient by inject() private val categoryHandler: CategoryInteractionHandler,
libraryPreferences: LibraryPreferences,
val serverUrl = preferences.serverUrl.asStateFlow(scope) serverPreferences: ServerPreferences,
): ViewModel() {
val serverUrl = serverPreferences.server().stateIn(scope)
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf()) private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
val categories = library.categories.asStateFlow() val categories = library.categories.asStateFlow()
@@ -42,7 +44,7 @@ class LibraryScreenViewModel: ViewModel() {
private val _selectedCategoryIndex = MutableStateFlow(0) private val _selectedCategoryIndex = MutableStateFlow(0)
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
val displayMode = preferences.libraryDisplay.asStateFlow(scope) val displayMode = libraryPreferences.displayMode().stateIn(scope)
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
@@ -55,19 +57,19 @@ class LibraryScreenViewModel: ViewModel() {
scope.launch { scope.launch {
_isLoading.value = true _isLoading.value = true
try { try {
val categories = CategoryInteractionHandler(httpClient).getCategories() val categories = categoryHandler.getCategories()
if (categories.isEmpty()) { if (categories.isEmpty()) {
library.categories.value = listOf(defaultCategory) library.categories.value = listOf(defaultCategory)
library.mangaMap.setManga(defaultCategory.order, LibraryInteractionHandler(httpClient).getLibraryManga()) library.mangaMap.setManga(defaultCategory.order, libraryHandler.getLibraryManga())
} else { } else {
library.categories.value = listOf(defaultCategory) + categories.sortedBy { it.order } library.categories.value = listOf(defaultCategory) + categories.sortedBy { it.order }
categories.map { categories.map {
async { async {
library.mangaMap.setManga(it.order, CategoryInteractionHandler(httpClient).getMangaFromCategory(it)) library.mangaMap.setManga(it.order, categoryHandler.getMangaFromCategory(it))
} }
}.awaitAll() }.awaitAll()
val mangaInCategories = library.mangaMap.flatMap { it.value.value }.map { it.id }.distinct() 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) { } catch (e: Exception) {
} finally { } finally {

View File

@@ -32,9 +32,8 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.ui.base.components.KtorImage
import ca.gosyer.util.system.get
@Composable @Composable
fun LibraryMangaCompactGrid( fun LibraryMangaCompactGrid(
@@ -78,7 +77,7 @@ private fun LibraryMangaCompactGridItem(
.clickable(onClick = onClick) .clickable(onClick = onClick)
) { ) {
if (cover != null) { if (cover != null) {
KtorImage(get(), cover, contentScale = ContentScale.Crop) KtorImage(cover, contentScale = ContentScale.Crop)
} }
Box(modifier = Modifier.fillMaxSize().then(shadowGradient)) Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
Text( Text(

View File

@@ -7,5 +7,6 @@
package ca.gosyer.ui.main package ca.gosyer.ui.main
import ca.gosyer.ui.base.vm.ViewModel import ca.gosyer.ui.base.vm.ViewModel
import javax.inject.Inject
class MainViewModel : ViewModel() class MainViewModel @Inject constructor(): ViewModel()

View File

@@ -6,17 +6,15 @@
package ca.gosyer.ui.main package ca.gosyer.ui.main
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import ca.gosyer.backend.network.networkModule import ca.gosyer.BuildConfig
import ca.gosyer.backend.preferences.preferencesModule import ca.gosyer.data.DataModule
import ca.gosyer.ui.base.vm.composeViewModel import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.base.vm.viewModelModule
import ca.gosyer.ui.categories.openCategoriesMenu import ca.gosyer.ui.categories.openCategoriesMenu
import ca.gosyer.ui.extensions.openExtensionsMenu import ca.gosyer.ui.extensions.openExtensionsMenu
import ca.gosyer.ui.library.openLibraryMenu import ca.gosyer.ui.library.openLibraryMenu
@@ -26,11 +24,21 @@ import ca.gosyer.util.system.userDataDir
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import org.koin.core.context.startKoin import org.apache.logging.log4j.core.config.Configurator
import kotlin.concurrent.thread import toothpick.configuration.Configuration
import toothpick.ktp.KTP
import java.io.BufferedReader
import java.io.File import java.io.File
import kotlin.concurrent.thread
fun main() { fun main() {
val clazz = MainViewModel::class.java
Configurator.initialize(
null,
clazz.classLoader,
clazz.getResource("log4j2.xml")?.toURI()
)
GlobalScope.launch { GlobalScope.launch {
val logger = KotlinLogging.logger("Server") val logger = KotlinLogging.logger("Server")
val runtime = Runtime.getRuntime() val runtime = Runtime.getRuntime()
@@ -38,19 +46,30 @@ fun main() {
val jarFile = File(userDataDir,"Tachidesk.jar") val jarFile = File(userDataDir,"Tachidesk.jar")
if (!jarFile.exists()) { if (!jarFile.exists()) {
logger.info { "Copying server to resources" } logger.info { "Copying server to resources" }
javaClass.getResourceAsStream("/Tachidesk.jar").buffered().use { input -> javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input ->
jarFile.outputStream().use { output -> jarFile.outputStream().use { output ->
input.copyTo(output) input.copyTo(output)
} }
} }
} }
logger.info { "Starting server" } val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator)
val process = runtime.exec("""java -jar "${jarFile.absolutePath}"""") 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) { runtime.addShutdownHook(thread(start = false) {
process?.destroy() process?.destroy()
}) })
val reader = process.inputStream.reader().buffered()
logger.info { "Server started successfully" } logger.info { "Server started successfully" }
var line: String? var line: String?
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
@@ -61,18 +80,28 @@ fun main() {
logger.info { "Process exitValue: $exitVal" } logger.info { "Process exitValue: $exitVal" }
} }
startKoin { if (BuildConfig.DEBUG) {
modules( System.setProperty("kotlinx.coroutines.debug", "on")
preferencesModule,
networkModule,
viewModelModule
)
} }
KTP.setConfiguration(
if (BuildConfig.DEBUG) {
Configuration.forDevelopment()
} else {
Configuration.forProduction()
}
)
KTP.openRootScope()
.installModules(
DataModule
)
ThemedWindow(title = "TachideskJUI") { ThemedWindow(title = "TachideskJUI") {
val vm = composeViewModel<MainViewModel>() val vm = viewModel<MainViewModel>()
Column(Modifier.fillMaxSize().background(MaterialTheme.colors.background)) { Surface {
Column(Modifier.fillMaxSize()) {
Button( Button(
onClick = ::openExtensionsMenu onClick = ::openExtensionsMenu
) { ) {
@@ -95,4 +124,5 @@ fun main() {
} }
} }
} }
}
} }

View File

@@ -39,14 +39,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ca.gosyer.backend.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.ui.base.components.KtorImage import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.mangaAspectRatio 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.compose.ThemedWindow
import ca.gosyer.util.system.get
fun openMangaMenu(mangaId: Long) { fun openMangaMenu(mangaId: Long) {
ThemedWindow("TachideskJUI") { ThemedWindow("TachideskJUI") {
@@ -62,7 +61,7 @@ fun openMangaMenu(manga: Manga) {
@Composable @Composable
fun MangaMenu(mangaId: Long) { fun MangaMenu(mangaId: Long) {
val vm = composeViewModel<MangaMenuViewModel>() val vm = viewModel<MangaMenuViewModel>()
remember(mangaId) { remember(mangaId) {
vm.init(mangaId) vm.init(mangaId)
} }
@@ -71,7 +70,7 @@ fun MangaMenu(mangaId: Long) {
@Composable @Composable
fun MangaMenu(manga: Manga) { fun MangaMenu(manga: Manga) {
val vm = composeViewModel<MangaMenuViewModel>() val vm = viewModel<MangaMenuViewModel>()
remember(manga) { remember(manga) {
vm.init(manga) vm.init(manga)
} }
@@ -161,7 +160,7 @@ private fun Cover(manga: Manga, serverUrl: String, modifier: Modifier = Modifier
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
manga.cover(serverUrl).let { manga.cover(serverUrl).let {
if (it != null) { if (it != null) {
KtorImage(get(), it) KtorImage(it)
} }
} }
} }

View File

@@ -6,15 +6,13 @@
package ca.gosyer.ui.manga package ca.gosyer.ui.manga
import ca.gosyer.backend.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.backend.network.interactions.ChapterInteractionHandler import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.network.interactions.LibraryInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.backend.network.interactions.MangaInteractionHandler import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.interactions.MangaInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -22,12 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
class MangaMenuViewModel : ViewModel() { class MangaMenuViewModel @Inject constructor(
private val preferences: PreferenceHelper by inject() private val mangaHandler: MangaInteractionHandler,
private val httpClient: HttpClient by inject() private val chapterHandler: ChapterInteractionHandler,
private val libraryHandler: LibraryInteractionHandler,
val serverUrl = preferences.serverUrl.asStateFlow(scope) serverPreferences: ServerPreferences
) : ViewModel() {
val serverUrl = serverPreferences.server().stateIn(scope)
private val _manga = MutableStateFlow<Manga?>(null) private val _manga = MutableStateFlow<Manga?>(null)
val manga = _manga.asStateFlow() val manga = _manga.asStateFlow()
@@ -53,7 +54,7 @@ class MangaMenuViewModel : ViewModel() {
private suspend fun refreshMangaAsync(mangaId: Long) = withContext(Dispatchers.IO) { private suspend fun refreshMangaAsync(mangaId: Long) = withContext(Dispatchers.IO) {
async { async {
try { try {
_manga.value = MangaInteractionHandler(httpClient).getManga(mangaId) _manga.value = mangaHandler.getManga(mangaId)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
} }
@@ -63,7 +64,7 @@ class MangaMenuViewModel : ViewModel() {
suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) { suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) {
async { async {
try { try {
_chapters.value = ChapterInteractionHandler(httpClient).getChapters(mangaId) _chapters.value = chapterHandler.getChapters(mangaId)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
} }
@@ -74,9 +75,9 @@ class MangaMenuViewModel : ViewModel() {
scope.launch { scope.launch {
manga.value?.let { manga.value?.let {
if (it.inLibrary) { if (it.inLibrary) {
LibraryInteractionHandler(httpClient).removeMangaFromLibrary(it) libraryHandler.removeMangaFromLibrary(it)
} else { } else {
LibraryInteractionHandler(httpClient).addMangaToLibrary(it) libraryHandler.addMangaToLibrary(it)
} }
refreshMangaAsync(it.id).await() refreshMangaAsync(it.id).await()

View File

@@ -14,8 +14,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import ca.gosyer.backend.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.vm.composeViewModel import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.sources.components.SourceHomeScreen import ca.gosyer.ui.sources.components.SourceHomeScreen
import ca.gosyer.ui.sources.components.SourceScreen import ca.gosyer.ui.sources.components.SourceScreen
import ca.gosyer.ui.sources.components.SourceTopBar import ca.gosyer.ui.sources.components.SourceTopBar
@@ -29,7 +29,7 @@ fun openSourcesMenu() {
@Composable @Composable
fun SourcesMenu() { fun SourcesMenu() {
val vm = composeViewModel<SourcesMenuViewModel>() val vm = viewModel<SourcesMenuViewModel>()
val isLoading by vm.isLoading.collectAsState() val isLoading by vm.isLoading.collectAsState()
val sources by vm.sources.collectAsState() val sources by vm.sources.collectAsState()
val sourceTabs by vm.sourceTabs.collectAsState() val sourceTabs by vm.sourceTabs.collectAsState()

View File

@@ -6,24 +6,28 @@
package ca.gosyer.ui.sources package ca.gosyer.ui.sources
import ca.gosyer.backend.models.Source import ca.gosyer.data.catalog.CatalogPreferences
import ca.gosyer.backend.network.interactions.SourceInteractionHandler import ca.gosyer.data.models.Source
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import javax.inject.Inject
class SourcesMenuViewModel: ViewModel() { class SourcesMenuViewModel @Inject constructor(
private val preferences: PreferenceHelper by inject() private val sourceHandler: SourceInteractionHandler,
private val httpClient: HttpClient by inject() serverPreferences: ServerPreferences,
catalogPreferences: CatalogPreferences
): ViewModel() {
private val logger = KotlinLogging.logger {} 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) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
@@ -44,9 +48,9 @@ class SourcesMenuViewModel: ViewModel() {
private fun getSources() { private fun getSources() {
scope.launch { scope.launch {
try { try {
val sources = SourceInteractionHandler(httpClient).getSourceList() val sources = sourceHandler.getSourceList()
logger.info { sources } 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 } logger.info { _sources.value }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e

View File

@@ -29,10 +29,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.KtorImage
import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.util.system.get
@Composable @Composable
fun SourceHomeScreen( fun SourceHomeScreen(
@@ -110,7 +109,7 @@ fun SourceItem(
}, },
horizontalAlignment = Alignment.CenterHorizontally 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)) Spacer(Modifier.height(4.dp))
Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground) Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground)
} }

View File

@@ -21,18 +21,18 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.backend.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.MangaGridItem 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 import ca.gosyer.ui.manga.openMangaMenu
@Composable @Composable
fun SourceScreen( fun SourceScreen(
source: Source source: Source
) { ) {
val vm = composeViewModel<SourceScreenViewModel>() val vm = viewModel<SourceScreenViewModel>()
remember(source) { remember(source) {
vm.init(source) vm.init(source)
} }

View File

@@ -6,26 +6,24 @@
package ca.gosyer.ui.sources.components package ca.gosyer.ui.sources.components
import ca.gosyer.backend.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.backend.models.MangaPage import ca.gosyer.data.models.MangaPage
import ca.gosyer.backend.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.backend.network.interactions.SourceInteractionHandler import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.backend.preferences.PreferenceHelper import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch 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 lateinit var source: Source
private val preferences: PreferenceHelper by inject()
private val httpClient: HttpClient by inject()
val serverUrl = preferences.serverUrl.asFLow() val serverUrl = serverPreferences.server().stateIn(scope)
.asStateFlow(preferences.serverUrl.get(),scope, true)
private val _mangas = MutableStateFlow(emptyList<Manga>()) private val _mangas = MutableStateFlow(emptyList<Manga>())
val mangas = _mangas.asStateFlow() val mangas = _mangas.asStateFlow()
@@ -49,6 +47,7 @@ class SourceScreenViewModel: ViewModel() {
_mangas.value = emptyList() _mangas.value = emptyList()
_hasNextPage.value = false _hasNextPage.value = false
_pageNum.value = 1 _pageNum.value = 1
_isLatest.value = source.supportsLatest
val page = getPage() val page = getPage()
_mangas.value += page.mangaList _mangas.value += page.mangaList
_hasNextPage.value = page.hasNextPage _hasNextPage.value = page.hasNextPage
@@ -76,9 +75,9 @@ class SourceScreenViewModel: ViewModel() {
private suspend fun getPage(): MangaPage { private suspend fun getPage(): MangaPage {
return if (isLatest.value) { return if (isLatest.value) {
SourceInteractionHandler(httpClient).getLatestManga(source, pageNum.value) sourceHandler.getLatestManga(source, pageNum.value)
} else { } else {
SourceInteractionHandler(httpClient).getPopularManga(source, pageNum.value) sourceHandler.getPopularManga(source, pageNum.value)
} }
} }
} }

View File

@@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.backend.models.Source import ca.gosyer.data.models.Source
@Composable @Composable
fun SourceTopBar( fun SourceTopBar(

View 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
}

View File

@@ -8,7 +8,7 @@ package ca.gosyer.util.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap 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.request.get
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readBytes import io.ktor.client.statement.readBytes
@@ -19,6 +19,6 @@ fun imageFromFile(file: File): ImageBitmap {
return Image.makeFromEncoded(file.readBytes()).asImageBitmap() 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() return Image.makeFromEncoded(client.get<HttpResponse>(url).readBytes()).asImageBitmap()
} }

View File

@@ -13,8 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.MenuBar
import ca.gosyer.backend.preferences.PreferenceHelper
import ca.gosyer.util.system.get
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
fun ThemedWindow( fun ThemedWindow(
@@ -31,7 +29,7 @@ fun ThemedWindow(
content: @Composable () -> Unit = { } content: @Composable () -> Unit = { }
) { ) {
Window(title, size, location, centered, icon, menuBar, undecorated, resizable, events, onDismissRequest) { Window(title, size, location, centered, icon, menuBar, undecorated, resizable, events, onDismissRequest) {
DesktopMaterialTheme(get<PreferenceHelper>().getTheme()) { DesktopMaterialTheme {
content() content()
} }
} }

File diff suppressed because it is too large Load Diff

View 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>

View File

@@ -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"/>
&lt;!&ndash;<appender-ref ref="FILE"/>&ndash;&gt;
</logger>-->
<root level="debug">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@@ -1,4 +1,4 @@
package ca.gosyer.backend.network package ca.gosyer.data.server
import kotlin.test.Test import kotlin.test.Test