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
*.iws
/.idea/*
!/.idea/runConfigurations
!/.idea/runConfigurations/
out/
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
plugins {
kotlin("jvm") version "1.4.31"
kotlin("plugin.serialization") version "1.4.31"
id("org.jetbrains.compose") version "0.4.0-build177"
kotlin("jvm") version "1.4.32"
kotlin("kapt") version "1.4.32"
kotlin("plugin.serialization") version "1.4.32"
id("org.jetbrains.compose") version "0.4.0-build184"
id("de.fuerstenau.buildconfig") version "1.1.8"
}
group = "ca.gosyer"
version = "1.0.0"
repositories {
jcenter()
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
}
@@ -28,7 +29,8 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
// Dependency Injection
implementation("io.insert-koin:koin-core-ext:3.0.1-beta-1")
implementation("com.github.stephanenicolas.toothpick:ktp:3.1.0")
kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:3.1.0")
// Http client
val ktorVersion = "1.5.2"
@@ -38,8 +40,10 @@ dependencies {
implementation("io.ktor:ktor-client-logging:$ktorVersion")
// Logging
implementation("ch.qos.logback:logback-classic:1.2.3")
//implementation("org.fusesource.jansi:jansi:1.18")
val log4jVersion = "2.14.1"
implementation("org.apache.logging.log4j:log4j-api:$log4jVersion")
implementation("org.apache.logging.log4j:log4j-core:$log4jVersion")
implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion")
implementation("io.github.microutils:kotlin-logging:2.0.5")
// User storage
@@ -106,3 +110,13 @@ compose.desktop {
}
}
}
buildConfig {
appName = project.name
version = project.version.toString()
clsName = "BuildConfig"
packageName = project.group.toString()
buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString())
}

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/.
*/
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/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

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

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.models
package ca.gosyer.data.models
import kotlinx.serialization.Serializable

View File

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

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

View File

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

View File

@@ -4,18 +4,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.interactions
package ca.gosyer.data.server.interactions
import ca.gosyer.backend.models.Chapter
import ca.gosyer.backend.models.Manga
import ca.gosyer.backend.network.requests.getChapterQuery
import ca.gosyer.backend.network.requests.getMangaChaptersQuery
import ca.gosyer.backend.network.requests.getPageQuery
import io.ktor.client.HttpClient
import ca.gosyer.data.models.Chapter
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.getChapterQuery
import ca.gosyer.data.server.requests.getMangaChaptersQuery
import ca.gosyer.data.server.requests.getPageQuery
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ChapterInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
class ChapterInteractionHandler @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
): BaseInteractionHandler(client, serverPreferences) {
suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) {
client.getRepeat<List<Chapter>>(

View File

@@ -4,19 +4,24 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.interactions
package ca.gosyer.data.server.interactions
import ca.gosyer.backend.models.Extension
import ca.gosyer.backend.network.requests.apkIconQuery
import ca.gosyer.backend.network.requests.apkInstallQuery
import ca.gosyer.backend.network.requests.apkUninstallQuery
import ca.gosyer.backend.network.requests.extensionListQuery
import io.ktor.client.HttpClient
import ca.gosyer.data.models.Extension
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.apkIconQuery
import ca.gosyer.data.server.requests.apkInstallQuery
import ca.gosyer.data.server.requests.apkUninstallQuery
import ca.gosyer.data.server.requests.extensionListQuery
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ExtensionInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
class ExtensionInteractionHandler @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
): BaseInteractionHandler(client, serverPreferences) {
suspend fun getExtensionList() = withContext(Dispatchers.IO) {
client.getRepeat<List<Extension>>(

View File

@@ -4,18 +4,23 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.interactions
package ca.gosyer.data.server.interactions
import ca.gosyer.backend.models.Manga
import ca.gosyer.backend.network.requests.addMangaToLibraryQuery
import ca.gosyer.backend.network.requests.getLibraryQuery
import ca.gosyer.backend.network.requests.removeMangaFromLibraryRequest
import io.ktor.client.HttpClient
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.addMangaToLibraryQuery
import ca.gosyer.data.server.requests.getLibraryQuery
import ca.gosyer.data.server.requests.removeMangaFromLibraryRequest
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class LibraryInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
class LibraryInteractionHandler @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
): BaseInteractionHandler(client, serverPreferences) {
suspend fun getLibraryManga() = withContext(Dispatchers.IO) {
client.getRepeat<List<Manga>>(

View File

@@ -4,16 +4,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.interactions
package ca.gosyer.data.server.interactions
import ca.gosyer.backend.models.Manga
import ca.gosyer.backend.network.requests.mangaQuery
import ca.gosyer.backend.network.requests.mangaThumbnailQuery
import io.ktor.client.HttpClient
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.mangaQuery
import ca.gosyer.data.server.requests.mangaThumbnailQuery
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class MangaInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
class MangaInteractionHandler @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
): BaseInteractionHandler(client, serverPreferences) {
suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) {
client.getRepeat<Manga>(

View File

@@ -4,23 +4,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.interactions
package ca.gosyer.data.server.interactions
import ca.gosyer.backend.models.MangaPage
import ca.gosyer.backend.models.Source
import ca.gosyer.backend.network.requests.getFilterListQuery
import ca.gosyer.backend.network.requests.globalSearchQuery
import ca.gosyer.backend.network.requests.sourceInfoQuery
import ca.gosyer.backend.network.requests.sourceLatestQuery
import ca.gosyer.backend.network.requests.sourceListQuery
import ca.gosyer.backend.network.requests.sourcePopularQuery
import ca.gosyer.backend.network.requests.sourceSearchQuery
import io.ktor.client.HttpClient
import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.requests.getFilterListQuery
import ca.gosyer.data.server.requests.globalSearchQuery
import ca.gosyer.data.server.requests.sourceInfoQuery
import ca.gosyer.data.server.requests.sourceLatestQuery
import ca.gosyer.data.server.requests.sourceListQuery
import ca.gosyer.data.server.requests.sourcePopularQuery
import ca.gosyer.data.server.requests.sourceSearchQuery
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class SourceInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
class SourceInteractionHandler @Inject constructor(
client: Http,
serverPreferences: ServerPreferences
): BaseInteractionHandler(client, serverPreferences) {
suspend fun getSourceList() = withContext(Dispatchers.IO) {
client.getRepeat<List<Source>>(

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun getMangaCategoriesQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun getMangaChaptersQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun extensionListQuery() =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun addMangaToLibraryQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun mangaQuery(mangaId: Long) =

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
annotation class Get

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.backend.network.requests
package ca.gosyer.data.server.requests
@Get
fun sourceListQuery() =

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

View File

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

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.min
@@ -23,7 +24,10 @@ fun LoadingScreen(
) {
BoxWithConstraints(modifier) {
if (isLoading) {
CircularProgressIndicator(Modifier.align(Alignment.Center).size(min(maxHeight, maxWidth) / 2))
val size = remember(maxHeight, maxWidth) {
min(maxHeight, maxWidth) / 2
}
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size))
} else {
ErrorScreen(errorMessage)
}

View File

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

View File

@@ -7,14 +7,18 @@
package ca.gosyer.ui.base.vm
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import org.koin.core.context.GlobalContext
import ca.gosyer.common.di.AppScope
import toothpick.Toothpick
import toothpick.ktp.binding.module
import toothpick.ktp.extension.getInstance
@Composable
inline fun <reified VM : ViewModel> composeViewModel(): VM {
inline fun <reified VM : ViewModel> viewModel(): VM {
val viewModel = remember {
GlobalContext.get().get<VM>()
AppScope.getInstance<VM>()
}
DisposableEffect(viewModel) {
onDispose {
@@ -23,3 +27,26 @@ inline fun <reified VM : ViewModel> composeViewModel(): VM {
}
return viewModel
}
@Composable
inline fun <reified VM : ViewModel> viewModel(
crossinline binding: @DisallowComposableCalls () -> Any,
): VM {
val (viewModel, submodule) = remember {
val submodule = module {
binding().let { bind(it.javaClass).toInstance(it) }
}
val subscope = AppScope.subscope(submodule).also {
it.installModules(submodule)
}
val viewModel = subscope.getInstance<VM>()
Pair(viewModel, submodule)
}
DisposableEffect(viewModel) {
onDispose {
viewModel.destroy()
Toothpick.closeScope(submodule)
}
}
return viewModel
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,24 +6,28 @@
package ca.gosyer.ui.sources
import ca.gosyer.backend.models.Source
import ca.gosyer.backend.network.interactions.SourceInteractionHandler
import ca.gosyer.backend.preferences.PreferenceHelper
import ca.gosyer.data.catalog.CatalogPreferences
import ca.gosyer.data.models.Source
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.util.system.inject
import io.ktor.client.HttpClient
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import mu.KotlinLogging
import javax.inject.Inject
class SourcesMenuViewModel: ViewModel() {
private val preferences: PreferenceHelper by inject()
private val httpClient: HttpClient by inject()
class SourcesMenuViewModel @Inject constructor(
private val sourceHandler: SourceInteractionHandler,
serverPreferences: ServerPreferences,
catalogPreferences: CatalogPreferences
): ViewModel() {
private val logger = KotlinLogging.logger {}
val serverUrl = preferences.serverUrl.asStateFlow(scope)
val serverUrl = serverPreferences.server().stateIn(scope)
private val languages = catalogPreferences.languages().stateIn(scope)
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow()
@@ -44,9 +48,9 @@ class SourcesMenuViewModel: ViewModel() {
private fun getSources() {
scope.launch {
try {
val sources = SourceInteractionHandler(httpClient).getSourceList()
val sources = sourceHandler.getSourceList()
logger.info { sources }
_sources.value = sources//.filter { it.lang in Preferences.enabledLangs }
_sources.value = sources.filter { it.lang in languages.value }
logger.info { _sources.value }
} catch (e: Exception) {
if (e is CancellationException) throw e

View File

@@ -29,10 +29,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ca.gosyer.backend.models.Source
import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.util.system.get
@Composable
fun SourceHomeScreen(
@@ -110,7 +109,7 @@ fun SourceItem(
},
horizontalAlignment = Alignment.CenterHorizontally
) {
KtorImage(get(), source.iconUrl(serverUrl), Modifier.size(96.dp))
KtorImage(source.iconUrl(serverUrl), Modifier.size(96.dp))
Spacer(Modifier.height(4.dp))
Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground)
}

View File

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

View File

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

View File

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

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.asImageBitmap
import io.ktor.client.HttpClient
import ca.gosyer.data.server.Http
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readBytes
@@ -19,6 +19,6 @@ fun imageFromFile(file: File): ImageBitmap {
return Image.makeFromEncoded(file.readBytes()).asImageBitmap()
}
suspend fun imageFromUrl(client: HttpClient, url: String): ImageBitmap {
suspend fun imageFromUrl(client: Http, url: String): ImageBitmap {
return Image.makeFromEncoded(client.get<HttpResponse>(url).readBytes()).asImageBitmap()
}

View File

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

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