mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Rewrite backend
- Use Tachiyomi 1.x Preference backend - Switch DI from Koin to Toothpick - Use gradle BuildConfig library to move variables from gradle to the App - Switch from Logback to Log4j2 with slf4j implmenetation - Try to use the same java as the application for the server - Add Run Debug run configuration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,6 +19,6 @@ bin/
|
|||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
/.idea/*
|
/.idea/*
|
||||||
!/.idea/runConfigurations
|
!/.idea/runConfigurations/
|
||||||
out/
|
out/
|
||||||
workspace.xml
|
workspace.xml
|
||||||
23
.run/TachideskJUI [run].run.xml
Normal file
23
.run/TachideskJUI [run].run.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="TachideskJUI Run Debug" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="-PdebugApp" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="run" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" value="" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -3,16 +3,17 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.4.31"
|
kotlin("jvm") version "1.4.32"
|
||||||
kotlin("plugin.serialization") version "1.4.31"
|
kotlin("kapt") version "1.4.32"
|
||||||
id("org.jetbrains.compose") version "0.4.0-build177"
|
kotlin("plugin.serialization") version "1.4.32"
|
||||||
|
id("org.jetbrains.compose") version "0.4.0-build184"
|
||||||
|
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "ca.gosyer"
|
group = "ca.gosyer"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,8 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
implementation("io.insert-koin:koin-core-ext:3.0.1-beta-1")
|
implementation("com.github.stephanenicolas.toothpick:ktp:3.1.0")
|
||||||
|
kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:3.1.0")
|
||||||
|
|
||||||
// Http client
|
// Http client
|
||||||
val ktorVersion = "1.5.2"
|
val ktorVersion = "1.5.2"
|
||||||
@@ -38,8 +40,10 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-client-logging:$ktorVersion")
|
implementation("io.ktor:ktor-client-logging:$ktorVersion")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
val log4jVersion = "2.14.1"
|
||||||
//implementation("org.fusesource.jansi:jansi:1.18")
|
implementation("org.apache.logging.log4j:log4j-api:$log4jVersion")
|
||||||
|
implementation("org.apache.logging.log4j:log4j-core:$log4jVersion")
|
||||||
|
implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.5")
|
implementation("io.github.microutils:kotlin-logging:2.0.5")
|
||||||
|
|
||||||
// User storage
|
// User storage
|
||||||
@@ -106,3 +110,13 @@ compose.desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfig {
|
||||||
|
appName = project.name
|
||||||
|
version = project.version.toString()
|
||||||
|
|
||||||
|
clsName = "BuildConfig"
|
||||||
|
packageName = project.group.toString()
|
||||||
|
|
||||||
|
buildConfigField("boolean", "DEBUG", project.hasProperty("debugApp").toString())
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences
|
|
||||||
|
|
||||||
import androidx.compose.material.darkColors
|
|
||||||
import androidx.compose.material.lightColors
|
|
||||||
import ca.gosyer.backend.preferences.impl.getBooleanPreference
|
|
||||||
import ca.gosyer.backend.preferences.impl.getJsonPreference
|
|
||||||
import ca.gosyer.backend.preferences.impl.getLongPreference
|
|
||||||
import ca.gosyer.backend.preferences.impl.getStringPreference
|
|
||||||
import ca.gosyer.ui.library.DisplayMode
|
|
||||||
import ca.gosyer.util.compose.color
|
|
||||||
import com.russhwolf.settings.JvmPreferencesSettings
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import java.util.prefs.Preferences
|
|
||||||
|
|
||||||
class PreferenceHelper {
|
|
||||||
private val settings = JvmPreferencesSettings(Preferences.userRoot()) as ObservableSettings
|
|
||||||
val serverUrl = settings.getStringPreference("server_url", "http://localhost:4567")
|
|
||||||
val enabledLangs = settings.getJsonPreference("server_langs", listOf("all", "en"), ListSerializer(String.serializer()))
|
|
||||||
val libraryDisplay = settings.getJsonPreference("library_display", DisplayMode.CompactGrid, DisplayMode.serializer())
|
|
||||||
|
|
||||||
val lightTheme = settings.getBooleanPreference("light_theme", true)
|
|
||||||
val lightPrimary = settings.getLongPreference("light_color_primary", 0xFF00a2ff)
|
|
||||||
val lightPrimaryVariant = settings.getLongPreference("light_color_primary_variant", 0xFF0091EA)
|
|
||||||
val lightSecondary = settings.getLongPreference("light_color_secondary", 0xFFF44336)
|
|
||||||
val lightSecondaryVaraint = settings.getLongPreference("light_color_secondary_variant", 0xFFE53935)
|
|
||||||
|
|
||||||
fun getTheme() = when (lightTheme.get()) {
|
|
||||||
true -> lightColors(
|
|
||||||
lightPrimary.get().color,
|
|
||||||
lightPrimaryVariant.get().color,
|
|
||||||
lightSecondary.get().color,
|
|
||||||
lightSecondaryVaraint.get().color
|
|
||||||
)
|
|
||||||
false -> darkColors()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val preferencesModule = module {
|
|
||||||
single { PreferenceHelper() }
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getBooleanFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getBooleanOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class BooleanPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: Boolean
|
|
||||||
): DefaultPreference<Boolean> {
|
|
||||||
override fun get() = settings.getBoolean(key, default)
|
|
||||||
override fun asFLow() = settings.getBooleanFlow(key, default)
|
|
||||||
override fun set(value: Boolean) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BooleanNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<Boolean> {
|
|
||||||
override fun get() = settings.getBooleanOrNull(key)
|
|
||||||
override fun asFLow() = settings.getBooleanOrNullFlow(key)
|
|
||||||
override fun set(value: Boolean?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getBooleanPreference(key: String, default: Boolean) = BooleanPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getBooleanPreference(key: String) = BooleanNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getDoubleFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getDoubleOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class DoublePreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: Double
|
|
||||||
): DefaultPreference<Double> {
|
|
||||||
override fun get() = settings.getDouble(key, default)
|
|
||||||
override fun asFLow() = settings.getDoubleFlow(key, default)
|
|
||||||
override fun set(value: Double) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DoubleNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<Double> {
|
|
||||||
override fun get() = settings.getDoubleOrNull(key)
|
|
||||||
override fun asFLow() = settings.getDoubleOrNullFlow(key)
|
|
||||||
override fun set(value: Double?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getDoublePreference(key: String, default: Double) = DoublePreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getDoublePreference(key: String) = DoubleNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getFloatFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getFloatOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class FloatPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: Float
|
|
||||||
): DefaultPreference<Float> {
|
|
||||||
override fun get() = settings.getFloat(key, default)
|
|
||||||
override fun asFLow() = settings.getFloatFlow(key, default)
|
|
||||||
override fun set(value: Float) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FloatNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<Float> {
|
|
||||||
override fun get() = settings.getFloatOrNull(key)
|
|
||||||
override fun asFLow() = settings.getFloatOrNullFlow(key)
|
|
||||||
override fun set(value: Float?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getFloatPreference(key: String, default: Float) = FloatPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getFloatPreference(key: String) = FloatNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getIntFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getIntOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class IntPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: Int
|
|
||||||
): DefaultPreference<Int> {
|
|
||||||
override fun get() = settings.getInt(key, default)
|
|
||||||
override fun asFLow() = settings.getIntFlow(key, default)
|
|
||||||
override fun set(value: Int) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IntNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<Int> {
|
|
||||||
override fun get() = settings.getIntOrNull(key)
|
|
||||||
override fun asFLow() = settings.getIntOrNullFlow(key)
|
|
||||||
override fun set(value: Int?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getIntPreference(key: String, default: Int) = IntPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getIntPreference(key: String) = IntNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.Settings
|
|
||||||
import com.russhwolf.settings.get
|
|
||||||
import com.russhwolf.settings.serialization.decodeValue
|
|
||||||
import com.russhwolf.settings.serialization.decodeValueOrNull
|
|
||||||
import com.russhwolf.settings.serialization.encodeValue
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
|
|
||||||
class JsonPreference<T>(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: T,
|
|
||||||
private val serializer: KSerializer<T>
|
|
||||||
): DefaultPreference<T> {
|
|
||||||
override fun get() = settings.decodeValue(serializer, key, default)
|
|
||||||
override fun asFLow() = settings.createFlow(key, default) { key, default ->
|
|
||||||
decodeValue(serializer, key, default)
|
|
||||||
}
|
|
||||||
override fun set(value: T) {
|
|
||||||
settings.encodeValue(serializer, key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getJson(): String? {
|
|
||||||
return settings[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class JsonNullPreference<T>(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
private val serializer: KSerializer<T>
|
|
||||||
): NullPreference<T> {
|
|
||||||
override fun get() = settings.decodeValueOrNull(serializer, key)
|
|
||||||
override fun asFLow() = settings.createFlow<T?>(key, null) { key, _ ->
|
|
||||||
decodeValueOrNull(serializer, key)
|
|
||||||
}
|
|
||||||
override fun set(value: T?) {
|
|
||||||
if (value != null) {
|
|
||||||
settings.encodeValue(serializer, key, value)
|
|
||||||
} else {
|
|
||||||
settings.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getJson(): String? {
|
|
||||||
return settings[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> ObservableSettings.getJsonPreference(key: String, default: T, serializer: KSerializer<T>) = JsonPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default,
|
|
||||||
serializer
|
|
||||||
)
|
|
||||||
|
|
||||||
fun <T> ObservableSettings.getJsonPreference(key: String, serializer: KSerializer<T>) = JsonNullPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
serializer
|
|
||||||
)
|
|
||||||
|
|
||||||
private inline fun <T> ObservableSettings.createFlow(
|
|
||||||
key: String,
|
|
||||||
defaultValue: T,
|
|
||||||
crossinline getter: Settings.(String, T) -> T
|
|
||||||
): Flow<T> = callbackFlow {
|
|
||||||
offer(getter(key, defaultValue))
|
|
||||||
val listener = addListener(key) {
|
|
||||||
offer(getter(key, defaultValue))
|
|
||||||
}
|
|
||||||
awaitClose {
|
|
||||||
listener.deactivate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getLongFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getLongOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class LongPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: Long
|
|
||||||
): DefaultPreference<Long> {
|
|
||||||
override fun get() = settings.getLong(key, default)
|
|
||||||
override fun asFLow() = settings.getLongFlow(key, default)
|
|
||||||
override fun set(value: Long) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LongNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<Long> {
|
|
||||||
override fun get() = settings.getLongOrNull(key)
|
|
||||||
override fun asFLow() = settings.getLongOrNullFlow(key)
|
|
||||||
override fun set(value: Long?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getLongPreference(key: String, default: Long) = LongPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getLongPreference(key: String) = LongNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import ca.gosyer.util.system.asStateFlow
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
interface Preference {
|
|
||||||
/**
|
|
||||||
* The settings to watch for this preference. Must be a instance of [ObservableSettings]
|
|
||||||
* so that we can watch changes
|
|
||||||
*/
|
|
||||||
val settings: ObservableSettings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key for this preference
|
|
||||||
*/
|
|
||||||
val key: String
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface NullPreference <T>: Preference {
|
|
||||||
/**
|
|
||||||
* Returns the value stored at [key] as [T], or `null` if no value was stored. If a value of a different
|
|
||||||
* type was stored at `key`, the behavior is not defined.
|
|
||||||
*/
|
|
||||||
fun get(): T?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new [Flow], based on observing the given [key]. This flow will immediately emit the
|
|
||||||
* current value and then emit any subsequent values when the underlying `Settings` changes. When no value is present,
|
|
||||||
* `null` will be emitted instead.
|
|
||||||
*/
|
|
||||||
fun asFLow(): Flow<T?>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting
|
|
||||||
*/
|
|
||||||
fun asStateFlow(scope: CoroutineScope): StateFlow<T?> = asFLow().asStateFlow(get(), scope, true)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores a [T] value at [key], or remove what's there if [value] is null.
|
|
||||||
*/
|
|
||||||
fun set(value: T?)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DefaultPreference <T>: Preference {
|
|
||||||
val default: T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value stored at [key] as [T], or [default] if no value was stored. If a value of a different
|
|
||||||
* type was stored at `key`, the behavior is not defined.
|
|
||||||
*/
|
|
||||||
fun get(): T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new [Flow], based on observing the given [key]. This flow will immediately emit the
|
|
||||||
* current value and then emit any subsequent values when the underlying `Settings` changes. When no value is present,
|
|
||||||
* [default] will be emitted instead.
|
|
||||||
*/
|
|
||||||
fun asFLow(): Flow<T>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See [asFLow], this function is equilivent to that except in that it stores the latest value instead of emitting
|
|
||||||
*/
|
|
||||||
fun asStateFlow(scope: CoroutineScope): StateFlow<T> = asFLow().asStateFlow(get(), scope, true)
|
|
||||||
/**
|
|
||||||
* Stores the [T] [value] at [key].
|
|
||||||
*/
|
|
||||||
fun set(value: T)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.backend.preferences.impl
|
|
||||||
|
|
||||||
import com.russhwolf.settings.ObservableSettings
|
|
||||||
import com.russhwolf.settings.coroutines.getStringFlow
|
|
||||||
import com.russhwolf.settings.coroutines.getStringOrNullFlow
|
|
||||||
import com.russhwolf.settings.set
|
|
||||||
|
|
||||||
class StringPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
override val default: String
|
|
||||||
): DefaultPreference<String> {
|
|
||||||
override fun get() = settings.getString(key, default)
|
|
||||||
override fun asFLow() = settings.getStringFlow(key, default)
|
|
||||||
override fun set(value: String) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringNullPreference(
|
|
||||||
override val settings: ObservableSettings,
|
|
||||||
override val key: String,
|
|
||||||
): NullPreference<String> {
|
|
||||||
override fun get() = settings.getStringOrNull(key)
|
|
||||||
override fun asFLow() = settings.getStringOrNullFlow(key)
|
|
||||||
override fun set(value: String?) {
|
|
||||||
settings[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ObservableSettings.getStringPreference(key: String, default: String) = StringPreference(
|
|
||||||
this,
|
|
||||||
key,
|
|
||||||
default
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ObservableSettings.getStringPreference(key: String) = StringNullPreference(
|
|
||||||
this,
|
|
||||||
key
|
|
||||||
)
|
|
||||||
31
src/main/kotlin/ca/gosyer/common/di/AppScope.kt
Normal file
31
src/main/kotlin/ca/gosyer/common/di/AppScope.kt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.di
|
||||||
|
|
||||||
|
import toothpick.Scope
|
||||||
|
import toothpick.ktp.KTP
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The global scope for dependency injection that will provide all the application level components.
|
||||||
|
*/
|
||||||
|
object AppScope : Scope by KTP.openRootScope() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new subscope inheriting the root scope.
|
||||||
|
*/
|
||||||
|
fun subscope(any: Any): Scope {
|
||||||
|
return openSubScope(any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an instance of [T] from the root scope.
|
||||||
|
*/
|
||||||
|
inline fun <reified T> getInstance(): T {
|
||||||
|
return getInstance(T::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt
Normal file
17
src/main/kotlin/ca/gosyer/common/di/GenericsModule.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.di
|
||||||
|
|
||||||
|
import toothpick.Scope
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
|
class GenericsProvider<T>(private val cls: Class<T>, val scope: Scope = AppScope) : Provider<T> {
|
||||||
|
|
||||||
|
override fun get(): T {
|
||||||
|
return scope.getInstance(cls)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt
Normal file
16
src/main/kotlin/ca/gosyer/common/di/ModuleExtensions.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.di
|
||||||
|
|
||||||
|
import toothpick.config.Module
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the given [instance] to its class.
|
||||||
|
*/
|
||||||
|
inline fun <reified B> Module.bindInstance(instance: B) {
|
||||||
|
bind(B::class.java).toInstance(instance)
|
||||||
|
}
|
||||||
54
src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt
Normal file
54
src/main/kotlin/ca/gosyer/common/io/DataUriStringSource.kt
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.io
|
||||||
|
|
||||||
|
import ca.gosyer.common.util.decodeBase64
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.Source
|
||||||
|
import okio.Timeout
|
||||||
|
|
||||||
|
class DataUriStringSource(private val data: String) : Source {
|
||||||
|
|
||||||
|
private val timeout = Timeout()
|
||||||
|
|
||||||
|
private val headers = data.substringBefore(",")
|
||||||
|
|
||||||
|
private var pos = headers.length + 1
|
||||||
|
|
||||||
|
private val decoder: (Buffer, String) -> Long = if ("base64" in headers) {
|
||||||
|
{ sink, bytes ->
|
||||||
|
val decoded = bytes.decodeBase64()
|
||||||
|
sink.write(decoded)
|
||||||
|
decoded.size.toLong()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{ sink, bytes ->
|
||||||
|
val decoded = bytes.toByteArray()
|
||||||
|
sink.write(decoded)
|
||||||
|
decoded.size.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
|
if (pos >= data.length) return -1
|
||||||
|
|
||||||
|
val charsToRead = minOf(data.length - pos, byteCount.toInt())
|
||||||
|
val nextChars = data.substring(pos, pos + charsToRead)
|
||||||
|
|
||||||
|
pos += charsToRead
|
||||||
|
|
||||||
|
return decoder(sink, nextChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun timeout(): Timeout {
|
||||||
|
return timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt
Normal file
32
src/main/kotlin/ca/gosyer/common/io/OkioExtensions.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.io
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
suspend fun Source.saveTo(file: File) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
use { source ->
|
||||||
|
file.sink().buffer().use { it.writeAll(source) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Source.copyTo(sink: BufferedSink) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
use { source ->
|
||||||
|
sink.use { it.writeAll(source) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.prefs
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of a [PreferenceStore] which is initialized on first access. Useful when
|
||||||
|
* providing preference instances to classes that may not use them at all.
|
||||||
|
*/
|
||||||
|
class LazyPreferenceStore(
|
||||||
|
private val lazyStore: Lazy<PreferenceStore>
|
||||||
|
) : PreferenceStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [String] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getString(key: String, defaultValue: String): Preference<String> {
|
||||||
|
return lazyStore.value.getString(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Long] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
|
||||||
|
return lazyStore.value.getLong(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Int] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
|
||||||
|
return lazyStore.value.getInt(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Float] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
|
||||||
|
return lazyStore.value.getFloat(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Boolean] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
|
||||||
|
return lazyStore.value.getBoolean(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Set<String>] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||||
|
return lazyStore.value.getStringSet(key, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||||
|
* must be provided.
|
||||||
|
*/
|
||||||
|
override fun <T> getObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T
|
||||||
|
): Preference<T> {
|
||||||
|
return lazyStore.value.getObject(key, defaultValue, serializer, deserializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T> getJsonObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: KSerializer<T>,
|
||||||
|
serializersModule: SerializersModule
|
||||||
|
): Preference<T> {
|
||||||
|
return lazyStore.value.getJsonObject(key, defaultValue, serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/main/kotlin/ca/gosyer/common/prefs/Preference.kt
Normal file
61
src/main/kotlin/ca/gosyer/common/prefs/Preference.kt
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.prefs
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around application preferences without knowing implementation details. Instances of
|
||||||
|
* this interface must be provided through a [PreferenceStore].
|
||||||
|
*/
|
||||||
|
interface Preference<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key of this preference.
|
||||||
|
*/
|
||||||
|
fun key(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value of this preference.
|
||||||
|
*/
|
||||||
|
fun get(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new [value] for this preference.
|
||||||
|
*/
|
||||||
|
fun set(value: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether there's an existing entry for this preference.
|
||||||
|
*/
|
||||||
|
fun isSet(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the entry of this preference.
|
||||||
|
*/
|
||||||
|
fun delete()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default value of this preference.
|
||||||
|
*/
|
||||||
|
fun defaultValue(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cold [Flow] of this preference to receive updates when its value changes.
|
||||||
|
*/
|
||||||
|
fun changes(): Flow<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a hot [StateFlow] of this preference bound to the given [scope], allowing to read the
|
||||||
|
* current value and receive preference updates.
|
||||||
|
*/
|
||||||
|
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
||||||
|
|
||||||
|
}
|
||||||
87
src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt
Normal file
87
src/main/kotlin/ca/gosyer/common/prefs/PreferenceStore.kt
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.prefs
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.modules.EmptySerializersModule
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around an application preferences store. Implementations of this interface should
|
||||||
|
* persist these preferences on disk.
|
||||||
|
*/
|
||||||
|
interface PreferenceStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [String] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getString(key: String, defaultValue: String = ""): Preference<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Long] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getLong(key: String, defaultValue: Long = 0): Preference<Long>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Int] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getInt(key: String, defaultValue: Int = 0): Preference<Int>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Float] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getFloat(key: String, defaultValue: Float = 0f): Preference<Float>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Boolean] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getBoolean(key: String, defaultValue: Boolean = false): Preference<Boolean>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Set<String>] preference for this [key].
|
||||||
|
*/
|
||||||
|
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||||
|
* must be provided.
|
||||||
|
*/
|
||||||
|
fun <T> getObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T
|
||||||
|
): Preference<T>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns preference of type [T] for this [key]. The [serializer] must be provided.
|
||||||
|
*/
|
||||||
|
fun <T> getJsonObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: KSerializer<T>,
|
||||||
|
serializersModule: SerializersModule = EmptySerializersModule
|
||||||
|
): Preference<T>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an enum preference of type [T] for this [key].
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T
|
||||||
|
): Preference<T> {
|
||||||
|
return getObject(key, defaultValue, { it.name }, {
|
||||||
|
try {
|
||||||
|
enumValueOf(it)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.util.system
|
package ca.gosyer.common.util
|
||||||
|
|
||||||
import org.koin.core.context.GlobalContext
|
import okio.ByteString.Companion.decodeBase64
|
||||||
|
import okio.ByteString.Companion.encode
|
||||||
|
|
||||||
inline fun <reified T: Any> get() = GlobalContext.get().get<T>()
|
fun String.decodeBase64() = decodeBase64()!!
|
||||||
|
|
||||||
inline fun <reified T: Any> inject() = GlobalContext.get().inject<T>()
|
fun String.md5() = encode().md5().hex()
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new list that replaces the item at the given [position] with [newItem].
|
||||||
|
*/
|
||||||
|
fun <T> List<T>.replace(position: Int, newItem: T): List<T> {
|
||||||
|
val newList = toMutableList()
|
||||||
|
newList[position] = newItem
|
||||||
|
return newList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new list that replaces the first occurrence that matches the given [predicate] with
|
||||||
|
* [newItem]. If no item matches the predicate, the same list is returned (and unmodified).
|
||||||
|
*/
|
||||||
|
inline fun <T> List<T>.replaceFirst(predicate: (T) -> Boolean, newItem: T): List<T> {
|
||||||
|
forEachIndexed { index, element ->
|
||||||
|
if (predicate(element)) {
|
||||||
|
return replace(index, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the first item of this collection that matches the given [predicate].
|
||||||
|
*/
|
||||||
|
inline fun <T> MutableCollection<T>.removeFirst(predicate: (T) -> Boolean): T? {
|
||||||
|
val iter = iterator()
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
val element = iter.next()
|
||||||
|
if (predicate(element)) {
|
||||||
|
iter.remove()
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
43
src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt
Normal file
43
src/main/kotlin/ca/gosyer/common/util/ImageUtil.kt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.common.util
|
||||||
|
|
||||||
|
object ImageUtil {
|
||||||
|
|
||||||
|
private val jpgMagic = charByteArrayOf(0xFF, 0xD8, 0xFF)
|
||||||
|
private val pngMagic = charByteArrayOf(0x89, 0x50, 0x4E, 0x47)
|
||||||
|
private val gifMagic = "GIF8".toByteArray()
|
||||||
|
private val webpMagic = "RIFF".toByteArray()
|
||||||
|
|
||||||
|
fun findType(bytes: ByteArray): ImageType? {
|
||||||
|
return when {
|
||||||
|
bytes.compareWith(jpgMagic) -> ImageType.JPG
|
||||||
|
bytes.compareWith(pngMagic) -> ImageType.PNG
|
||||||
|
bytes.compareWith(gifMagic) -> ImageType.GIF
|
||||||
|
bytes.compareWith(webpMagic) -> ImageType.WEBP
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
|
||||||
|
for (i in magic.indices) {
|
||||||
|
if (this[i] != magic[i]) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
|
||||||
|
return ByteArray(bytes.size) { pos -> bytes[pos].toByte() }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ImageType(val mime: String, val extension: String) {
|
||||||
|
JPG("image/jpeg", "jpg"),
|
||||||
|
PNG("image/png", "png"),
|
||||||
|
GIF("image/gif", "gif"),
|
||||||
|
WEBP("image/webp", "webp")
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt
Normal file
99
src/main/kotlin/ca/gosyer/core/prefs/JvmPreference.kt
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.core.prefs
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import com.russhwolf.settings.ObservableSettings
|
||||||
|
import com.russhwolf.settings.contains
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
|
internal class JvmPreference<T>(
|
||||||
|
private val preferences: ObservableSettings,
|
||||||
|
private val key: String,
|
||||||
|
private val defaultValue: T,
|
||||||
|
private val adapter: Adapter<T>
|
||||||
|
) : Preference<T> {
|
||||||
|
|
||||||
|
interface Adapter<T> {
|
||||||
|
fun get(key: String, preferences: ObservableSettings): T
|
||||||
|
|
||||||
|
fun set(key: String, value: T, editor: ObservableSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key of this preference.
|
||||||
|
*/
|
||||||
|
override fun key(): String {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value of this preference.
|
||||||
|
*/
|
||||||
|
override fun get(): T {
|
||||||
|
return if (!preferences.contains(key)) {
|
||||||
|
defaultValue
|
||||||
|
} else {
|
||||||
|
adapter.get(key, preferences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new [value] for this preference.
|
||||||
|
*/
|
||||||
|
override fun set(value: T) {
|
||||||
|
adapter.set(key, value, preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether there's an existing entry for this preference.
|
||||||
|
*/
|
||||||
|
override fun isSet(): Boolean {
|
||||||
|
return preferences.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the entry of this preference.
|
||||||
|
*/
|
||||||
|
override fun delete() {
|
||||||
|
preferences.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default value of this preference
|
||||||
|
*/
|
||||||
|
override fun defaultValue(): T {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cold [Flow] of this preference to receive updates when its value changes.
|
||||||
|
*/
|
||||||
|
override fun changes(): Flow<T> {
|
||||||
|
return callbackFlow {
|
||||||
|
val listener = preferences.addListener(key) {
|
||||||
|
offer(get())
|
||||||
|
}
|
||||||
|
awaitClose { listener.deactivate() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a hot [StateFlow] of this preference bound to the given [scope], allowing to read the
|
||||||
|
* current value and receive preference updates.
|
||||||
|
*/
|
||||||
|
override fun stateIn(scope: CoroutineScope): StateFlow<T> {
|
||||||
|
return changes().stateIn(scope, SharingStarted.Eagerly, get())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
107
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt
Normal file
107
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceAdapters.kt
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.core.prefs
|
||||||
|
|
||||||
|
import com.russhwolf.settings.ObservableSettings
|
||||||
|
import com.russhwolf.settings.serialization.decodeValue
|
||||||
|
import com.russhwolf.settings.serialization.encodeValue
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.builtins.SetSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.modules.EmptySerializersModule
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
|
||||||
|
internal object StringAdapter : JvmPreference.Adapter<String> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): String {
|
||||||
|
return preferences.getString(key) // Not called unless key is present.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: String, editor: ObservableSettings) {
|
||||||
|
editor.putString(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object LongAdapter : JvmPreference.Adapter<Long> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): Long {
|
||||||
|
return preferences.getLong(key, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: Long, editor: ObservableSettings) {
|
||||||
|
editor.putLong(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object IntAdapter : JvmPreference.Adapter<Int> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): Int {
|
||||||
|
return preferences.getInt(key, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: Int, editor: ObservableSettings) {
|
||||||
|
editor.putInt(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object FloatAdapter : JvmPreference.Adapter<Float> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): Float {
|
||||||
|
return preferences.getFloat(key, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: Float, editor: ObservableSettings) {
|
||||||
|
editor.putFloat(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object BooleanAdapter : JvmPreference.Adapter<Boolean> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): Boolean {
|
||||||
|
return preferences.getBoolean(key, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: Boolean, editor: ObservableSettings) {
|
||||||
|
editor.putBoolean(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object StringSetAdapter : JvmPreference.Adapter<Set<String>> {
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): Set<String> {
|
||||||
|
return preferences.decodeValue(SetSerializer(String.serializer()), key, emptySet()) // Not called unless key is present.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: Set<String>, editor: ObservableSettings) {
|
||||||
|
editor.encodeValue(SetSerializer(String.serializer()), key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ObjectAdapter<T>(
|
||||||
|
private val serializer: (T) -> String,
|
||||||
|
private val deserializer: (String) -> T
|
||||||
|
) : JvmPreference.Adapter<T> {
|
||||||
|
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): T {
|
||||||
|
return deserializer(preferences.getString(key)) // Not called unless key is present.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: T, editor: ObservableSettings) {
|
||||||
|
editor.putString(key, serializer(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class JsonObjectAdapter<T>(
|
||||||
|
private val defaultValue: T,
|
||||||
|
private val serializer: KSerializer<T>,
|
||||||
|
private val serializersModule: SerializersModule = EmptySerializersModule
|
||||||
|
) : JvmPreference.Adapter<T> {
|
||||||
|
|
||||||
|
override fun get(key: String, preferences: ObservableSettings): T {
|
||||||
|
return preferences.decodeValue(serializer, key, defaultValue, serializersModule) // Not called unless key is present.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(key: String, value: T, editor: ObservableSettings) {
|
||||||
|
editor.encodeValue(serializer, key, value, serializersModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt
Normal file
86
src/main/kotlin/ca/gosyer/core/prefs/JvmPreferenceStore.kt
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.core.prefs
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
import com.russhwolf.settings.ObservableSettings
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
|
||||||
|
class JvmPreferenceStore(private val preferences: ObservableSettings) : PreferenceStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [String] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getString(key: String, defaultValue: String): Preference<String> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, StringAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Long] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, LongAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Int] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, IntAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Float] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, FloatAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Boolean] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, BooleanAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [Set<String>] preference for this [key].
|
||||||
|
*/
|
||||||
|
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||||
|
return JvmPreference(preferences, key, defaultValue, StringSetAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function
|
||||||
|
* must be provided.
|
||||||
|
*/
|
||||||
|
override fun <T> getObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: (T) -> String,
|
||||||
|
deserializer: (String) -> T
|
||||||
|
): Preference<T> {
|
||||||
|
val adapter = ObjectAdapter(serializer, deserializer)
|
||||||
|
return JvmPreference(preferences, key, defaultValue, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns preference of type [T] for this [key]. The [serializer] must be provided.
|
||||||
|
*/
|
||||||
|
override fun <T> getJsonObject(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T,
|
||||||
|
serializer: KSerializer<T>,
|
||||||
|
serializersModule: SerializersModule
|
||||||
|
): Preference<T> {
|
||||||
|
val adapter = JsonObjectAdapter(defaultValue, serializer, serializersModule)
|
||||||
|
return JvmPreference(preferences, key, defaultValue, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.core.prefs
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
import com.russhwolf.settings.JvmPreferencesSettings
|
||||||
|
import java.util.prefs.Preferences
|
||||||
|
|
||||||
|
class PreferenceStoreFactory {
|
||||||
|
|
||||||
|
fun create(name: String? = null): PreferenceStore {
|
||||||
|
val jvmPreferences = if (!name.isNullOrBlank()) {
|
||||||
|
JvmPreferencesSettings(Preferences.userRoot().node(name))
|
||||||
|
} else {
|
||||||
|
JvmPreferencesSettings(Preferences.userRoot())
|
||||||
|
}
|
||||||
|
return JvmPreferenceStore(jvmPreferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
src/main/kotlin/ca/gosyer/data/DataModule.kt
Normal file
67
src/main/kotlin/ca/gosyer/data/DataModule.kt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data
|
||||||
|
|
||||||
|
import ca.gosyer.core.prefs.PreferenceStoreFactory
|
||||||
|
import ca.gosyer.data.catalog.CatalogPreferences
|
||||||
|
import ca.gosyer.data.extension.ExtensionPreferences
|
||||||
|
import ca.gosyer.data.library.LibraryPreferences
|
||||||
|
import ca.gosyer.data.server.Http
|
||||||
|
import ca.gosyer.data.server.HttpProvider
|
||||||
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
|
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||||
|
import ca.gosyer.data.ui.UiPreferences
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import toothpick.ktp.binding.bind
|
||||||
|
import toothpick.ktp.binding.module
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
val DataModule = module {
|
||||||
|
val preferenceFactory = PreferenceStoreFactory()
|
||||||
|
|
||||||
|
bind<ServerPreferences>()
|
||||||
|
.toProviderInstance { ServerPreferences(preferenceFactory.create("server")) }
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<ExtensionPreferences>()
|
||||||
|
.toProviderInstance { ExtensionPreferences(preferenceFactory.create("extension")) }
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<CatalogPreferences>()
|
||||||
|
.toProviderInstance { CatalogPreferences(preferenceFactory.create("catalog")) }
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<LibraryPreferences>()
|
||||||
|
.toProviderInstance { LibraryPreferences(preferenceFactory.create("library")) }
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<UiPreferences>()
|
||||||
|
.toProviderInstance { UiPreferences(preferenceFactory.create("ui")) }
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<Http>()
|
||||||
|
.toProvider(HttpProvider::class)
|
||||||
|
.providesSingleton()
|
||||||
|
|
||||||
|
bind<CategoryInteractionHandler>()
|
||||||
|
.toClass<CategoryInteractionHandler>()
|
||||||
|
bind<ChapterInteractionHandler>()
|
||||||
|
.toClass<ChapterInteractionHandler>()
|
||||||
|
bind<ExtensionInteractionHandler>()
|
||||||
|
.toClass<ExtensionInteractionHandler>()
|
||||||
|
bind<LibraryInteractionHandler>()
|
||||||
|
.toClass<LibraryInteractionHandler>()
|
||||||
|
bind<MangaInteractionHandler>()
|
||||||
|
.toClass<MangaInteractionHandler>()
|
||||||
|
bind<SourceInteractionHandler>()
|
||||||
|
.toClass<SourceInteractionHandler>()
|
||||||
|
}
|
||||||
16
src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt
Normal file
16
src/main/kotlin/ca/gosyer/data/catalog/CatalogPreferences.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.catalog
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
|
||||||
|
class CatalogPreferences(private val preferenceStore: PreferenceStore) {
|
||||||
|
fun languages(): Preference<Set<String>> {
|
||||||
|
return preferenceStore.getStringSet("enabled_langs", setOf("en"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.extension
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
|
||||||
|
class ExtensionPreferences(private val preferenceStore: PreferenceStore) {
|
||||||
|
fun languages(): Preference<Set<String>> {
|
||||||
|
return preferenceStore.getStringSet("enabled_langs", setOf("all", "en"))
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt
Normal file
18
src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.library
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
import ca.gosyer.data.library.model.DisplayMode
|
||||||
|
|
||||||
|
class LibraryPreferences(private val preferenceStore: PreferenceStore) {
|
||||||
|
|
||||||
|
fun displayMode(): Preference<DisplayMode> {
|
||||||
|
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt
Normal file
20
src/main/kotlin/ca/gosyer/data/library/model/DisplayMode.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.library.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class DisplayMode {
|
||||||
|
CompactGrid,
|
||||||
|
ComfortableGrid,
|
||||||
|
List;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val values = values()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -21,4 +21,6 @@ data class Chapter(
|
|||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
val pageCount: Int? = null,
|
val pageCount: Int? = null,
|
||||||
|
val chapterIndex: Int,
|
||||||
|
val chapterCount: Int
|
||||||
)
|
)
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.models
|
package ca.gosyer.data.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -4,18 +4,21 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network
|
package ca.gosyer.data.server
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.features.json.JsonFeature
|
import io.ktor.client.features.json.JsonFeature
|
||||||
import io.ktor.client.features.logging.LogLevel
|
import io.ktor.client.features.logging.LogLevel
|
||||||
import io.ktor.client.features.logging.Logging
|
import io.ktor.client.features.logging.Logging
|
||||||
import org.koin.dsl.module
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
val networkModule = module {
|
typealias Http = HttpClient
|
||||||
single {
|
|
||||||
HttpClient(OkHttp) {
|
internal class HttpProvider @Inject constructor() : Provider<Http> {
|
||||||
|
override fun get(): Http {
|
||||||
|
return HttpClient(OkHttp) {
|
||||||
install(JsonFeature)
|
install(JsonFeature)
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
level = LogLevel.INFO
|
level = LogLevel.INFO
|
||||||
16
src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt
Normal file
16
src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.server
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
|
||||||
|
class ServerPreferences(private val preferenceStore: PreferenceStore) {
|
||||||
|
fun server(): Preference<String> {
|
||||||
|
return preferenceStore.getString("server_url", "http://localhost:4567")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,11 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.util.system.inject
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import io.ktor.client.request.delete
|
import io.ktor.client.request.delete
|
||||||
import io.ktor.client.request.forms.submitForm
|
import io.ktor.client.request.forms.submitForm
|
||||||
@@ -19,11 +18,14 @@ import io.ktor.client.request.post
|
|||||||
import io.ktor.http.Parameters
|
import io.ktor.http.Parameters
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
open class BaseInteractionHandler {
|
open class BaseInteractionHandler(
|
||||||
val preferences: PreferenceHelper by inject()
|
protected val client: Http,
|
||||||
val serverUrl get() = preferences.serverUrl.get()
|
serverPreferences: ServerPreferences
|
||||||
|
) {
|
||||||
|
private val _serverUrl = serverPreferences.server()
|
||||||
|
val serverUrl get() = _serverUrl.get()
|
||||||
|
|
||||||
protected suspend inline fun <reified T> HttpClient.getRepeat(
|
protected suspend inline fun <reified T> Http.getRepeat(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
block: HttpRequestBuilder.() -> Unit = {}
|
block: HttpRequestBuilder.() -> Unit = {}
|
||||||
): T {
|
): T {
|
||||||
@@ -41,7 +43,7 @@ open class BaseInteractionHandler {
|
|||||||
throw lastException
|
throw lastException
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> HttpClient.deleteRepeat(
|
protected suspend inline fun <reified T> Http.deleteRepeat(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
block: HttpRequestBuilder.() -> Unit = {}
|
block: HttpRequestBuilder.() -> Unit = {}
|
||||||
): T {
|
): T {
|
||||||
@@ -59,7 +61,7 @@ open class BaseInteractionHandler {
|
|||||||
throw lastException
|
throw lastException
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> HttpClient.patchRepeat(
|
protected suspend inline fun <reified T> Http.patchRepeat(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
block: HttpRequestBuilder.() -> Unit = {}
|
block: HttpRequestBuilder.() -> Unit = {}
|
||||||
): T {
|
): T {
|
||||||
@@ -77,7 +79,7 @@ open class BaseInteractionHandler {
|
|||||||
throw lastException
|
throw lastException
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> HttpClient.postRepeat(
|
protected suspend inline fun <reified T> Http.postRepeat(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
block: HttpRequestBuilder.() -> Unit = {}
|
block: HttpRequestBuilder.() -> Unit = {}
|
||||||
): T {
|
): T {
|
||||||
@@ -95,7 +97,7 @@ open class BaseInteractionHandler {
|
|||||||
throw lastException
|
throw lastException
|
||||||
}
|
}
|
||||||
|
|
||||||
protected suspend inline fun <reified T> HttpClient.submitFormRepeat(
|
protected suspend inline fun <reified T> Http.submitFormRepeat(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
formParameters: Parameters = Parameters.Empty,
|
formParameters: Parameters = Parameters.Empty,
|
||||||
encodeInQuery: Boolean = false,
|
encodeInQuery: Boolean = false,
|
||||||
@@ -115,7 +117,7 @@ open class BaseInteractionHandler {
|
|||||||
throw lastException
|
throw lastException
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun imageFromUrl(client: HttpClient, imageUrl: String): ImageBitmap {
|
suspend fun imageFromUrl(client: Http, imageUrl: String): ImageBitmap {
|
||||||
var attempt = 1
|
var attempt = 1
|
||||||
var lastException: Exception
|
var lastException: Exception
|
||||||
do {
|
do {
|
||||||
@@ -4,27 +4,32 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Category
|
import ca.gosyer.data.models.Category
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.requests.addMangaToCategoryQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.categoryDeleteRequest
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.requests.categoryModifyRequest
|
import ca.gosyer.data.server.requests.addMangaToCategoryQuery
|
||||||
import ca.gosyer.backend.network.requests.categoryReorderRequest
|
import ca.gosyer.data.server.requests.categoryDeleteRequest
|
||||||
import ca.gosyer.backend.network.requests.createCategoryRequest
|
import ca.gosyer.data.server.requests.categoryModifyRequest
|
||||||
import ca.gosyer.backend.network.requests.getCategoriesQuery
|
import ca.gosyer.data.server.requests.categoryReorderRequest
|
||||||
import ca.gosyer.backend.network.requests.getMangaCategoriesQuery
|
import ca.gosyer.data.server.requests.createCategoryRequest
|
||||||
import ca.gosyer.backend.network.requests.getMangaInCategoryQuery
|
import ca.gosyer.data.server.requests.getCategoriesQuery
|
||||||
import ca.gosyer.backend.network.requests.removeMangaFromCategoryRequest
|
import ca.gosyer.data.server.requests.getMangaCategoriesQuery
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.getMangaInCategoryQuery
|
||||||
|
import ca.gosyer.data.server.requests.removeMangaFromCategoryRequest
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.http.HttpMethod
|
import io.ktor.http.HttpMethod
|
||||||
import io.ktor.http.Parameters
|
import io.ktor.http.Parameters
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CategoryInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class CategoryInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getMangaCategories(mangaId: Long) = withContext(Dispatchers.IO) {
|
suspend fun getMangaCategories(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<List<Category>>(
|
client.getRepeat<List<Category>>(
|
||||||
@@ -4,18 +4,23 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Chapter
|
import ca.gosyer.data.models.Chapter
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.requests.getChapterQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.getMangaChaptersQuery
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.requests.getPageQuery
|
import ca.gosyer.data.server.requests.getChapterQuery
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.getMangaChaptersQuery
|
||||||
|
import ca.gosyer.data.server.requests.getPageQuery
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ChapterInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class ChapterInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) {
|
suspend fun getChapters(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<List<Chapter>>(
|
client.getRepeat<List<Chapter>>(
|
||||||
@@ -4,19 +4,24 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Extension
|
import ca.gosyer.data.models.Extension
|
||||||
import ca.gosyer.backend.network.requests.apkIconQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.apkInstallQuery
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.requests.apkUninstallQuery
|
import ca.gosyer.data.server.requests.apkIconQuery
|
||||||
import ca.gosyer.backend.network.requests.extensionListQuery
|
import ca.gosyer.data.server.requests.apkInstallQuery
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.apkUninstallQuery
|
||||||
|
import ca.gosyer.data.server.requests.extensionListQuery
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ExtensionInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class ExtensionInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getExtensionList() = withContext(Dispatchers.IO) {
|
suspend fun getExtensionList() = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<List<Extension>>(
|
client.getRepeat<List<Extension>>(
|
||||||
@@ -4,18 +4,23 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.requests.addMangaToLibraryQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.getLibraryQuery
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.requests.removeMangaFromLibraryRequest
|
import ca.gosyer.data.server.requests.addMangaToLibraryQuery
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.getLibraryQuery
|
||||||
|
import ca.gosyer.data.server.requests.removeMangaFromLibraryRequest
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LibraryInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class LibraryInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getLibraryManga() = withContext(Dispatchers.IO) {
|
suspend fun getLibraryManga() = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<List<Manga>>(
|
client.getRepeat<List<Manga>>(
|
||||||
@@ -4,16 +4,21 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.requests.mangaQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.mangaThumbnailQuery
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.mangaQuery
|
||||||
|
import ca.gosyer.data.server.requests.mangaThumbnailQuery
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MangaInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class MangaInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) {
|
suspend fun getManga(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<Manga>(
|
client.getRepeat<Manga>(
|
||||||
@@ -4,23 +4,28 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.interactions
|
package ca.gosyer.data.server.interactions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.MangaPage
|
import ca.gosyer.data.models.MangaPage
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.backend.network.requests.getFilterListQuery
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.backend.network.requests.globalSearchQuery
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.requests.sourceInfoQuery
|
import ca.gosyer.data.server.requests.getFilterListQuery
|
||||||
import ca.gosyer.backend.network.requests.sourceLatestQuery
|
import ca.gosyer.data.server.requests.globalSearchQuery
|
||||||
import ca.gosyer.backend.network.requests.sourceListQuery
|
import ca.gosyer.data.server.requests.sourceInfoQuery
|
||||||
import ca.gosyer.backend.network.requests.sourcePopularQuery
|
import ca.gosyer.data.server.requests.sourceLatestQuery
|
||||||
import ca.gosyer.backend.network.requests.sourceSearchQuery
|
import ca.gosyer.data.server.requests.sourceListQuery
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.requests.sourcePopularQuery
|
||||||
|
import ca.gosyer.data.server.requests.sourceSearchQuery
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SourceInteractionHandler(private val client: HttpClient): BaseInteractionHandler() {
|
class SourceInteractionHandler @Inject constructor(
|
||||||
|
client: Http,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): BaseInteractionHandler(client, serverPreferences) {
|
||||||
|
|
||||||
suspend fun getSourceList() = withContext(Dispatchers.IO) {
|
suspend fun getSourceList() = withContext(Dispatchers.IO) {
|
||||||
client.getRepeat<List<Source>>(
|
client.getRepeat<List<Source>>(
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun getMangaCategoriesQuery(mangaId: Long) =
|
fun getMangaCategoriesQuery(mangaId: Long) =
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun getMangaChaptersQuery(mangaId: Long) =
|
fun getMangaChaptersQuery(mangaId: Long) =
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun extensionListQuery() =
|
fun extensionListQuery() =
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun addMangaToLibraryQuery(mangaId: Long) =
|
fun addMangaToLibraryQuery(mangaId: Long) =
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun mangaQuery(mangaId: Long) =
|
fun mangaQuery(mangaId: Long) =
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
annotation class Get
|
annotation class Get
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ca.gosyer.backend.network.requests
|
package ca.gosyer.data.server.requests
|
||||||
|
|
||||||
@Get
|
@Get
|
||||||
fun sourceListQuery() =
|
fun sourceListQuery() =
|
||||||
64
src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt
Normal file
64
src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.ui
|
||||||
|
|
||||||
|
import ca.gosyer.common.prefs.Preference
|
||||||
|
import ca.gosyer.common.prefs.PreferenceStore
|
||||||
|
import ca.gosyer.common.prefs.getEnum
|
||||||
|
import ca.gosyer.data.ui.model.ThemeMode
|
||||||
|
|
||||||
|
class UiPreferences(private val preferenceStore: PreferenceStore) {
|
||||||
|
|
||||||
|
fun themeMode(): Preference<ThemeMode> {
|
||||||
|
return preferenceStore.getEnum("theme_mode", ThemeMode.System)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lightTheme(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("theme_light", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun darkTheme(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("theme_dark", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorPrimaryLight(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_primary_light", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorPrimaryDark(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_primary_dark", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorSecondaryLight(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_secondary_light", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorSecondaryDark(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_secondary_dark", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorBarsLight(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_bar_light", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorBarsDark(): Preference<Int> {
|
||||||
|
return preferenceStore.getInt("color_bar_dark", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmExit(): Preference<Boolean> {
|
||||||
|
return preferenceStore.getBoolean("confirm_exit", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun language(): Preference<String> {
|
||||||
|
return preferenceStore.getString("language", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dateFormat(): Preference<String> {
|
||||||
|
return preferenceStore.getString("date_format", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt
Normal file
13
src/main/kotlin/ca/gosyer/data/ui/model/ThemeMode.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.data.ui.model
|
||||||
|
|
||||||
|
enum class ThemeMode {
|
||||||
|
System,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -22,7 +23,8 @@ import kotlin.random.Random
|
|||||||
fun ErrorScreen(errorMessage: String? = null) {
|
fun ErrorScreen(errorMessage: String? = null) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||||
Text(getRandomErrorFace(), fontSize = 36.sp, color = MaterialTheme.colors.onBackground)
|
val errorFace = remember { getRandomErrorFace() }
|
||||||
|
Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground)
|
||||||
if (errorMessage != null) {
|
if (errorMessage != null) {
|
||||||
Text(errorMessage, color = MaterialTheme.colors.onBackground)
|
Text(errorMessage, color = MaterialTheme.colors.onBackground)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.graphics.DefaultAlpha
|
import androidx.compose.ui.graphics.DefaultAlpha
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import ca.gosyer.common.di.AppScope
|
||||||
|
import ca.gosyer.data.server.Http
|
||||||
import ca.gosyer.util.compose.imageFromUrl
|
import ca.gosyer.util.compose.imageFromUrl
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -29,7 +30,6 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KtorImage(
|
fun KtorImage(
|
||||||
client: HttpClient,
|
|
||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
imageModifier: Modifier = Modifier.fillMaxSize(),
|
imageModifier: Modifier = Modifier.fillMaxSize(),
|
||||||
loadingModifier: Modifier = imageModifier,
|
loadingModifier: Modifier = imageModifier,
|
||||||
@@ -38,14 +38,18 @@ fun KtorImage(
|
|||||||
contentScale: ContentScale = ContentScale.Fit,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
alpha: Float = DefaultAlpha,
|
alpha: Float = DefaultAlpha,
|
||||||
colorFilter: ColorFilter? = null,
|
colorFilter: ColorFilter? = null,
|
||||||
retries: Int = 3
|
retries: Int = 3,
|
||||||
|
httpClient: Http? = null
|
||||||
) {
|
) {
|
||||||
|
val client = remember { httpClient ?: AppScope.getInstance() }
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) }
|
val drawable: MutableState<ImageBitmap?> = remember { mutableStateOf(null) }
|
||||||
val loading: MutableState<Boolean> = remember { mutableStateOf(true) }
|
val loading: MutableState<Boolean> = remember { mutableStateOf(true) }
|
||||||
|
val error: MutableState<String?> = remember { mutableStateOf(null) }
|
||||||
DisposableEffect(imageUrl) {
|
DisposableEffect(imageUrl) {
|
||||||
val handler = CoroutineExceptionHandler { _, _ ->
|
val handler = CoroutineExceptionHandler { _, throwable ->
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
error.value = throwable.message
|
||||||
}
|
}
|
||||||
val job = GlobalScope.launch(handler) {
|
val job = GlobalScope.launch(handler) {
|
||||||
if (drawable.value == null) {
|
if (drawable.value == null) {
|
||||||
@@ -72,12 +76,12 @@ fun KtorImage(
|
|||||||
colorFilter = colorFilter
|
colorFilter = colorFilter
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LoadingScreen(loading.value, loadingModifier)
|
LoadingScreen(loading.value, loadingModifier, error.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getImage(client: HttpClient, imageUrl: String, retries: Int = 3): ImageBitmap {
|
private suspend fun getImage(client: Http, imageUrl: String, retries: Int = 3): ImageBitmap {
|
||||||
var attempt = 1
|
var attempt = 1
|
||||||
var lastException: Exception
|
var lastException: Exception
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.min
|
import androidx.compose.ui.unit.min
|
||||||
@@ -23,7 +24,10 @@ fun LoadingScreen(
|
|||||||
) {
|
) {
|
||||||
BoxWithConstraints(modifier) {
|
BoxWithConstraints(modifier) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
CircularProgressIndicator(Modifier.align(Alignment.Center).size(min(maxHeight, maxWidth) / 2))
|
val size = remember(maxHeight, maxWidth) {
|
||||||
|
min(maxHeight, maxWidth) / 2
|
||||||
|
}
|
||||||
|
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size))
|
||||||
} else {
|
} else {
|
||||||
ErrorScreen(errorMessage)
|
ErrorScreen(errorMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaGridItem(
|
fun MangaGridItem(
|
||||||
@@ -53,7 +52,7 @@ fun MangaGridItem(
|
|||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
KtorImage(get(), cover, contentScale = ContentScale.Crop)
|
KtorImage(cover, contentScale = ContentScale.Crop)
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
|
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -7,14 +7,18 @@
|
|||||||
package ca.gosyer.ui.base.vm
|
package ca.gosyer.ui.base.vm
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisallowComposableCalls
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import org.koin.core.context.GlobalContext
|
import ca.gosyer.common.di.AppScope
|
||||||
|
import toothpick.Toothpick
|
||||||
|
import toothpick.ktp.binding.module
|
||||||
|
import toothpick.ktp.extension.getInstance
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <reified VM : ViewModel> composeViewModel(): VM {
|
inline fun <reified VM : ViewModel> viewModel(): VM {
|
||||||
val viewModel = remember {
|
val viewModel = remember {
|
||||||
GlobalContext.get().get<VM>()
|
AppScope.getInstance<VM>()
|
||||||
}
|
}
|
||||||
DisposableEffect(viewModel) {
|
DisposableEffect(viewModel) {
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -23,3 +27,26 @@ inline fun <reified VM : ViewModel> composeViewModel(): VM {
|
|||||||
}
|
}
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun <reified VM : ViewModel> viewModel(
|
||||||
|
crossinline binding: @DisallowComposableCalls () -> Any,
|
||||||
|
): VM {
|
||||||
|
val (viewModel, submodule) = remember {
|
||||||
|
val submodule = module {
|
||||||
|
binding().let { bind(it.javaClass).toInstance(it) }
|
||||||
|
}
|
||||||
|
val subscope = AppScope.subscope(submodule).also {
|
||||||
|
it.installModules(submodule)
|
||||||
|
}
|
||||||
|
val viewModel = subscope.getInstance<VM>()
|
||||||
|
Pair(viewModel, submodule)
|
||||||
|
}
|
||||||
|
DisposableEffect(viewModel) {
|
||||||
|
onDispose {
|
||||||
|
viewModel.destroy()
|
||||||
|
Toothpick.closeScope(submodule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return viewModel
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ca.gosyer.ui.base.vm
|
|
||||||
|
|
||||||
import ca.gosyer.ui.categories.CategoriesMenuViewModel
|
|
||||||
import ca.gosyer.ui.extensions.ExtensionsMenuViewModel
|
|
||||||
import ca.gosyer.ui.library.LibraryScreenViewModel
|
|
||||||
import ca.gosyer.ui.main.MainViewModel
|
|
||||||
import ca.gosyer.ui.manga.MangaMenuViewModel
|
|
||||||
import ca.gosyer.ui.sources.SourcesMenuViewModel
|
|
||||||
import ca.gosyer.ui.sources.components.SourceScreenViewModel
|
|
||||||
import org.koin.dsl.module
|
|
||||||
|
|
||||||
val viewModelModule = module {
|
|
||||||
factory { MainViewModel() }
|
|
||||||
factory { ExtensionsMenuViewModel() }
|
|
||||||
factory { SourcesMenuViewModel() }
|
|
||||||
factory { SourceScreenViewModel() }
|
|
||||||
factory { MangaMenuViewModel() }
|
|
||||||
factory { LibraryScreenViewModel() }
|
|
||||||
factory { CategoriesMenuViewModel() }
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.util.compose.ThemedWindow
|
import ca.gosyer.util.compose.ThemedWindow
|
||||||
|
|
||||||
fun openCategoriesMenu() {
|
fun openCategoriesMenu() {
|
||||||
@@ -51,7 +51,7 @@ fun openCategoriesMenu() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoriesMenu(windowEvents: WindowEvents) {
|
fun CategoriesMenu(windowEvents: WindowEvents) {
|
||||||
val vm = composeViewModel<CategoriesMenuViewModel>()
|
val vm = viewModel<CategoriesMenuViewModel>()
|
||||||
val categories by vm.categories.collectAsState()
|
val categories by vm.categories.collectAsState()
|
||||||
remember {
|
remember {
|
||||||
windowEvents.onClose = { vm.updateCategories() }
|
windowEvents.onClose = { vm.updateCategories() }
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.categories
|
package ca.gosyer.ui.categories
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Category
|
import ca.gosyer.data.models.Category
|
||||||
import ca.gosyer.backend.network.interactions.CategoryInteractionHandler
|
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -18,9 +16,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CategoriesMenuViewModel : ViewModel() {
|
class CategoriesMenuViewModel @Inject constructor(
|
||||||
private val httpClient: HttpClient by inject()
|
private val categoryHandler: CategoryInteractionHandler
|
||||||
|
) : ViewModel() {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private var originalCategories = emptyList<Category>()
|
private var originalCategories = emptyList<Category>()
|
||||||
private val _categories = MutableStateFlow(emptyList<MenuCategory>())
|
private val _categories = MutableStateFlow(emptyList<MenuCategory>())
|
||||||
@@ -38,7 +38,7 @@ class CategoriesMenuViewModel : ViewModel() {
|
|||||||
_categories.value = emptyList()
|
_categories.value = emptyList()
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
try {
|
try {
|
||||||
_categories.value = CategoryInteractionHandler(httpClient).getCategories()
|
_categories.value = categoryHandler.getCategories()
|
||||||
.sortedBy { it.order }
|
.sortedBy { it.order }
|
||||||
.also { originalCategories = it }
|
.also { originalCategories = it }
|
||||||
.map { it.toMenuCategory() }
|
.map { it.toMenuCategory() }
|
||||||
@@ -58,22 +58,22 @@ class CategoriesMenuViewModel : ViewModel() {
|
|||||||
val categories = _categories.value
|
val categories = _categories.value
|
||||||
val newCategories = categories.filter { it.id == null }
|
val newCategories = categories.filter { it.id == null }
|
||||||
newCategories.forEach {
|
newCategories.forEach {
|
||||||
CategoryInteractionHandler(httpClient).createCategory(it.name)
|
categoryHandler.createCategory(it.name)
|
||||||
}
|
}
|
||||||
originalCategories.forEach { originalCategory ->
|
originalCategories.forEach { originalCategory ->
|
||||||
val category = categories.find { it.id == originalCategory.id }
|
val category = categories.find { it.id == originalCategory.id }
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
CategoryInteractionHandler(httpClient).deleteCategory(originalCategory)
|
categoryHandler.deleteCategory(originalCategory)
|
||||||
} else if (category.name != originalCategory.name) {
|
} else if (category.name != originalCategory.name) {
|
||||||
CategoryInteractionHandler(httpClient).modifyCategory(originalCategory, category.name)
|
categoryHandler.modifyCategory(originalCategory, category.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val updatedCategories = CategoryInteractionHandler(httpClient).getCategories()
|
val updatedCategories = categoryHandler.getCategories()
|
||||||
updatedCategories.forEach { updatedCategory ->
|
updatedCategories.forEach { updatedCategory ->
|
||||||
val category = categories.find { it.id == updatedCategory.id || it.name == updatedCategory.name } ?: return@forEach
|
val category = categories.find { it.id == updatedCategory.id || it.name == updatedCategory.name } ?: return@forEach
|
||||||
if (category.order != updatedCategory.order) {
|
if (category.order != updatedCategory.order) {
|
||||||
logger.debug { "${category.order} to ${updatedCategory.order}" }
|
logger.debug { "${category.order} to ${updatedCategory.order}" }
|
||||||
CategoryInteractionHandler(httpClient).reorderCategory(updatedCategory, category.order, updatedCategory.order)
|
categoryHandler.reorderCategory(updatedCategory, category.order, updatedCategory.order)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,11 @@ import androidx.compose.ui.text.withStyle
|
|||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ca.gosyer.backend.models.Extension
|
import ca.gosyer.data.models.Extension
|
||||||
import ca.gosyer.ui.base.components.KtorImage
|
import ca.gosyer.ui.base.components.KtorImage
|
||||||
import ca.gosyer.ui.base.components.LoadingScreen
|
import ca.gosyer.ui.base.components.LoadingScreen
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.util.compose.ThemedWindow
|
import ca.gosyer.util.compose.ThemedWindow
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
|
|
||||||
fun openExtensionsMenu() {
|
fun openExtensionsMenu() {
|
||||||
ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) {
|
ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) {
|
||||||
@@ -56,7 +55,7 @@ fun openExtensionsMenu() {
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionsMenu() {
|
fun ExtensionsMenu() {
|
||||||
val vm = composeViewModel<ExtensionsMenuViewModel>()
|
val vm = viewModel<ExtensionsMenuViewModel>()
|
||||||
val extensions by vm.extensions.collectAsState()
|
val extensions by vm.extensions.collectAsState()
|
||||||
val isLoading by vm.isLoading.collectAsState()
|
val isLoading by vm.isLoading.collectAsState()
|
||||||
val serverUrl by vm.serverUrl.collectAsState()
|
val serverUrl by vm.serverUrl.collectAsState()
|
||||||
@@ -107,7 +106,7 @@ fun ExtensionItem(
|
|||||||
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) {
|
Box(modifier = Modifier.fillMaxWidth().height(64.dp).background(MaterialTheme.colors.background)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
KtorImage(get(), extension.iconUrl(serverUrl), Modifier.size(60.dp))
|
KtorImage(extension.iconUrl(serverUrl), Modifier.size(60.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Column {
|
Column {
|
||||||
val title = buildAnnotatedString {
|
val title = buildAnnotatedString {
|
||||||
|
|||||||
@@ -6,24 +6,26 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.extensions
|
package ca.gosyer.ui.extensions
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Extension
|
import ca.gosyer.data.extension.ExtensionPreferences
|
||||||
import ca.gosyer.backend.network.interactions.ExtensionInteractionHandler
|
import ca.gosyer.data.models.Extension
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
|
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ExtensionsMenuViewModel: ViewModel() {
|
class ExtensionsMenuViewModel @Inject constructor(
|
||||||
private val preferences: PreferenceHelper by inject()
|
private val extensionHandler: ExtensionInteractionHandler,
|
||||||
private val httpClient: HttpClient by inject()
|
serverPreferences: ServerPreferences,
|
||||||
|
private val extensionPreferences: ExtensionPreferences
|
||||||
|
): ViewModel() {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
val serverUrl = preferences.serverUrl.asStateFlow(scope)
|
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||||
|
|
||||||
private val _extensions = MutableStateFlow(emptyList<Extension>())
|
private val _extensions = MutableStateFlow(emptyList<Extension>())
|
||||||
val extensions = _extensions.asStateFlow()
|
val extensions = _extensions.asStateFlow()
|
||||||
@@ -41,8 +43,8 @@ class ExtensionsMenuViewModel: ViewModel() {
|
|||||||
private suspend fun getExtensions() {
|
private suspend fun getExtensions() {
|
||||||
try {
|
try {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
val enabledLangs = preferences.enabledLangs.get()
|
val enabledLangs = extensionPreferences.languages().get()
|
||||||
val extensions = ExtensionInteractionHandler(httpClient).getExtensionList()
|
val extensions = extensionHandler.getExtensionList()
|
||||||
_extensions.value = extensions.filter { it.lang in enabledLangs }.sortedWith(compareBy({ it.lang }, { it.pkgName }))
|
_extensions.value = extensions.filter { it.lang in enabledLangs }.sortedWith(compareBy({ it.lang }, { it.pkgName }))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
@@ -55,7 +57,7 @@ class ExtensionsMenuViewModel: ViewModel() {
|
|||||||
logger.info { "Install clicked" }
|
logger.info { "Install clicked" }
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
ExtensionInteractionHandler(httpClient).installExtension(extension)
|
extensionHandler.installExtension(extension)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
}
|
}
|
||||||
@@ -67,7 +69,7 @@ class ExtensionsMenuViewModel: ViewModel() {
|
|||||||
logger.info { "Uninstall clicked" }
|
logger.info { "Uninstall clicked" }
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
ExtensionInteractionHandler(httpClient).uninstallExtension(extension)
|
extensionHandler.uninstallExtension(extension)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.gosyer.backend.models.Category
|
import ca.gosyer.data.library.model.DisplayMode
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Category
|
||||||
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.ui.base.components.LoadingScreen
|
import ca.gosyer.ui.base.components.LoadingScreen
|
||||||
import ca.gosyer.ui.base.components.Pager
|
import ca.gosyer.ui.base.components.Pager
|
||||||
import ca.gosyer.ui.base.components.PagerState
|
import ca.gosyer.ui.base.components.PagerState
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.ui.manga.openMangaMenu
|
import ca.gosyer.ui.manga.openMangaMenu
|
||||||
import ca.gosyer.util.compose.ThemedWindow
|
import ca.gosyer.util.compose.ThemedWindow
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
fun openLibraryMenu() {
|
fun openLibraryMenu() {
|
||||||
ThemedWindow {
|
ThemedWindow {
|
||||||
@@ -42,7 +42,7 @@ fun openLibraryMenu() {
|
|||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen() {
|
fun LibraryScreen() {
|
||||||
val vm = composeViewModel<LibraryScreenViewModel>()
|
val vm = viewModel<LibraryScreenViewModel>()
|
||||||
val categories by vm.categories.collectAsState()
|
val categories by vm.categories.collectAsState()
|
||||||
val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState()
|
val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState()
|
||||||
val displayMode by vm.displayMode.collectAsState()
|
val displayMode by vm.displayMode.collectAsState()
|
||||||
@@ -170,13 +170,3 @@ private fun LibraryPager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
sealed class DisplayMode {
|
|
||||||
@Serializable
|
|
||||||
object List : DisplayMode()
|
|
||||||
@Serializable
|
|
||||||
object CompactGrid : DisplayMode()
|
|
||||||
@Serializable
|
|
||||||
object ComfortableGrid : DisplayMode()
|
|
||||||
}
|
|
||||||
@@ -6,20 +6,20 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.library
|
package ca.gosyer.ui.library
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Category
|
import ca.gosyer.data.library.LibraryPreferences
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Category
|
||||||
import ca.gosyer.backend.network.interactions.CategoryInteractionHandler
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.interactions.LibraryInteractionHandler
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||||
|
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
private typealias LibraryMap = MutableMap<Int, MutableStateFlow<List<Manga>>>
|
private typealias LibraryMap = MutableMap<Int, MutableStateFlow<List<Manga>>>
|
||||||
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap)
|
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap)
|
||||||
@@ -30,11 +30,13 @@ private fun LibraryMap.setManga(order: Int, manga: List<Manga>) {
|
|||||||
getManga(order).value = manga
|
getManga(order).value = manga
|
||||||
}
|
}
|
||||||
|
|
||||||
class LibraryScreenViewModel: ViewModel() {
|
class LibraryScreenViewModel @Inject constructor(
|
||||||
private val preferences: PreferenceHelper by inject()
|
private val libraryHandler: LibraryInteractionHandler,
|
||||||
private val httpClient: HttpClient by inject()
|
private val categoryHandler: CategoryInteractionHandler,
|
||||||
|
libraryPreferences: LibraryPreferences,
|
||||||
val serverUrl = preferences.serverUrl.asStateFlow(scope)
|
serverPreferences: ServerPreferences,
|
||||||
|
): ViewModel() {
|
||||||
|
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||||
|
|
||||||
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
|
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
|
||||||
val categories = library.categories.asStateFlow()
|
val categories = library.categories.asStateFlow()
|
||||||
@@ -42,7 +44,7 @@ class LibraryScreenViewModel: ViewModel() {
|
|||||||
private val _selectedCategoryIndex = MutableStateFlow(0)
|
private val _selectedCategoryIndex = MutableStateFlow(0)
|
||||||
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
|
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
|
||||||
|
|
||||||
val displayMode = preferences.libraryDisplay.asStateFlow(scope)
|
val displayMode = libraryPreferences.displayMode().stateIn(scope)
|
||||||
|
|
||||||
private val _isLoading = MutableStateFlow(true)
|
private val _isLoading = MutableStateFlow(true)
|
||||||
val isLoading = _isLoading.asStateFlow()
|
val isLoading = _isLoading.asStateFlow()
|
||||||
@@ -55,19 +57,19 @@ class LibraryScreenViewModel: ViewModel() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
try {
|
try {
|
||||||
val categories = CategoryInteractionHandler(httpClient).getCategories()
|
val categories = categoryHandler.getCategories()
|
||||||
if (categories.isEmpty()) {
|
if (categories.isEmpty()) {
|
||||||
library.categories.value = listOf(defaultCategory)
|
library.categories.value = listOf(defaultCategory)
|
||||||
library.mangaMap.setManga(defaultCategory.order, LibraryInteractionHandler(httpClient).getLibraryManga())
|
library.mangaMap.setManga(defaultCategory.order, libraryHandler.getLibraryManga())
|
||||||
} else {
|
} else {
|
||||||
library.categories.value = listOf(defaultCategory) + categories.sortedBy { it.order }
|
library.categories.value = listOf(defaultCategory) + categories.sortedBy { it.order }
|
||||||
categories.map {
|
categories.map {
|
||||||
async {
|
async {
|
||||||
library.mangaMap.setManga(it.order, CategoryInteractionHandler(httpClient).getMangaFromCategory(it))
|
library.mangaMap.setManga(it.order, categoryHandler.getMangaFromCategory(it))
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
val mangaInCategories = library.mangaMap.flatMap { it.value.value }.map { it.id }.distinct()
|
val mangaInCategories = library.mangaMap.flatMap { it.value.value }.map { it.id }.distinct()
|
||||||
library.mangaMap.setManga(defaultCategory.order, LibraryInteractionHandler(httpClient).getLibraryManga().filterNot { it.id in mangaInCategories })
|
library.mangaMap.setManga(defaultCategory.order, libraryHandler.getLibraryManga().filterNot { it.id in mangaInCategories })
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ import androidx.compose.ui.text.TextStyle
|
|||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.ui.base.components.KtorImage
|
import ca.gosyer.ui.base.components.KtorImage
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryMangaCompactGrid(
|
fun LibraryMangaCompactGrid(
|
||||||
@@ -78,7 +77,7 @@ private fun LibraryMangaCompactGridItem(
|
|||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
) {
|
) {
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
KtorImage(get(), cover, contentScale = ContentScale.Crop)
|
KtorImage(cover, contentScale = ContentScale.Crop)
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
|
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
package ca.gosyer.ui.main
|
package ca.gosyer.ui.main
|
||||||
|
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MainViewModel : ViewModel()
|
class MainViewModel @Inject constructor(): ViewModel()
|
||||||
@@ -6,17 +6,15 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.main
|
package ca.gosyer.ui.main
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import ca.gosyer.backend.network.networkModule
|
import ca.gosyer.BuildConfig
|
||||||
import ca.gosyer.backend.preferences.preferencesModule
|
import ca.gosyer.data.DataModule
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.ui.base.vm.viewModelModule
|
|
||||||
import ca.gosyer.ui.categories.openCategoriesMenu
|
import ca.gosyer.ui.categories.openCategoriesMenu
|
||||||
import ca.gosyer.ui.extensions.openExtensionsMenu
|
import ca.gosyer.ui.extensions.openExtensionsMenu
|
||||||
import ca.gosyer.ui.library.openLibraryMenu
|
import ca.gosyer.ui.library.openLibraryMenu
|
||||||
@@ -26,11 +24,21 @@ import ca.gosyer.util.system.userDataDir
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.koin.core.context.startKoin
|
import org.apache.logging.log4j.core.config.Configurator
|
||||||
import kotlin.concurrent.thread
|
import toothpick.configuration.Configuration
|
||||||
|
import toothpick.ktp.KTP
|
||||||
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
|
val clazz = MainViewModel::class.java
|
||||||
|
Configurator.initialize(
|
||||||
|
null,
|
||||||
|
clazz.classLoader,
|
||||||
|
clazz.getResource("log4j2.xml")?.toURI()
|
||||||
|
)
|
||||||
|
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
val logger = KotlinLogging.logger("Server")
|
val logger = KotlinLogging.logger("Server")
|
||||||
val runtime = Runtime.getRuntime()
|
val runtime = Runtime.getRuntime()
|
||||||
@@ -38,19 +46,30 @@ fun main() {
|
|||||||
val jarFile = File(userDataDir,"Tachidesk.jar")
|
val jarFile = File(userDataDir,"Tachidesk.jar")
|
||||||
if (!jarFile.exists()) {
|
if (!jarFile.exists()) {
|
||||||
logger.info { "Copying server to resources" }
|
logger.info { "Copying server to resources" }
|
||||||
javaClass.getResourceAsStream("/Tachidesk.jar").buffered().use { input ->
|
javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input ->
|
||||||
jarFile.outputStream().use { output ->
|
jarFile.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info { "Starting server" }
|
val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator)
|
||||||
val process = runtime.exec("""java -jar "${jarFile.absolutePath}"""")
|
val javaExeFile = File(javaLibraryPath, "java.exe")
|
||||||
|
val javaUnixFile = File(javaLibraryPath, "java")
|
||||||
|
val javaExePath = when {
|
||||||
|
javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"'
|
||||||
|
javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"'
|
||||||
|
else -> "java"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Starting server with $javaExePath" }
|
||||||
|
val reader: BufferedReader
|
||||||
|
val process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also {
|
||||||
|
reader = it.inputStream.bufferedReader()
|
||||||
|
}
|
||||||
runtime.addShutdownHook(thread(start = false) {
|
runtime.addShutdownHook(thread(start = false) {
|
||||||
process?.destroy()
|
process?.destroy()
|
||||||
})
|
})
|
||||||
val reader = process.inputStream.reader().buffered()
|
|
||||||
logger.info { "Server started successfully" }
|
logger.info { "Server started successfully" }
|
||||||
var line: String?
|
var line: String?
|
||||||
while (reader.readLine().also { line = it } != null) {
|
while (reader.readLine().also { line = it } != null) {
|
||||||
@@ -61,18 +80,28 @@ fun main() {
|
|||||||
logger.info { "Process exitValue: $exitVal" }
|
logger.info { "Process exitValue: $exitVal" }
|
||||||
}
|
}
|
||||||
|
|
||||||
startKoin {
|
if (BuildConfig.DEBUG) {
|
||||||
modules(
|
System.setProperty("kotlinx.coroutines.debug", "on")
|
||||||
preferencesModule,
|
|
||||||
networkModule,
|
|
||||||
viewModelModule
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KTP.setConfiguration(
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Configuration.forDevelopment()
|
||||||
|
} else {
|
||||||
|
Configuration.forProduction()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
KTP.openRootScope()
|
||||||
|
.installModules(
|
||||||
|
DataModule
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ThemedWindow(title = "TachideskJUI") {
|
ThemedWindow(title = "TachideskJUI") {
|
||||||
val vm = composeViewModel<MainViewModel>()
|
val vm = viewModel<MainViewModel>()
|
||||||
Column(Modifier.fillMaxSize().background(MaterialTheme.colors.background)) {
|
Surface {
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
Button(
|
Button(
|
||||||
onClick = ::openExtensionsMenu
|
onClick = ::openExtensionsMenu
|
||||||
) {
|
) {
|
||||||
@@ -95,4 +124,5 @@ fun main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,14 +39,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ca.gosyer.backend.models.Chapter
|
import ca.gosyer.data.models.Chapter
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.ui.base.components.KtorImage
|
import ca.gosyer.ui.base.components.KtorImage
|
||||||
import ca.gosyer.ui.base.components.LoadingScreen
|
import ca.gosyer.ui.base.components.LoadingScreen
|
||||||
import ca.gosyer.ui.base.components.mangaAspectRatio
|
import ca.gosyer.ui.base.components.mangaAspectRatio
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.util.compose.ThemedWindow
|
import ca.gosyer.util.compose.ThemedWindow
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
|
|
||||||
fun openMangaMenu(mangaId: Long) {
|
fun openMangaMenu(mangaId: Long) {
|
||||||
ThemedWindow("TachideskJUI") {
|
ThemedWindow("TachideskJUI") {
|
||||||
@@ -62,7 +61,7 @@ fun openMangaMenu(manga: Manga) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaMenu(mangaId: Long) {
|
fun MangaMenu(mangaId: Long) {
|
||||||
val vm = composeViewModel<MangaMenuViewModel>()
|
val vm = viewModel<MangaMenuViewModel>()
|
||||||
remember(mangaId) {
|
remember(mangaId) {
|
||||||
vm.init(mangaId)
|
vm.init(mangaId)
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,7 @@ fun MangaMenu(mangaId: Long) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaMenu(manga: Manga) {
|
fun MangaMenu(manga: Manga) {
|
||||||
val vm = composeViewModel<MangaMenuViewModel>()
|
val vm = viewModel<MangaMenuViewModel>()
|
||||||
remember(manga) {
|
remember(manga) {
|
||||||
vm.init(manga)
|
vm.init(manga)
|
||||||
}
|
}
|
||||||
@@ -161,7 +160,7 @@ private fun Cover(manga: Manga, serverUrl: String, modifier: Modifier = Modifier
|
|||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
manga.cover(serverUrl).let {
|
manga.cover(serverUrl).let {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
KtorImage(get(), it)
|
KtorImage(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,13 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.manga
|
package ca.gosyer.ui.manga
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Chapter
|
import ca.gosyer.data.models.Chapter
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.network.interactions.ChapterInteractionHandler
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.network.interactions.LibraryInteractionHandler
|
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||||
import ca.gosyer.backend.network.interactions.MangaInteractionHandler
|
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -22,12 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MangaMenuViewModel : ViewModel() {
|
class MangaMenuViewModel @Inject constructor(
|
||||||
private val preferences: PreferenceHelper by inject()
|
private val mangaHandler: MangaInteractionHandler,
|
||||||
private val httpClient: HttpClient by inject()
|
private val chapterHandler: ChapterInteractionHandler,
|
||||||
|
private val libraryHandler: LibraryInteractionHandler,
|
||||||
val serverUrl = preferences.serverUrl.asStateFlow(scope)
|
serverPreferences: ServerPreferences
|
||||||
|
) : ViewModel() {
|
||||||
|
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||||
|
|
||||||
private val _manga = MutableStateFlow<Manga?>(null)
|
private val _manga = MutableStateFlow<Manga?>(null)
|
||||||
val manga = _manga.asStateFlow()
|
val manga = _manga.asStateFlow()
|
||||||
@@ -53,7 +54,7 @@ class MangaMenuViewModel : ViewModel() {
|
|||||||
private suspend fun refreshMangaAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
private suspend fun refreshMangaAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
_manga.value = MangaInteractionHandler(httpClient).getManga(mangaId)
|
_manga.value = mangaHandler.getManga(mangaId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ class MangaMenuViewModel : ViewModel() {
|
|||||||
suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
suspend fun refreshChaptersAsync(mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
_chapters.value = ChapterInteractionHandler(httpClient).getChapters(mangaId)
|
_chapters.value = chapterHandler.getChapters(mangaId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
}
|
}
|
||||||
@@ -74,9 +75,9 @@ class MangaMenuViewModel : ViewModel() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
manga.value?.let {
|
manga.value?.let {
|
||||||
if (it.inLibrary) {
|
if (it.inLibrary) {
|
||||||
LibraryInteractionHandler(httpClient).removeMangaFromLibrary(it)
|
libraryHandler.removeMangaFromLibrary(it)
|
||||||
} else {
|
} else {
|
||||||
LibraryInteractionHandler(httpClient).addMangaToLibrary(it)
|
libraryHandler.addMangaToLibrary(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMangaAsync(it.id).await()
|
refreshMangaAsync(it.id).await()
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.ui.sources.components.SourceHomeScreen
|
import ca.gosyer.ui.sources.components.SourceHomeScreen
|
||||||
import ca.gosyer.ui.sources.components.SourceScreen
|
import ca.gosyer.ui.sources.components.SourceScreen
|
||||||
import ca.gosyer.ui.sources.components.SourceTopBar
|
import ca.gosyer.ui.sources.components.SourceTopBar
|
||||||
@@ -29,7 +29,7 @@ fun openSourcesMenu() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesMenu() {
|
fun SourcesMenu() {
|
||||||
val vm = composeViewModel<SourcesMenuViewModel>()
|
val vm = viewModel<SourcesMenuViewModel>()
|
||||||
val isLoading by vm.isLoading.collectAsState()
|
val isLoading by vm.isLoading.collectAsState()
|
||||||
val sources by vm.sources.collectAsState()
|
val sources by vm.sources.collectAsState()
|
||||||
val sourceTabs by vm.sourceTabs.collectAsState()
|
val sourceTabs by vm.sourceTabs.collectAsState()
|
||||||
|
|||||||
@@ -6,24 +6,28 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.sources
|
package ca.gosyer.ui.sources
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.catalog.CatalogPreferences
|
||||||
import ca.gosyer.backend.network.interactions.SourceInteractionHandler
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
|
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SourcesMenuViewModel: ViewModel() {
|
class SourcesMenuViewModel @Inject constructor(
|
||||||
private val preferences: PreferenceHelper by inject()
|
private val sourceHandler: SourceInteractionHandler,
|
||||||
private val httpClient: HttpClient by inject()
|
serverPreferences: ServerPreferences,
|
||||||
|
catalogPreferences: CatalogPreferences
|
||||||
|
): ViewModel() {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
val serverUrl = preferences.serverUrl.asStateFlow(scope)
|
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||||
|
|
||||||
|
private val languages = catalogPreferences.languages().stateIn(scope)
|
||||||
|
|
||||||
private val _isLoading = MutableStateFlow(true)
|
private val _isLoading = MutableStateFlow(true)
|
||||||
val isLoading = _isLoading.asStateFlow()
|
val isLoading = _isLoading.asStateFlow()
|
||||||
@@ -44,9 +48,9 @@ class SourcesMenuViewModel: ViewModel() {
|
|||||||
private fun getSources() {
|
private fun getSources() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val sources = SourceInteractionHandler(httpClient).getSourceList()
|
val sources = sourceHandler.getSourceList()
|
||||||
logger.info { sources }
|
logger.info { sources }
|
||||||
_sources.value = sources//.filter { it.lang in Preferences.enabledLangs }
|
_sources.value = sources.filter { it.lang in languages.value }
|
||||||
logger.info { _sources.value }
|
logger.info { _sources.value }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.ui.base.components.KtorImage
|
import ca.gosyer.ui.base.components.KtorImage
|
||||||
import ca.gosyer.ui.base.components.LoadingScreen
|
import ca.gosyer.ui.base.components.LoadingScreen
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceHomeScreen(
|
fun SourceHomeScreen(
|
||||||
@@ -110,7 +109,7 @@ fun SourceItem(
|
|||||||
},
|
},
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
KtorImage(get(), source.iconUrl(serverUrl), Modifier.size(96.dp))
|
KtorImage(source.iconUrl(serverUrl), Modifier.size(96.dp))
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground)
|
Text("${source.name} (${source.lang})", color = MaterialTheme.colors.onBackground)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.ui.base.components.LoadingScreen
|
import ca.gosyer.ui.base.components.LoadingScreen
|
||||||
import ca.gosyer.ui.base.components.MangaGridItem
|
import ca.gosyer.ui.base.components.MangaGridItem
|
||||||
import ca.gosyer.ui.base.vm.composeViewModel
|
import ca.gosyer.ui.base.vm.viewModel
|
||||||
import ca.gosyer.ui.manga.openMangaMenu
|
import ca.gosyer.ui.manga.openMangaMenu
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceScreen(
|
fun SourceScreen(
|
||||||
source: Source
|
source: Source
|
||||||
) {
|
) {
|
||||||
val vm = composeViewModel<SourceScreenViewModel>()
|
val vm = viewModel<SourceScreenViewModel>()
|
||||||
remember(source) {
|
remember(source) {
|
||||||
vm.init(source)
|
vm.init(source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,24 @@
|
|||||||
|
|
||||||
package ca.gosyer.ui.sources.components
|
package ca.gosyer.ui.sources.components
|
||||||
|
|
||||||
import ca.gosyer.backend.models.Manga
|
import ca.gosyer.data.models.Manga
|
||||||
import ca.gosyer.backend.models.MangaPage
|
import ca.gosyer.data.models.MangaPage
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
import ca.gosyer.backend.network.interactions.SourceInteractionHandler
|
import ca.gosyer.data.server.ServerPreferences
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||||
import ca.gosyer.ui.base.vm.ViewModel
|
import ca.gosyer.ui.base.vm.ViewModel
|
||||||
import ca.gosyer.util.system.asStateFlow
|
|
||||||
import ca.gosyer.util.system.inject
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SourceScreenViewModel: ViewModel() {
|
class SourceScreenViewModel @Inject constructor(
|
||||||
|
private val sourceHandler: SourceInteractionHandler,
|
||||||
|
serverPreferences: ServerPreferences
|
||||||
|
): ViewModel() {
|
||||||
private lateinit var source: Source
|
private lateinit var source: Source
|
||||||
private val preferences: PreferenceHelper by inject()
|
|
||||||
private val httpClient: HttpClient by inject()
|
|
||||||
|
|
||||||
val serverUrl = preferences.serverUrl.asFLow()
|
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||||
.asStateFlow(preferences.serverUrl.get(),scope, true)
|
|
||||||
|
|
||||||
private val _mangas = MutableStateFlow(emptyList<Manga>())
|
private val _mangas = MutableStateFlow(emptyList<Manga>())
|
||||||
val mangas = _mangas.asStateFlow()
|
val mangas = _mangas.asStateFlow()
|
||||||
@@ -49,6 +47,7 @@ class SourceScreenViewModel: ViewModel() {
|
|||||||
_mangas.value = emptyList()
|
_mangas.value = emptyList()
|
||||||
_hasNextPage.value = false
|
_hasNextPage.value = false
|
||||||
_pageNum.value = 1
|
_pageNum.value = 1
|
||||||
|
_isLatest.value = source.supportsLatest
|
||||||
val page = getPage()
|
val page = getPage()
|
||||||
_mangas.value += page.mangaList
|
_mangas.value += page.mangaList
|
||||||
_hasNextPage.value = page.hasNextPage
|
_hasNextPage.value = page.hasNextPage
|
||||||
@@ -76,9 +75,9 @@ class SourceScreenViewModel: ViewModel() {
|
|||||||
|
|
||||||
private suspend fun getPage(): MangaPage {
|
private suspend fun getPage(): MangaPage {
|
||||||
return if (isLatest.value) {
|
return if (isLatest.value) {
|
||||||
SourceInteractionHandler(httpClient).getLatestManga(source, pageNum.value)
|
sourceHandler.getLatestManga(source, pageNum.value)
|
||||||
} else {
|
} else {
|
||||||
SourceInteractionHandler(httpClient).getPopularManga(source, pageNum.value)
|
sourceHandler.getPopularManga(source, pageNum.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ca.gosyer.backend.models.Source
|
import ca.gosyer.data.models.Source
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceTopBar(
|
fun SourceTopBar(
|
||||||
|
|||||||
19
src/main/kotlin/ca/gosyer/util/compose/Flow.kt
Normal file
19
src/main/kotlin/ca/gosyer/util/compose/Flow.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ca.gosyer.util.compose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
operator fun <T> StateFlow<T>.getValue(thisObj: Any?, property: KProperty<*>): T {
|
||||||
|
val item by collectAsState()
|
||||||
|
return item
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ package ca.gosyer.util.compose
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import io.ktor.client.HttpClient
|
import ca.gosyer.data.server.Http
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.statement.HttpResponse
|
||||||
import io.ktor.client.statement.readBytes
|
import io.ktor.client.statement.readBytes
|
||||||
@@ -19,6 +19,6 @@ fun imageFromFile(file: File): ImageBitmap {
|
|||||||
return Image.makeFromEncoded(file.readBytes()).asImageBitmap()
|
return Image.makeFromEncoded(file.readBytes()).asImageBitmap()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun imageFromUrl(client: HttpClient, url: String): ImageBitmap {
|
suspend fun imageFromUrl(client: Http, url: String): ImageBitmap {
|
||||||
return Image.makeFromEncoded(client.get<HttpResponse>(url).readBytes()).asImageBitmap()
|
return Image.makeFromEncoded(client.get<HttpResponse>(url).readBytes()).asImageBitmap()
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.window.MenuBar
|
import androidx.compose.ui.window.MenuBar
|
||||||
import ca.gosyer.backend.preferences.PreferenceHelper
|
|
||||||
import ca.gosyer.util.system.get
|
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
fun ThemedWindow(
|
fun ThemedWindow(
|
||||||
@@ -31,7 +29,7 @@ fun ThemedWindow(
|
|||||||
content: @Composable () -> Unit = { }
|
content: @Composable () -> Unit = { }
|
||||||
) {
|
) {
|
||||||
Window(title, size, location, centered, icon, menuBar, undecorated, resizable, events, onDismissRequest) {
|
Window(title, size, location, centered, icon, menuBar, undecorated, resizable, events, onDismissRequest) {
|
||||||
DesktopMaterialTheme(get<PreferenceHelper>().getTheme()) {
|
DesktopMaterialTheme {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1341
src/main/resources/Log4j-config.xsd
Normal file
1341
src/main/resources/Log4j-config.xsd
Normal file
File diff suppressed because it is too large
Load Diff
18
src/main/resources/log4j2.xml
Normal file
18
src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://logging.apache.org/log4j/2.0/config"
|
||||||
|
xsi:noNamespaceSchemaLocation="Log4j-config.xsd">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout disableAnsi="false" pattern="%highlight{%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}} [%t] ${LOG_LEVEL_PATTERN:-%p}/%c{1}: %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%xEx}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=black, DEBUG=black, TRACE=black}" />
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<Loggers>
|
||||||
|
<Root level="debug">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
|
||||||
|
</Configuration>
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<configuration debug="true">
|
|
||||||
|
|
||||||
<property name="HOME_LOG" value="logs/app.log"/>
|
|
||||||
|
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
|
||||||
<!--<withJansi>true</withJansi>-->
|
|
||||||
<encoder>
|
|
||||||
<pattern>%highlight(%d{HH:mm:ss.SSS} [%thread] %level/%logger{0}: %msg%n)</pattern>
|
|
||||||
</encoder>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<!--<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
|
||||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
|
||||||
<Pattern>
|
|
||||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
|
||||||
</Pattern>
|
|
||||||
</layout>
|
|
||||||
</appender>-->
|
|
||||||
|
|
||||||
<!--<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
|
||||||
<file>${HOME_LOG}</file>
|
|
||||||
<append>true</append>
|
|
||||||
<immediateFlush>true</immediateFlush>
|
|
||||||
<encoder>
|
|
||||||
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
|
|
||||||
</encoder>
|
|
||||||
</appender>-->
|
|
||||||
|
|
||||||
<!--<logger name="ca.gosyer" level="debug" additivity="false">
|
|
||||||
<appender-ref ref="CONSOLE"/>
|
|
||||||
<!–<appender-ref ref="FILE"/>–>
|
|
||||||
</logger>-->
|
|
||||||
|
|
||||||
<root level="debug">
|
|
||||||
<appender-ref ref="CONSOLE"/>
|
|
||||||
</root>
|
|
||||||
|
|
||||||
</configuration>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package ca.gosyer.backend.network
|
package ca.gosyer.data.server
|
||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
Reference in New Issue
Block a user