diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/App.kt b/android/src/main/kotlin/ca/gosyer/jui/android/App.kt index 6152186d..f2e0ff93 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/App.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/App.kt @@ -21,7 +21,6 @@ import org.lighthousegames.logging.logging import java.util.Locale class App : Application(), DefaultLifecycleObserver { - override fun onCreate() { super.onCreate() diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/AppComponent.kt b/android/src/main/kotlin/ca/gosyer/jui/android/AppComponent.kt index 3daeb726..4baea889 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/AppComponent.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/AppComponent.kt @@ -23,7 +23,6 @@ abstract class AppComponent( @get:Provides val context: Context, ) : ViewModelComponent, DataComponent, DomainComponent, UiComponent { - abstract val appMigrations: AppMigrations @get:AppScope @@ -39,7 +38,8 @@ abstract class AppComponent( private var appComponentInstance: AppComponent? = null @Suppress("UNRESOLVED_REFERENCE", "EXPRESSION_EXPECTED_PACKAGE_FOUND") - fun getInstance(context: Context) = appComponentInstance ?: create(context) - .also { appComponentInstance = it } + fun getInstance(context: Context) = + appComponentInstance ?: create(context) + .also { appComponentInstance = it } } } diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/AppMigrations.kt b/android/src/main/kotlin/ca/gosyer/jui/android/AppMigrations.kt index 154ce56d..81dee2aa 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/AppMigrations.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/AppMigrations.kt @@ -11,24 +11,25 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences import ca.gosyer.jui.uicore.vm.ContextWrapper import me.tatarka.inject.annotations.Inject -class AppMigrations @Inject constructor( - private val migrationPreferences: MigrationPreferences, - private val contextWrapper: ContextWrapper, -) { +class AppMigrations + @Inject + constructor( + private val migrationPreferences: MigrationPreferences, + private val contextWrapper: ContextWrapper, + ) { + fun runMigrations(): Boolean { + val oldVersion = migrationPreferences.appVersion().get() + if (oldVersion < BuildConfig.VERSION_CODE) { + migrationPreferences.appVersion().set(BuildConfig.VERSION_CODE) - fun runMigrations(): Boolean { - val oldVersion = migrationPreferences.appVersion().get() - if (oldVersion < BuildConfig.VERSION_CODE) { - migrationPreferences.appVersion().set(BuildConfig.VERSION_CODE) + UpdateCheckWorker.setupTask(contextWrapper) - UpdateCheckWorker.setupTask(contextWrapper) - - // Fresh install - if (oldVersion == 0) { - return false + // Fresh install + if (oldVersion == 0) { + return false + } + return true } - return true + return false } - return false } -} diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt b/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt index aff7cf23..5d8da232 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/ReaderActivity.kt @@ -16,9 +16,12 @@ import ca.gosyer.jui.ui.base.theme.AppTheme import ca.gosyer.jui.ui.reader.ReaderMenu class ReaderActivity : AppCompatActivity() { - companion object { - fun newIntent(context: Context, mangaId: Long, chapterIndex: Int): Intent { + fun newIntent( + context: Context, + mangaId: Long, + chapterIndex: Int, + ): Intent { return Intent(context, ReaderActivity::class.java).apply { putExtra("manga", mangaId) putExtra("chapter", chapterIndex) diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt b/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt index 072234ee..fa7f1f0c 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt @@ -55,11 +55,13 @@ import org.lighthousegames.logging.logging import java.util.regex.Pattern class AndroidDownloadService : Service() { - companion object { private var instance: AndroidDownloadService? = null - fun start(context: Context, actions: Actions) { + fun start( + context: Context, + actions: Actions, + ) { if (!isRunning() && actions != Actions.STOP) { val intent = Intent(context, AndroidDownloadService::class.java).apply { action = actions.name @@ -106,7 +108,11 @@ class AndroidDownloadService : Service() { super.onDestroy() } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int, + ): Int { instance = this ioScope.coroutineContext.job.cancelChildren() diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/data/library/AndroidLibraryService.kt b/android/src/main/kotlin/ca/gosyer/jui/android/data/library/AndroidLibraryService.kt index 98d33ffa..e9d5685a 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/data/library/AndroidLibraryService.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/data/library/AndroidLibraryService.kt @@ -55,11 +55,13 @@ import kotlinx.serialization.json.Json import org.lighthousegames.logging.logging class AndroidLibraryService : Service() { - companion object { private var instance: AndroidLibraryService? = null - fun start(context: Context, actions: Actions) { + fun start( + context: Context, + actions: Actions, + ) { if (!isRunning() && actions != Actions.STOP) { val intent = Intent(context, AndroidLibraryService::class.java).apply { action = actions.name @@ -106,7 +108,11 @@ class AndroidLibraryService : Service() { super.onDestroy() } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int, + ): Int { instance = this ioScope.coroutineContext.job.cancelChildren() diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/data/notification/Notifications.kt b/android/src/main/kotlin/ca/gosyer/jui/android/data/notification/Notifications.kt index a3afe40a..fe026e9e 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/data/notification/Notifications.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/data/notification/Notifications.kt @@ -14,7 +14,6 @@ import ca.gosyer.jui.i18n.MR import dev.icerock.moko.resources.desc.desc object Notifications { - /** * Notification channel and ids used by the downloader. */ diff --git a/android/src/main/kotlin/ca/gosyer/jui/android/util/Context.kt b/android/src/main/kotlin/ca/gosyer/jui/android/util/Context.kt index a4910ab9..a64803bb 100644 --- a/android/src/main/kotlin/ca/gosyer/jui/android/util/Context.kt +++ b/android/src/main/kotlin/ca/gosyer/jui/android/util/Context.kt @@ -37,7 +37,10 @@ fun Context.acquireWakeLock(tag: String): PowerManager.WakeLock { * @param block the function that will execute inside the builder. * @return a notification to be displayed or updated. */ -fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null): NotificationCompat.Builder { +fun Context.notificationBuilder( + channelId: String, + block: (NotificationCompat.Builder.() -> Unit)? = null, +): NotificationCompat.Builder { val builder = NotificationCompat.Builder(this, channelId) // .setColor(getColor(R.color.accent_blue)) if (block != null) { @@ -53,7 +56,10 @@ fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Bu * @param block the function that will execute inside the builder. * @return a notification to be displayed or updated. */ -fun Context.notification(channelId: String, block: (NotificationCompat.Builder.() -> Unit)?): Notification { +fun Context.notification( + channelId: String, + block: (NotificationCompat.Builder.() -> Unit)?, +): Notification { val builder = notificationBuilder(channelId, block) return builder.build() } diff --git a/core/src/androidMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt b/core/src/androidMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt index 1f840383..692bc556 100644 --- a/core/src/androidMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt +++ b/core/src/androidMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt @@ -10,15 +10,17 @@ import android.content.Context import com.russhwolf.settings.SharedPreferencesSettings import me.tatarka.inject.annotations.Inject -actual class PreferenceStoreFactory @Inject constructor(private val context: Context) { - actual fun create(vararg names: String): PreferenceStore { - return StandardPreferenceStore( - SharedPreferencesSettings( - context.getSharedPreferences( - names.joinToString(separator = "_"), - Context.MODE_PRIVATE, +actual class PreferenceStoreFactory + @Inject + constructor(private val context: Context) { + actual fun create(vararg names: String): PreferenceStore { + return StandardPreferenceStore( + SharedPreferencesSettings( + context.getSharedPreferences( + names.joinToString(separator = "_"), + Context.MODE_PRIVATE, + ), ), - ), - ) + ) + } } -} diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/CoroutineExtensions.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/CoroutineExtensions.kt index d867b83f..ad6aee28 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/CoroutineExtensions.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/CoroutineExtensions.kt @@ -48,17 +48,11 @@ fun CoroutineScope.launchIO( block: suspend CoroutineScope.() -> Unit, ) = launch(Dispatchers.IO, start, block) -suspend fun withDefaultContext( - block: suspend CoroutineScope.() -> T, -): T = withContext(Dispatchers.Default, block) +suspend fun withDefaultContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Default, block) -suspend fun withUIContext( - block: suspend CoroutineScope.() -> T, -): T = withContext(Dispatchers.Main, block) +suspend fun withUIContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Main, block) -suspend fun withIOContext( - block: suspend CoroutineScope.() -> T, -): T = withContext(Dispatchers.IO, block) +suspend fun withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block) fun Throwable.throwIfCancellation() { if (this is CancellationException) throw this } diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/PriorityChannel.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/PriorityChannel.kt index 488a1162..a25db00f 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/PriorityChannel.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/PriorityChannel.kt @@ -29,7 +29,6 @@ internal open class ProcessChannel( internal val inChannel: Channel, internal val outChannel: Channel, ) : Channel { - @DelicateCoroutinesApi override val isClosedForReceive: Boolean get() = outChannel.isClosedForReceive diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/String.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/String.kt index 13008f92..9e795c91 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/String.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/lang/String.kt @@ -10,7 +10,10 @@ package ca.gosyer.jui.core.lang * Replaces the given string to have at most [count] characters using [replacement] at its end. * If [replacement] is longer than [count] an exception will be thrown when `length > count`. */ -fun String.chop(count: Int, replacement: String = "…"): String { +fun String.chop( + count: Int, + replacement: String = "…", +): String { return if (length > count) { take(count - replacement.length) + replacement } else { diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/LazyPreferenceStore.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/LazyPreferenceStore.kt index bcd674b7..6bba50db 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/LazyPreferenceStore.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/LazyPreferenceStore.kt @@ -16,46 +16,63 @@ import kotlinx.serialization.modules.SerializersModule class LazyPreferenceStore( private val lazyStore: Lazy, ) : PreferenceStore { - /** * Returns an [String] preference for this [key]. */ - override fun getString(key: String, defaultValue: String): Preference { + override fun getString( + key: String, + defaultValue: String, + ): Preference { return lazyStore.value.getString(key, defaultValue) } /** * Returns a [Long] preference for this [key]. */ - override fun getLong(key: String, defaultValue: Long): Preference { + override fun getLong( + key: String, + defaultValue: Long, + ): Preference { return lazyStore.value.getLong(key, defaultValue) } /** * Returns an [Int] preference for this [key]. */ - override fun getInt(key: String, defaultValue: Int): Preference { + override fun getInt( + key: String, + defaultValue: Int, + ): Preference { return lazyStore.value.getInt(key, defaultValue) } /** * Returns a [Float] preference for this [key]. */ - override fun getFloat(key: String, defaultValue: Float): Preference { + override fun getFloat( + key: String, + defaultValue: Float, + ): Preference { return lazyStore.value.getFloat(key, defaultValue) } /** * Returns a [Boolean] preference for this [key]. */ - override fun getBoolean(key: String, defaultValue: Boolean): Preference { + override fun getBoolean( + key: String, + defaultValue: Boolean, + ): Preference { return lazyStore.value.getBoolean(key, defaultValue) } /** * Returns a [Set] preference for this [key]. */ - override fun getStringSet(key: String, defaultValue: Set): Preference> { + override fun getStringSet( + key: String, + defaultValue: Set, + ): Preference> { return lazyStore.value.getStringSet(key, defaultValue) } diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/Preference.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/Preference.kt index 232b4518..5b0174cc 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/Preference.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/Preference.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.onStart * this interface must be provided through a [PreferenceStore]. */ interface Preference { - /** * Returns the key of this preference. */ diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStore.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStore.kt index feea7580..6841fa0b 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStore.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStore.kt @@ -15,36 +15,53 @@ import kotlinx.serialization.modules.SerializersModule * persist these preferences on disk. */ interface PreferenceStore { - /** * Returns an [String] preference for this [key]. */ - fun getString(key: String, defaultValue: String = ""): Preference + fun getString( + key: String, + defaultValue: String = "", + ): Preference /** * Returns a [Long] preference for this [key]. */ - fun getLong(key: String, defaultValue: Long = 0): Preference + fun getLong( + key: String, + defaultValue: Long = 0, + ): Preference /** * Returns an [Int] preference for this [key]. */ - fun getInt(key: String, defaultValue: Int = 0): Preference + fun getInt( + key: String, + defaultValue: Int = 0, + ): Preference /** * Returns a [Float] preference for this [key]. */ - fun getFloat(key: String, defaultValue: Float = 0f): Preference + fun getFloat( + key: String, + defaultValue: Float = 0f, + ): Preference /** * Returns a [Boolean] preference for this [key]. */ - fun getBoolean(key: String, defaultValue: Boolean = false): Preference + fun getBoolean( + key: String, + defaultValue: Boolean = false, + ): Preference /** * Returns a [Set] preference for this [key]. */ - fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> + fun getStringSet( + key: String, + defaultValue: Set = emptySet(), + ): Preference> /** * Returns preference of type [T] for this [key]. The [serializer] and [deserializer] function diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardAdapters.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardAdapters.kt index d6892ced..3aa3419a 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardAdapters.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardAdapters.kt @@ -17,81 +17,150 @@ import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.modules.SerializersModule interface Adapter { - fun get(key: String, preferences: ObservableSettings): T + fun get( + key: String, + preferences: ObservableSettings, + ): T - fun set(key: String, value: T, editor: ObservableSettings) + fun set( + key: String, + value: T, + editor: ObservableSettings, + ) - fun isSet(keys: Set, key: String): Boolean = key in keys + fun isSet( + keys: Set, + key: String, + ): Boolean = key in keys - fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener + fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener } internal object StringAdapter : Adapter { - override fun get(key: String, preferences: ObservableSettings): 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) { + override fun set( + key: String, + value: String, + editor: ObservableSettings, + ) { editor.putString(key, value) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addStringOrNullListener(key) { callback() } } } internal object LongAdapter : Adapter { - override fun get(key: String, preferences: ObservableSettings): Long { + override fun get( + key: String, + preferences: ObservableSettings, + ): Long { return preferences.getLong(key, 0) } - override fun set(key: String, value: Long, editor: ObservableSettings) { + override fun set( + key: String, + value: Long, + editor: ObservableSettings, + ) { editor.putLong(key, value) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addLongOrNullListener(key) { callback() } } } internal object IntAdapter : Adapter { - override fun get(key: String, preferences: ObservableSettings): Int { + override fun get( + key: String, + preferences: ObservableSettings, + ): Int { return preferences.getInt(key, 0) } - override fun set(key: String, value: Int, editor: ObservableSettings) { + override fun set( + key: String, + value: Int, + editor: ObservableSettings, + ) { editor.putInt(key, value) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addIntOrNullListener(key) { callback() } } } internal object FloatAdapter : Adapter { - override fun get(key: String, preferences: ObservableSettings): Float { + override fun get( + key: String, + preferences: ObservableSettings, + ): Float { return preferences.getFloat(key, 0f) } - override fun set(key: String, value: Float, editor: ObservableSettings) { + override fun set( + key: String, + value: Float, + editor: ObservableSettings, + ) { editor.putFloat(key, value) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addFloatOrNullListener(key) { callback() } } } internal object BooleanAdapter : Adapter { - override fun get(key: String, preferences: ObservableSettings): Boolean { + override fun get( + key: String, + preferences: ObservableSettings, + ): Boolean { return preferences.getBoolean(key, false) } - override fun set(key: String, value: Boolean, editor: ObservableSettings) { + override fun set( + key: String, + value: Boolean, + editor: ObservableSettings, + ) { editor.putBoolean(key, value) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addBooleanOrNullListener(key) { callback() } } } @@ -99,18 +168,28 @@ internal object BooleanAdapter : Adapter { internal object StringSetAdapter : Adapter> { private val serializer = SetSerializer(String.serializer()) - override fun get(key: String, preferences: ObservableSettings): Set { + override fun get( + key: String, + preferences: ObservableSettings, + ): Set { return preferences.decodeValue(serializer, key, emptySet()) // Not called unless key is present. } - override fun set(key: String, value: Set, editor: ObservableSettings) { + override fun set( + key: String, + value: Set, + editor: ObservableSettings, + ) { editor.encodeValue(serializer, key, value) } /** * Encoding a string set makes a list of keys and a size key, such as key.size and key.0-size */ - override fun isSet(keys: Set, key: String): Boolean { + override fun isSet( + keys: Set, + key: String, + ): Boolean { return keys.contains("$key.size") } @@ -118,7 +197,11 @@ internal object StringSetAdapter : Adapter> { * Watching the regular key doesn't produce updates for a string set for some reason * TODO make better, doesn't produce updates when you add something and remove something */ - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addIntOrNullListener("$key.size") { callback() } } } @@ -127,16 +210,26 @@ internal class ObjectAdapter( private val serializer: (T) -> String, private val deserializer: (String) -> T, ) : Adapter { - - override fun get(key: String, preferences: ObservableSettings): 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) { + override fun set( + key: String, + value: T, + editor: ObservableSettings, + ) { editor.putString(key, serializer(value)) } - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { return preferences.addStringOrNullListener(key) { callback() } } } @@ -146,12 +239,18 @@ internal class JsonObjectAdapter( private val serializer: KSerializer, private val serializersModule: SerializersModule = EmptySerializersModule(), ) : Adapter { - - override fun get(key: String, preferences: ObservableSettings): 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) { + override fun set( + key: String, + value: T, + editor: ObservableSettings, + ) { editor.encodeValue(serializer, key, value, serializersModule) } @@ -159,14 +258,21 @@ internal class JsonObjectAdapter( * Encoding a structure makes keys start with the [key] and adds extensions for values, * for a pair it would be like [key].first [key].second. */ - override fun isSet(keys: Set, key: String): Boolean { + override fun isSet( + keys: Set, + key: String, + ): Boolean { return keys.any { it.startsWith(key) } } /** * Todo doesn't work */ - override fun addListener(key: String, preferences: ObservableSettings, callback: () -> Unit): SettingsListener { + override fun addListener( + key: String, + preferences: ObservableSettings, + callback: () -> Unit, + ): SettingsListener { @Suppress("DEPRECATION") // Because we don't care about the type, and it crashes with any other listener return preferences.addListener(key) { callback() } } diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreference.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreference.kt index b2a4129c..a413afff 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreference.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreference.kt @@ -21,7 +21,6 @@ internal class StandardPreference( private val defaultValue: T, private val adapter: Adapter, ) : Preference { - /** * Returns the key of this preference. */ diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreferenceStore.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreferenceStore.kt index c81fe2d3..900f3054 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreferenceStore.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/prefs/StandardPreferenceStore.kt @@ -11,46 +11,63 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.modules.SerializersModule class StandardPreferenceStore(private val preferences: ObservableSettings) : PreferenceStore { - /** * Returns an [String] preference for this [key]. */ - override fun getString(key: String, defaultValue: String): Preference { + override fun getString( + key: String, + defaultValue: String, + ): Preference { return StandardPreference(preferences, key, defaultValue, StringAdapter) } /** * Returns a [Long] preference for this [key]. */ - override fun getLong(key: String, defaultValue: Long): Preference { + override fun getLong( + key: String, + defaultValue: Long, + ): Preference { return StandardPreference(preferences, key, defaultValue, LongAdapter) } /** * Returns an [Int] preference for this [key]. */ - override fun getInt(key: String, defaultValue: Int): Preference { + override fun getInt( + key: String, + defaultValue: Int, + ): Preference { return StandardPreference(preferences, key, defaultValue, IntAdapter) } /** * Returns a [Float] preference for this [key]. */ - override fun getFloat(key: String, defaultValue: Float): Preference { + override fun getFloat( + key: String, + defaultValue: Float, + ): Preference { return StandardPreference(preferences, key, defaultValue, FloatAdapter) } /** * Returns a [Boolean] preference for this [key]. */ - override fun getBoolean(key: String, defaultValue: Boolean): Preference { + override fun getBoolean( + key: String, + defaultValue: Boolean, + ): Preference { return StandardPreference(preferences, key, defaultValue, BooleanAdapter) } /** * Returns a [Set] preference for this [key]. */ - override fun getStringSet(key: String, defaultValue: Set): Preference> { + override fun getStringSet( + key: String, + defaultValue: Set, + ): Preference> { return StandardPreference(preferences, key, defaultValue, StringSetAdapter) } diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/CollectionExtensions.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/CollectionExtensions.kt index 92c942ec..5d629494 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/CollectionExtensions.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/CollectionExtensions.kt @@ -9,7 +9,10 @@ package ca.gosyer.jui.core.util /** * Returns a new list that replaces the item at the given [position] with [newItem]. */ -fun List.replace(position: Int, newItem: T): List { +fun List.replace( + position: Int, + newItem: T, +): List { val newList = toMutableList() newList[position] = newItem return newList @@ -19,7 +22,10 @@ fun List.replace(position: Int, newItem: T): List { * Returns a new list that replaces the first occurrence that matches the given [predicate] with * [newItem]. If no item matches the predicate, the same list is returned (and unmodified). */ -inline fun List.replaceFirst(predicate: (T) -> Boolean, newItem: T): List { +inline fun List.replaceFirst( + predicate: (T) -> Boolean, + newItem: T, +): List { forEachIndexed { index, element -> if (predicate(element)) { return replace(index, newItem) diff --git a/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/ImageUtil.kt b/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/ImageUtil.kt index f1822956..2f9913fb 100644 --- a/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/ImageUtil.kt +++ b/core/src/commonMain/kotlin/ca/gosyer/jui/core/util/ImageUtil.kt @@ -9,7 +9,6 @@ package ca.gosyer.jui.core.util import io.ktor.utils.io.core.toByteArray object ImageUtil { - private val jpgMagic = charByteArrayOf(0xFF, 0xD8, 0xFF) private val pngMagic = charByteArrayOf(0x89, 0x50, 0x4E, 0x47) private val gifMagic = "GIF8".toByteArray() diff --git a/core/src/desktopMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt b/core/src/desktopMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt index 2ad0522c..56acda52 100644 --- a/core/src/desktopMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt +++ b/core/src/desktopMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt @@ -10,15 +10,17 @@ import com.russhwolf.settings.PreferencesSettings import me.tatarka.inject.annotations.Inject import java.util.prefs.Preferences -actual class PreferenceStoreFactory @Inject constructor() { - private val rootNode: Preferences = Preferences.userRoot() - .node("ca/gosyer/tachideskjui") +actual class PreferenceStoreFactory + @Inject + constructor() { + private val rootNode: Preferences = Preferences.userRoot() + .node("ca/gosyer/tachideskjui") - actual fun create(vararg names: String): PreferenceStore { - return StandardPreferenceStore( - PreferencesSettings( - rootNode.node(names.joinToString(separator = "/")), - ), - ) + actual fun create(vararg names: String): PreferenceStore { + return StandardPreferenceStore( + PreferencesSettings( + rootNode.node(names.joinToString(separator = "/")), + ), + ) + } } -} diff --git a/core/src/iosMain/kotlin/ca/gosyer/jui/core/lang/IosLocale.kt b/core/src/iosMain/kotlin/ca/gosyer/jui/core/lang/IosLocale.kt index d6b2a52b..9b5d582c 100644 --- a/core/src/iosMain/kotlin/ca/gosyer/jui/core/lang/IosLocale.kt +++ b/core/src/iosMain/kotlin/ca/gosyer/jui/core/lang/IosLocale.kt @@ -17,14 +17,16 @@ fun Locale.toPlatform(): PlatformLocale = PlatformLocale(toLanguageTag()) * First Locale: en_IN * Language: English */ -actual fun Locale.getDisplayLanguage(displayLocale: Locale): String = toPlatform() - .localizedStringForLanguageCode(displayLocale.toLanguageTag())!! +actual fun Locale.getDisplayLanguage(displayLocale: Locale): String = + toPlatform() + .localizedStringForLanguageCode(displayLocale.toLanguageTag())!! /** * First Locale: en_US * Language: English (United States) */ -actual fun Locale.getDisplayName(displayLocale: Locale): String = toPlatform() - .localizedStringForLocaleIdentifier(displayLocale.toLanguageTag()) +actual fun Locale.getDisplayName(displayLocale: Locale): String = + toPlatform() + .localizedStringForLocaleIdentifier(displayLocale.toLanguageTag()) actual val Locale.displayName: String get() = getDisplayLanguage(this) diff --git a/core/src/iosMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt b/core/src/iosMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt index 5146a8eb..f0556db4 100644 --- a/core/src/iosMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt +++ b/core/src/iosMain/kotlin/ca/gosyer/jui/core/prefs/PreferenceStoreFactory.kt @@ -10,12 +10,14 @@ import com.russhwolf.settings.NSUserDefaultsSettings import me.tatarka.inject.annotations.Inject import platform.Foundation.NSUserDefaults -actual class PreferenceStoreFactory @Inject constructor() { - actual fun create(vararg names: String): PreferenceStore { - return StandardPreferenceStore( - NSUserDefaultsSettings( - NSUserDefaults.standardUserDefaults, - ), - ) +actual class PreferenceStoreFactory + @Inject + constructor() { + actual fun create(vararg names: String): PreferenceStore { + return StandardPreferenceStore( + NSUserDefaultsSettings( + NSUserDefaults.standardUserDefaults, + ), + ) + } } -} diff --git a/core/src/jvmMain/kotlin/ca/gosyer/jui/core/lang/JvmLocale.kt b/core/src/jvmMain/kotlin/ca/gosyer/jui/core/lang/JvmLocale.kt index 54bc9a3c..ed38f268 100644 --- a/core/src/jvmMain/kotlin/ca/gosyer/jui/core/lang/JvmLocale.kt +++ b/core/src/jvmMain/kotlin/ca/gosyer/jui/core/lang/JvmLocale.kt @@ -11,10 +11,12 @@ import java.util.Locale as PlatformLocale fun Locale.toPlatform(): PlatformLocale = PlatformLocale.forLanguageTag(toLanguageTag()) -actual fun Locale.getDisplayLanguage(displayLocale: Locale): String = toPlatform() - .getDisplayLanguage(displayLocale.toPlatform()) +actual fun Locale.getDisplayLanguage(displayLocale: Locale): String = + toPlatform() + .getDisplayLanguage(displayLocale.toPlatform()) -actual fun Locale.getDisplayName(displayLocale: Locale): String = toPlatform() - .getDisplayName(displayLocale.toPlatform()) +actual fun Locale.getDisplayName(displayLocale: Locale): String = + toPlatform() + .getDisplayName(displayLocale.toPlatform()) actual val Locale.displayName: String get() = toPlatform().displayName diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt index 573e973e..c7900a17 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/DataComponent.kt @@ -24,9 +24,11 @@ import de.jensklingenberg.ktorfit.Ktorfit import me.tatarka.inject.annotations.Provides interface DataComponent { - @Provides - fun ktorfit(http: Http, serverPreferences: ServerPreferences) = Ktorfit + fun ktorfit( + http: Http, + serverPreferences: ServerPreferences, + ) = Ktorfit .Builder() .httpClient(http) .converterFactories(FlowConverterFactory()) diff --git a/data/src/commonMain/kotlin/ca/gosyer/jui/data/FlowConverterFactory.kt b/data/src/commonMain/kotlin/ca/gosyer/jui/data/FlowConverterFactory.kt index 3031eca3..a26cabd9 100644 --- a/data/src/commonMain/kotlin/ca/gosyer/jui/data/FlowConverterFactory.kt +++ b/data/src/commonMain/kotlin/ca/gosyer/jui/data/FlowConverterFactory.kt @@ -18,19 +18,17 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn class FlowConverterFactory : Converter.Factory { - private class FlowResponseConverter( val typeData: TypeData, - val ktorfit: Ktorfit + val ktorfit: Ktorfit, ) : Converter.ResponseConverter> { - override fun convert(getResponse: suspend () -> HttpResponse): Flow { return flow { val response = getResponse() val convertedBody = ktorfit.nextSuspendResponseConverter( null, - typeData.typeArgs.first() + typeData.typeArgs.first(), )?.convert(response) ?: response.body(typeData.typeArgs.first().typeInfo) emit(convertedBody) @@ -40,7 +38,7 @@ class FlowConverterFactory : Converter.Factory { override fun responseConverter( typeData: TypeData, - ktorfit: Ktorfit + ktorfit: Ktorfit, ): Converter.ResponseConverter? { if (typeData.typeInfo.type == Flow::class) { return FlowResponseConverter(typeData, ktorfit) @@ -50,9 +48,8 @@ class FlowConverterFactory : Converter.Factory { override fun suspendResponseConverter( typeData: TypeData, - ktorfit: Ktorfit + ktorfit: Ktorfit, ): Converter.SuspendResponseConverter? { return null } } - diff --git a/data/src/iosMain/kotlin/ca/gosyer/jui/data/base/IosDateHandler.kt b/data/src/iosMain/kotlin/ca/gosyer/jui/data/base/IosDateHandler.kt index b9f21b84..13e367d0 100644 --- a/data/src/iosMain/kotlin/ca/gosyer/jui/data/base/IosDateHandler.kt +++ b/data/src/iosMain/kotlin/ca/gosyer/jui/data/base/IosDateHandler.kt @@ -15,44 +15,47 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle -actual class DateHandler @Inject constructor() { - actual val formatOptions by lazy { - listOf( - "", - "MM/dd/yy", - "dd/MM/yy", - "yyyy-MM-dd", - ) - } - - actual fun getDateFormat(format: String): (Instant) -> String = when (format) { - "" -> NSDateFormatter() - .apply { - setDateStyle(NSDateFormatterShortStyle) - setTimeStyle(NSDateFormatterNoStyle) - setLocale(Locale.current.toPlatform()) - } - else -> NSDateFormatter() - .apply { - setDateFormat(format) - } - }.let { formatter -> - { - formatter.stringFromDate(it.toNSDate()) +actual class DateHandler + @Inject + constructor() { + actual val formatOptions by lazy { + listOf( + "", + "MM/dd/yy", + "dd/MM/yy", + "yyyy-MM-dd", + ) } - } - actual val dateTimeFormat: (Instant) -> String by lazy { - NSDateFormatter() - .apply { - setDateStyle(NSDateFormatterShortStyle) - setTimeStyle(NSDateFormatterShortStyle) - setLocale(Locale.current.toPlatform()) - } - .let { formatter -> + actual fun getDateFormat(format: String): (Instant) -> String = + when (format) { + "" -> NSDateFormatter() + .apply { + setDateStyle(NSDateFormatterShortStyle) + setTimeStyle(NSDateFormatterNoStyle) + setLocale(Locale.current.toPlatform()) + } + else -> NSDateFormatter() + .apply { + setDateFormat(format) + } + }.let { formatter -> { formatter.stringFromDate(it.toNSDate()) } } + + actual val dateTimeFormat: (Instant) -> String by lazy { + NSDateFormatter() + .apply { + setDateStyle(NSDateFormatterShortStyle) + setTimeStyle(NSDateFormatterShortStyle) + setLocale(Locale.current.toPlatform()) + } + .let { formatter -> + { + formatter.stringFromDate(it.toNSDate()) + } + } + } } -} diff --git a/data/src/jvmMain/kotlin/ca/gosyer/jui/data/base/JvmDateHandler.kt b/data/src/jvmMain/kotlin/ca/gosyer/jui/data/base/JvmDateHandler.kt index 961c4403..e4ee2f36 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/jui/data/base/JvmDateHandler.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/jui/data/base/JvmDateHandler.kt @@ -15,37 +15,40 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -actual class DateHandler @Inject constructor() { - actual val formatOptions by lazy { - listOf( - "", - "MM/dd/yy", - "dd/MM/yy", - "yyyy-MM-dd", - ) - } - - actual fun getDateFormat(format: String): (Instant) -> String = when (format) { - "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - .withLocale(Locale.current.toPlatform()) - .withZone(ZoneId.systemDefault()) - else -> DateTimeFormatter.ofPattern(format) - .withZone(ZoneId.systemDefault()) - }.let { formatter -> - { - formatter.format(it.toJavaInstant()) +actual class DateHandler + @Inject + constructor() { + actual val formatOptions by lazy { + listOf( + "", + "MM/dd/yy", + "dd/MM/yy", + "yyyy-MM-dd", + ) } - } - actual val dateTimeFormat: (Instant) -> String by lazy { - DateTimeFormatter - .ofLocalizedDateTime(FormatStyle.SHORT) - .withLocale(Locale.current.toPlatform()) - .withZone(ZoneId.systemDefault()) - .let { formatter -> + actual fun getDateFormat(format: String): (Instant) -> String = + when (format) { + "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) + .withLocale(Locale.current.toPlatform()) + .withZone(ZoneId.systemDefault()) + else -> DateTimeFormatter.ofPattern(format) + .withZone(ZoneId.systemDefault()) + }.let { formatter -> { formatter.format(it.toJavaInstant()) } } + + actual val dateTimeFormat: (Instant) -> String by lazy { + DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withLocale(Locale.current.toPlatform()) + .withZone(ZoneId.systemDefault()) + .let { formatter -> + { + formatter.format(it.toJavaInstant()) + } + } + } } -} diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt index 48d21e09..1b847679 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppComponent.kt @@ -21,7 +21,6 @@ abstract class AppComponent( @get:Provides val context: ContextWrapper, ) : ViewModelComponent, DataComponent, DomainComponent, UiComponent { - abstract val appMigrations: AppMigrations @get:AppScope @@ -35,7 +34,8 @@ abstract class AppComponent( companion object { private var appComponentInstance: AppComponent? = null - fun getInstance(context: ContextWrapper) = appComponentInstance ?: create(context) - .also { appComponentInstance = it } + fun getInstance(context: ContextWrapper) = + appComponentInstance ?: create(context) + .also { appComponentInstance = it } } } diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppMigrations.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppMigrations.kt index d3dff8ce..380299bc 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppMigrations.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/AppMigrations.kt @@ -11,22 +11,23 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences import ca.gosyer.jui.uicore.vm.ContextWrapper import me.tatarka.inject.annotations.Inject -class AppMigrations @Inject constructor( - private val migrationPreferences: MigrationPreferences, - private val contextWrapper: ContextWrapper, -) { +class AppMigrations + @Inject + constructor( + private val migrationPreferences: MigrationPreferences, + private val contextWrapper: ContextWrapper, + ) { + fun runMigrations(): Boolean { + val oldVersion = migrationPreferences.appVersion().get() + if (oldVersion < BuildConfig.MIGRATION_CODE) { + migrationPreferences.appVersion().set(BuildConfig.MIGRATION_CODE) - fun runMigrations(): Boolean { - val oldVersion = migrationPreferences.appVersion().get() - if (oldVersion < BuildConfig.MIGRATION_CODE) { - migrationPreferences.appVersion().set(BuildConfig.MIGRATION_CODE) - - // Fresh install - if (oldVersion == 0) { - return false + // Fresh install + if (oldVersion == 0) { + return false + } + return true } - return true + return false } - return false } -} diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/Slf4jLogFactory.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/Slf4jLogFactory.kt index f06bac0b..16b3c221 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/Slf4jLogFactory.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/logging/Slf4jLogFactory.kt @@ -12,32 +12,46 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory class Slf4jLogFactory : LogFactory { - - override fun createKmLog(tag: String, className: String): KmLog { + override fun createKmLog( + tag: String, + className: String, + ): KmLog { return Slf4jLog(tag) } } class Slf4jLog(tag: String) : KmLog(tag) { - private val logger: Logger = LoggerFactory.getLogger(tag) - override fun verbose(tag: String, msg: String) { + override fun verbose( + tag: String, + msg: String, + ) { super.verbose(tag, msg) logger.trace(msg) } - override fun debug(tag: String, msg: String) { + override fun debug( + tag: String, + msg: String, + ) { super.debug(tag, msg) logger.debug(msg) } - override fun info(tag: String, msg: String) { + override fun info( + tag: String, + msg: String, + ) { super.info(tag, msg) logger.info(msg) } - override fun warn(tag: String, msg: String, t: Throwable?) { + override fun warn( + tag: String, + msg: String, + t: Throwable?, + ) { super.warn(tag, msg, t) if (t != null) { logger.warn(msg, t) @@ -46,7 +60,11 @@ class Slf4jLog(tag: String) : KmLog(tag) { } } - override fun error(tag: String, msg: String, t: Throwable?) { + override fun error( + tag: String, + msg: String, + t: Throwable?, + ) { super.error(tag, msg, t) if (t != null) { logger.error(msg, t) diff --git a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt index 94da022c..a5c3c777 100644 --- a/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt +++ b/desktop/src/main/kotlin/ca/gosyer/jui/desktop/main.kt @@ -253,7 +253,10 @@ suspend fun main() { } @Composable -fun ToastOverlay(modifier: Modifier, context: ContextWrapper) { +fun ToastOverlay( + modifier: Modifier, + context: ContextWrapper, +) { var toast by remember { mutableStateOf?>(null) } LaunchedEffect(Unit) { context.toasts diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt index 94b528da..6ac77625 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ServerListeners.kt @@ -23,36 +23,40 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ServerListeners @Inject constructor() { - val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +class ServerListeners + @Inject + constructor() { + val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private fun Flow.startWith(value: T) = onStart { emit(value) } + private fun Flow.startWith(value: T) = onStart { emit(value) } - private val _mangaListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val mangaListener = _mangaListener.asSharedFlow() + private val _mangaListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val mangaListener = _mangaListener.asSharedFlow() - private val _chapterIndexesListener = MutableSharedFlow?>>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val chapterIndexesListener = _chapterIndexesListener.asSharedFlow() + private val _chapterIndexesListener = MutableSharedFlow?>>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val chapterIndexesListener = _chapterIndexesListener.asSharedFlow() - private val _chapterIdsListener = MutableSharedFlow>>( - extraBufferCapacity = Channel.UNLIMITED, - ) - val chapterIdsListener = _chapterIdsListener.asSharedFlow() + private val _chapterIdsListener = MutableSharedFlow>>( + extraBufferCapacity = Channel.UNLIMITED, + ) + val chapterIdsListener = _chapterIdsListener.asSharedFlow() - private val categoryMangaListener = MutableSharedFlow( - extraBufferCapacity = Channel.UNLIMITED, - ) + private val categoryMangaListener = MutableSharedFlow( + extraBufferCapacity = Channel.UNLIMITED, + ) - private val extensionListener = MutableSharedFlow>( - extraBufferCapacity = Channel.UNLIMITED, - ) + private val extensionListener = MutableSharedFlow>( + extraBufferCapacity = Channel.UNLIMITED, + ) - fun combineMangaUpdates(flow: Flow, predate: (suspend (List) -> Boolean)? = null) = - if (predate != null) { + fun combineMangaUpdates( + flow: Flow, + predate: (suspend (List) -> Boolean)? = null, + ) = if (predate != null) { _mangaListener .filter(predate) .startWith(Unit) @@ -62,14 +66,16 @@ class ServerListeners @Inject constructor() { .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) .flatMapLatest { flow } - fun updateManga(vararg ids: Long) { - scope.launch { - _mangaListener.emit(ids.toList()) + fun updateManga(vararg ids: Long) { + scope.launch { + _mangaListener.emit(ids.toList()) + } } - } - fun combineCategoryManga(flow: Flow, predate: (suspend (Long) -> Boolean)? = null) = - if (predate != null) { + fun combineCategoryManga( + flow: Flow, + predate: (suspend (Long) -> Boolean)? = null, + ) = if (predate != null) { categoryMangaListener.filter(predate).startWith(-1) } else { categoryMangaListener.startWith(-1) @@ -77,58 +83,70 @@ class ServerListeners @Inject constructor() { .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) .flatMapLatest { flow } - fun updateCategoryManga(id: Long) { - scope.launch { - categoryMangaListener.emit(id) - } - } - - fun combineChapters( - flow: Flow, - indexPredate: (suspend (Long, List?) -> Boolean)? = null, - idPredate: (suspend (Long?, List) -> Boolean)? = null, - ): Flow { - val indexListener = if (indexPredate != null) { - _chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit) - } else { - _chapterIndexesListener.startWith(Unit) - } - val idsListener = if (idPredate != null) { - _chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit) - } else { - _chapterIdsListener.startWith(Unit) + fun updateCategoryManga(id: Long) { + scope.launch { + categoryMangaListener.emit(id) + } } - return combine(indexListener, idsListener) { _, _ -> } - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .flatMapLatest { flow } - } + fun combineChapters( + flow: Flow, + indexPredate: (suspend (Long, List?) -> Boolean)? = null, + idPredate: (suspend (Long?, List) -> Boolean)? = null, + ): Flow { + val indexListener = if (indexPredate != null) { + _chapterIndexesListener.filter { indexPredate(it.first, it.second) }.startWith(Unit) + } else { + _chapterIndexesListener.startWith(Unit) + } + val idsListener = if (idPredate != null) { + _chapterIdsListener.filter { idPredate(it.first, it.second) }.startWith(Unit) + } else { + _chapterIdsListener.startWith(Unit) + } - fun updateChapters(mangaId: Long, chapterIndexes: List) { - scope.launch { - _chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null }) + return combine(indexListener, idsListener) { _, _ -> } + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .flatMapLatest { flow } + } + + fun updateChapters( + mangaId: Long, + chapterIndexes: List, + ) { + scope.launch { + _chapterIndexesListener.emit(mangaId to chapterIndexes.ifEmpty { null }) + } + } + + fun updateChapters( + mangaId: Long, + vararg chapterIndexes: Int, + ) { + scope.launch { + _chapterIndexesListener.emit(mangaId to chapterIndexes.toList().ifEmpty { null }) + } + } + + fun updateChapters( + mangaId: Long?, + chapterIds: List, + ) { + scope.launch { + _chapterIdsListener.emit(mangaId to chapterIds) + } + } + + fun updateChapters( + mangaId: Long?, + vararg chapterIds: Long, + ) { + scope.launch { + _chapterIdsListener.emit(mangaId to chapterIds.toList()) + } + } + + companion object { + private val log = logging() } } - - fun updateChapters(mangaId: Long, vararg chapterIndexes: Int) { - scope.launch { - _chapterIndexesListener.emit(mangaId to chapterIndexes.toList().ifEmpty { null }) - } - } - - fun updateChapters(mangaId: Long?, chapterIds: List) { - scope.launch { - _chapterIdsListener.emit(mangaId to chapterIds) - } - } - - fun updateChapters(mangaId: Long?, vararg chapterIds: Long) { - scope.launch { - _chapterIdsListener.emit(mangaId to chapterIds.toList()) - } - } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt index 8449415a..18277fb6 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/SharedDomainComponent.kt @@ -60,8 +60,10 @@ interface SharedDomainComponent : CoreComponent { @AppScope @Provides - fun httpFactory(serverPreferences: ServerPreferences, json: Json) = - httpClient(serverPreferences, json) + fun httpFactory( + serverPreferences: ServerPreferences, + json: Json, + ) = httpClient(serverPreferences, json) @get:AppScope @get:Provides diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt index a3829825..2fc446a8 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ExportBackupFile.kt @@ -13,19 +13,22 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ExportBackupFile @Inject constructor(private val backupRepository: BackupRepository) { +class ExportBackupFile + @Inject + constructor(private val backupRepository: BackupRepository) { + suspend fun await( + block: HttpRequestBuilder.() -> Unit = {}, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(block) + .catch { + onError(it) + log.warn(it) { "Failed to export backup" } + } + .singleOrNull() - suspend fun await(block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}) = asFlow(block) - .catch { - onError(it) - log.warn(it) { "Failed to export backup" } + fun asFlow(block: HttpRequestBuilder.() -> Unit = {}) = backupRepository.exportBackupFile(block) + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow(block: HttpRequestBuilder.() -> Unit = {}) = - backupRepository.exportBackupFile(block) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt index a584518f..6eb45adc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ImportBackupFile.kt @@ -14,19 +14,26 @@ import me.tatarka.inject.annotations.Inject import okio.Path import org.lighthousegames.logging.logging -class ImportBackupFile @Inject constructor(private val backupRepository: BackupRepository) { +class ImportBackupFile + @Inject + constructor(private val backupRepository: BackupRepository) { + suspend fun await( + file: Path, + block: HttpRequestBuilder.() -> Unit = {}, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(file, block) + .catch { + onError(it) + log.warn(it) { "Failed to import backup ${file.name}" } + } + .singleOrNull() - suspend fun await(file: Path, block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}) = asFlow(file, block) - .catch { - onError(it) - log.warn(it) { "Failed to import backup ${file.name}" } + fun asFlow( + file: Path, + block: HttpRequestBuilder.() -> Unit = {}, + ) = backupRepository.importBackupFile(BackupRepository.buildBackupFormData(file), block) + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = - backupRepository.importBackupFile(BackupRepository.buildBackupFormData(file), block) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt index 02ca3d96..23da9c97 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/interactor/ValidateBackupFile.kt @@ -14,19 +14,26 @@ import me.tatarka.inject.annotations.Inject import okio.Path import org.lighthousegames.logging.logging -class ValidateBackupFile @Inject constructor(private val backupRepository: BackupRepository) { +class ValidateBackupFile + @Inject + constructor(private val backupRepository: BackupRepository) { + suspend fun await( + file: Path, + block: HttpRequestBuilder.() -> Unit = {}, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(file, block) + .catch { + onError(it) + log.warn(it) { "Failed to validate backup ${file.name}" } + } + .singleOrNull() - suspend fun await(file: Path, block: HttpRequestBuilder.() -> Unit = {}, onError: suspend (Throwable) -> Unit = {}) = asFlow(file, block) - .catch { - onError(it) - log.warn(it) { "Failed to validate backup ${file.name}" } + fun asFlow( + file: Path, + block: HttpRequestBuilder.() -> Unit = {}, + ) = backupRepository.validateBackupFile(BackupRepository.buildBackupFormData(file), block) + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow(file: Path, block: HttpRequestBuilder.() -> Unit = {}) = - backupRepository.validateBackupFile(BackupRepository.buildBackupFormData(file), block) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt index 14002929..7e98de74 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/backup/service/BackupRepository.kt @@ -26,7 +26,6 @@ import okio.Path import okio.buffer interface BackupRepository { - @Multipart @POST("api/v1/backup/import/file") fun importBackupFile( @@ -47,15 +46,16 @@ interface BackupRepository { ): Flow companion object { - fun buildBackupFormData(file: Path) = formData { - append( - "backup.proto.gz", - FileSystem.SYSTEM.source(file).buffer().readByteArray(), - Headers.build { - append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString()) - append(HttpHeaders.ContentDisposition, "filename=backup.proto.gz") - }, - ) - } + fun buildBackupFormData(file: Path) = + formData { + append( + "backup.proto.gz", + FileSystem.SYSTEM.source(file).buffer().readByteArray(), + Headers.build { + append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString()) + append(HttpHeaders.ContentDisposition, "filename=backup.proto.gz") + }, + ) + } } } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt index aeccdee8..78bd23bc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/AddMangaToCategory.kt @@ -17,46 +17,61 @@ import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AddMangaToCategory @Inject constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, -) { +class AddMangaToCategory + @Inject + constructor( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to add $mangaId to category $categoryId" } + } + .collect() - suspend fun await(mangaId: Long, categoryId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to add $mangaId to category $categoryId" } + suspend fun await( + manga: Manga, + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, category) + .catch { + onError(it) + log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" } + } + .collect() + + fun asFlow( + mangaId: Long, + categoryId: Long, + ) = if (categoryId != 0L) { + categoryRepository.addMangaToCategory(mangaId, categoryId) + .map { serverListeners.updateCategoryManga(categoryId) } + } else { + flow { + serverListeners.updateCategoryManga(categoryId) + emit(Unit) + } } - .collect() - suspend fun await(manga: Manga, category: Category, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, category) - .catch { - onError(it) - log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to category ${category.name}" } + fun asFlow( + manga: Manga, + category: Category, + ) = if (category.id != 0L) { + categoryRepository.addMangaToCategory(manga.id, category.id) + .map { serverListeners.updateCategoryManga(category.id) } + } else { + flow { + serverListeners.updateCategoryManga(category.id) + emit(Unit) + } } - .collect() - fun asFlow(mangaId: Long, categoryId: Long) = if (categoryId != 0L) { - categoryRepository.addMangaToCategory(mangaId, categoryId) - .map { serverListeners.updateCategoryManga(categoryId) } - } else { - flow { - serverListeners.updateCategoryManga(categoryId) - emit(Unit) + companion object { + private val log = logging() } } - - fun asFlow(manga: Manga, category: Category) = if (category.id != 0L) { - categoryRepository.addMangaToCategory(manga.id, category.id) - .map { serverListeners.updateCategoryManga(category.id) } - } else { - flow { - serverListeners.updateCategoryManga(category.id) - emit(Unit) - } - } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt index 41874f31..fee63299 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/CreateCategory.kt @@ -12,18 +12,22 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class CreateCategory @Inject constructor(private val categoryRepository: CategoryRepository) { +class CreateCategory + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + name: String, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(name) + .catch { + onError(it) + log.warn(it) { "Failed to create category $name" } + } + .collect() - suspend fun await(name: String, onError: suspend (Throwable) -> Unit = {}) = asFlow(name) - .catch { - onError(it) - log.warn(it) { "Failed to create category $name" } + fun asFlow(name: String) = categoryRepository.createCategory(name) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(name: String) = categoryRepository.createCategory(name) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt index da4adaa6..3be997da 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/DeleteCategory.kt @@ -13,27 +13,34 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class DeleteCategory @Inject constructor(private val categoryRepository: CategoryRepository) { +class DeleteCategory + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to delete category $categoryId" } + } + .collect() - suspend fun await(categoryId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to delete category $categoryId" } + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .catch { + onError(it) + log.warn(it) { "Failed to delete category ${category.name}" } + } + .collect() + + fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId) + + fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(category: Category, onError: suspend (Throwable) -> Unit = {}) = asFlow(category) - .catch { - onError(it) - log.warn(it) { "Failed to delete category ${category.name}" } - } - .collect() - - fun asFlow(categoryId: Long) = categoryRepository.deleteCategory(categoryId) - - fun asFlow(category: Category) = categoryRepository.deleteCategory(category.id) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt index 3e3b6ddb..f774c862 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetCategories.kt @@ -13,25 +13,30 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetCategories @Inject constructor(private val categoryRepository: CategoryRepository) { - - suspend fun await(dropDefault: Boolean = false, onError: suspend (Throwable) -> Unit = {}) = asFlow(dropDefault) - .catch { - onError(it) - log.warn(it) { "Failed to get categories" } - } - .singleOrNull() - - fun asFlow(dropDefault: Boolean = false) = categoryRepository.getCategories() - .map { categories -> - if (dropDefault) { - categories.filterNot { it.name.equals("default", true) } - } else { - categories +class GetCategories + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + dropDefault: Boolean = false, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(dropDefault) + .catch { + onError(it) + log.warn(it) { "Failed to get categories" } } - } + .singleOrNull() - companion object { - private val log = logging() + fun asFlow(dropDefault: Boolean = false) = + categoryRepository.getCategories() + .map { categories -> + if (dropDefault) { + categories.filterNot { it.name.equals("default", true) } + } else { + categories + } + } + + companion object { + private val log = logging() + } } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt index 3f8f0aeb..25bd5e29 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaCategories.kt @@ -13,27 +13,34 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetMangaCategories @Inject constructor(private val categoryRepository: CategoryRepository) { +class GetMangaCategories + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to get categories for $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to get categories for $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId) + + fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to get categories for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = categoryRepository.getMangaCategories(mangaId) - - fun asFlow(manga: Manga) = categoryRepository.getMangaCategories(manga.id) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt index f7b6fea8..2f8d0559 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/GetMangaListFromCategory.kt @@ -15,36 +15,45 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetMangaListFromCategory @Inject constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, -) { +class GetMangaListFromCategory + @Inject + constructor( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga list from category $categoryId" } + } + .singleOrNull() - suspend fun await(categoryId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(categoryId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga list from category $categoryId" } + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga list from category ${category.name}" } + } + .singleOrNull() + + fun asFlow(categoryId: Long) = + serverListeners.combineCategoryManga( + categoryRepository.getMangaFromCategory(categoryId), + ) { categoryId == it } + + fun asFlow(category: Category) = + serverListeners.combineCategoryManga( + categoryRepository.getMangaFromCategory(category.id), + ) { category.id == it } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(category: Category, onError: suspend (Throwable) -> Unit = {}) = asFlow(category) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga list from category ${category.name}" } - } - .singleOrNull() - - fun asFlow(categoryId: Long) = serverListeners.combineCategoryManga( - categoryRepository.getMangaFromCategory(categoryId), - ) { categoryId == it } - - fun asFlow(category: Category) = serverListeners.combineCategoryManga( - categoryRepository.getMangaFromCategory(category.id), - ) { category.id == it } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt index a0f5f706..a7fa2b7c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ModifyCategory.kt @@ -13,35 +13,50 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ModifyCategory @Inject constructor(private val categoryRepository: CategoryRepository) { +class ModifyCategory + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + categoryId: Long, + name: String, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow( + categoryId = categoryId, + name = name, + ).catch { + onError(it) + log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } + }.collect() - suspend fun await(categoryId: Long, name: String, onError: suspend (Throwable) -> Unit = {}) = asFlow( - categoryId = categoryId, - name = name, - ).catch { - onError(it) - log.warn(it) { "Failed to modify category $categoryId with options: name=$name" } - }.collect() + suspend fun await( + category: Category, + name: String? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow( + category = category, + name = name, + ).catch { + onError(it) + log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } + }.collect() - suspend fun await(category: Category, name: String? = null, onError: suspend (Throwable) -> Unit = {}) = asFlow( - category = category, - name = name, - ).catch { - onError(it) - log.warn(it) { "Failed to modify category ${category.name} with options: name=$name" } - }.collect() + fun asFlow( + categoryId: Long, + name: String, + ) = categoryRepository.modifyCategory( + categoryId = categoryId, + name = name, + ) - fun asFlow(categoryId: Long, name: String) = categoryRepository.modifyCategory( - categoryId = categoryId, - name = name, - ) + fun asFlow( + category: Category, + name: String? = null, + ) = categoryRepository.modifyCategory( + categoryId = category.id, + name = name ?: category.name, + ) - fun asFlow(category: Category, name: String? = null) = categoryRepository.modifyCategory( - categoryId = category.id, - name = name ?: category.name, - ) - - companion object { - private val log = logging() + companion object { + private val log = logging() + } } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt index 953ed1c3..e69fa7b0 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/RemoveMangaFromCategory.kt @@ -17,46 +17,61 @@ import kotlinx.coroutines.flow.map import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RemoveMangaFromCategory @Inject constructor( - private val categoryRepository: CategoryRepository, - private val serverListeners: ServerListeners, -) { +class RemoveMangaFromCategory + @Inject + constructor( + private val categoryRepository: CategoryRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to remove $mangaId from category $categoryId" } + } + .collect() - suspend fun await(mangaId: Long, categoryId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to remove $mangaId from category $categoryId" } + suspend fun await( + manga: Manga, + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, category) + .catch { + onError(it) + log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" } + } + .collect() + + fun asFlow( + mangaId: Long, + categoryId: Long, + ) = if (categoryId != 0L) { + categoryRepository.removeMangaFromCategory(mangaId, categoryId) + .map { serverListeners.updateCategoryManga(categoryId) } + } else { + flow { + serverListeners.updateCategoryManga(categoryId) + emit(Unit) + } } - .collect() - suspend fun await(manga: Manga, category: Category, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, category) - .catch { - onError(it) - log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from category ${category.name}" } + fun asFlow( + manga: Manga, + category: Category, + ) = if (category.id != 0L) { + categoryRepository.removeMangaFromCategory(manga.id, category.id) + .map { serverListeners.updateCategoryManga(category.id) } + } else { + flow { + serverListeners.updateCategoryManga(category.id) + emit(Unit) + } } - .collect() - fun asFlow(mangaId: Long, categoryId: Long) = if (categoryId != 0L) { - categoryRepository.removeMangaFromCategory(mangaId, categoryId) - .map { serverListeners.updateCategoryManga(categoryId) } - } else { - flow { - serverListeners.updateCategoryManga(categoryId) - emit(Unit) + companion object { + private val log = logging() } } - - fun asFlow(manga: Manga, category: Category) = if (category.id != 0L) { - categoryRepository.removeMangaFromCategory(manga.id, category.id) - .map { serverListeners.updateCategoryManga(category.id) } - } else { - flow { - serverListeners.updateCategoryManga(category.id) - emit(Unit) - } - } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt index e4a54b3f..704b95b6 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/ReorderCategory.kt @@ -12,18 +12,26 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReorderCategory @Inject constructor(private val categoryRepository: CategoryRepository) { +class ReorderCategory + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + to: Int, + from: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(to, from) + .catch { + onError(it) + log.warn(it) { "Failed to move category from $from to $to" } + } + .collect() - suspend fun await(to: Int, from: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(to, from) - .catch { - onError(it) - log.warn(it) { "Failed to move category from $from to $to" } + fun asFlow( + to: Int, + from: Int, + ) = categoryRepository.reorderCategory(to, from) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(to: Int, from: Int) = categoryRepository.reorderCategory(to, from) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt index bc639df8..ff4e09df 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/category/interactor/UpdateCategoryMeta.kt @@ -14,34 +14,35 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateCategoryMeta @Inject constructor(private val categoryRepository: CategoryRepository) { +class UpdateCategoryMeta + @Inject + constructor(private val categoryRepository: CategoryRepository) { + suspend fun await( + category: Category, + example: Int = category.meta.example, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category, example) + .catch { + onError(it) + log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" } + } + .collect() - suspend fun await( - category: Category, - example: Int = category.meta.example, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(category, example) - .catch { - onError(it) - log.warn(it) { "Failed to update ${category.name}(${category.id}) meta" } + fun asFlow( + category: Category, + example: Int = category.meta.example, + ) = flow { + if (example != category.meta.example) { + categoryRepository.updateCategoryMeta( + category.id, + "example", + example.toString(), + ).collect() + } + emit(Unit) } - .collect() - fun asFlow( - category: Category, - example: Int = category.meta.example, - ) = flow { - if (example != category.meta.example) { - categoryRepository.updateCategoryMeta( - category.id, - "example", - example.toString(), - ).collect() + companion object { + private val log = logging() } - emit(Unit) } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/BatchUpdateChapter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/BatchUpdateChapter.kt index cb50721c..e0e5b019 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/BatchUpdateChapter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/BatchUpdateChapter.kt @@ -20,236 +20,237 @@ import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging import kotlin.jvm.JvmName -class BatchUpdateChapter @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class BatchUpdateChapter + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + @JvmName("awaitChapters") + suspend fun await( + mangaId: Long, + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, chapters, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update multiple chapters of $mangaId" } + } + .collect() - @JvmName("awaitChapters") - suspend fun await( - mangaId: Long, - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, chapters, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update multiple chapters of $mangaId" } - } - .collect() + suspend fun await( + mangaId: Long, + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, chapterIds, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update multiple chapters of $mangaId" } + } + .collect() - suspend fun await( - mangaId: Long, - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, chapterIds, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update multiple chapters of $mangaId" } - } - .collect() + @JvmName("awaitChapters") + suspend fun await( + manga: Manga, + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, chapters, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update multiple chapters of ${manga.title}(${manga.id})" } + } + .collect() - @JvmName("awaitChapters") - suspend fun await( - manga: Manga, - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, chapters, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update multiple chapters of ${manga.title}(${manga.id})" } - } - .collect() + suspend fun await( + manga: Manga, + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, chapterIds, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update multiple chapters of ${manga.title}(${manga.id})" } + } + .collect() - suspend fun await( - manga: Manga, - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, chapterIds, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update multiple chapters of ${manga.title}(${manga.id})" } - } - .collect() + @JvmName("awaitChapters") + suspend fun await( + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapters, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update multiple chapters" } + } + .collect() - @JvmName("awaitChapters") - suspend fun await( - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapters, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update multiple chapters" } - } - .collect() + suspend fun await( + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterIds, isRead, isBookmarked, lastPageRead, delete) + .catch { + onError(it) + log.warn(it) { "Failed to update update multiple chapters" } + } + .collect() - suspend fun await( - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapterIds, isRead, isBookmarked, lastPageRead, delete) - .catch { - onError(it) - log.warn(it) { "Failed to update update multiple chapters" } - } - .collect() - - @JvmName("asFlowChapters") - fun asFlow( - mangaId: Long, - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = mangaId, - chapterIds = chapters.map { it.id }, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - fun asFlow( - mangaId: Long, - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = mangaId, - chapterIds = chapterIds, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - @JvmName("asFlowChapters") - fun asFlow( - manga: Manga, - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = manga.id, - chapterIds = chapters.map { it.id }, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - fun asFlow( - manga: Manga, - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = manga.id, - chapterIds = chapterIds, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - @JvmName("asFlowChapters") - fun asFlow( - chapters: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = null, - chapterIds = chapters.map { it.id }, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - fun asFlow( - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = getFlow( - mangaId = null, - chapterIds = chapterIds, - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ) - - private fun getFlow( - mangaId: Long?, - chapterIds: List, - isRead: Boolean? = null, - isBookmarked: Boolean? = null, - lastPageRead: Int? = null, - delete: Boolean? = null, - ) = if (mangaId != null) { - chapterRepository.batchUpdateChapter( - mangaId, - MangaChapterBatchEditInput( - chapterIds = chapterIds, - change = ChapterChange( - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ), - ), + @JvmName("asFlowChapters") + fun asFlow( + mangaId: Long, + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = mangaId, + chapterIds = chapters.map { it.id }, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, ) - } else { - chapterRepository.batchUpdateChapter( - ChapterBatchEditInput( - chapterIds = chapterIds, - change = ChapterChange( - isRead = isRead, - isBookmarked = isBookmarked, - lastPageRead = lastPageRead, - delete = delete, - ), - ), - ) - }.onEach { - serverListeners.updateChapters(mangaId, chapterIds) - } - companion object { - private val log = logging() + fun asFlow( + mangaId: Long, + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = mangaId, + chapterIds = chapterIds, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ) + + @JvmName("asFlowChapters") + fun asFlow( + manga: Manga, + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = manga.id, + chapterIds = chapters.map { it.id }, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ) + + fun asFlow( + manga: Manga, + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = manga.id, + chapterIds = chapterIds, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ) + + @JvmName("asFlowChapters") + fun asFlow( + chapters: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = null, + chapterIds = chapters.map { it.id }, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ) + + fun asFlow( + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = getFlow( + mangaId = null, + chapterIds = chapterIds, + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ) + + private fun getFlow( + mangaId: Long?, + chapterIds: List, + isRead: Boolean? = null, + isBookmarked: Boolean? = null, + lastPageRead: Int? = null, + delete: Boolean? = null, + ) = if (mangaId != null) { + chapterRepository.batchUpdateChapter( + mangaId, + MangaChapterBatchEditInput( + chapterIds = chapterIds, + change = ChapterChange( + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ), + ), + ) + } else { + chapterRepository.batchUpdateChapter( + ChapterBatchEditInput( + chapterIds = chapterIds, + change = ChapterChange( + isRead = isRead, + isBookmarked = isBookmarked, + lastPageRead = lastPageRead, + delete = delete, + ), + ), + ) + }.onEach { + serverListeners.updateChapters(mangaId, chapterIds) + } + + companion object { + private val log = logging() + } } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt index 7168af6d..a56841e2 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/DeleteChapterDownload.kt @@ -16,42 +16,61 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class DeleteChapterDownload @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class DeleteChapterDownload + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for $index of $mangaId" } + } + .collect() - suspend fun await(mangaId: Long, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, index) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .catch { + onError(it) + log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + ) = chapterRepository.deleteChapterDownload(mangaId, index) + .onEach { serverListeners.updateChapters(mangaId, index) } + + fun asFlow( + manga: Manga, + index: Int, + ) = chapterRepository.deleteChapterDownload(manga.id, index) + .onEach { serverListeners.updateChapters(manga.id, index) } + + fun asFlow(chapter: Chapter) = + chapterRepository.deleteChapterDownload(chapter.mangaId, chapter.index) + .onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } + + companion object { + private val log = logging() } - .collect() - - suspend fun await(manga: Manga, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, index) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await(chapter: Chapter, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapter) - .catch { - onError(it) - log.warn(it) { "Failed to delete chapter download for ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow(mangaId: Long, index: Int) = chapterRepository.deleteChapterDownload(mangaId, index) - .onEach { serverListeners.updateChapters(mangaId, index) } - - fun asFlow(manga: Manga, index: Int) = chapterRepository.deleteChapterDownload(manga.id, index) - .onEach { serverListeners.updateChapters(manga.id, index) } - - fun asFlow(chapter: Chapter) = chapterRepository.deleteChapterDownload(chapter.mangaId, chapter.index) - .onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt index 420bc7bd..9ec587fc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapter.kt @@ -16,60 +16,79 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapter @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class GetChapter + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapter $index for $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, index) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapter $index for $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapter $index for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" } + } + .singleOrNull() + + fun asFlow( + mangaId: Long, + index: Int, + ) = serverListeners.combineChapters( + chapterRepository.getChapter(mangaId, index), + indexPredate = { id, chapterIndexes -> + id == mangaId && (chapterIndexes == null || index in chapterIndexes) + }, + idPredate = { id, _ -> id == mangaId }, + ) + + fun asFlow( + manga: Manga, + index: Int, + ) = serverListeners.combineChapters( + chapterRepository.getChapter(manga.id, index), + indexPredate = { id, chapterIndexes -> + id == manga.id && (chapterIndexes == null || index in chapterIndexes) + }, + idPredate = { id, _ -> id == manga.id }, + ) + + fun asFlow(chapter: Chapter) = + serverListeners.combineChapters( + chapterRepository.getChapter(chapter.mangaId, chapter.index), + indexPredate = { id, chapterIndexes -> + id == chapter.mangaId && (chapterIndexes == null || chapter.index in chapterIndexes) + }, + idPredate = { id, _ -> id == chapter.mangaId }, + ) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, index) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapter $index for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - suspend fun await(chapter: Chapter, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapter) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapter ${chapter.index} for ${chapter.mangaId}" } - } - .singleOrNull() - - fun asFlow(mangaId: Long, index: Int) = serverListeners.combineChapters( - chapterRepository.getChapter(mangaId, index), - indexPredate = { id, chapterIndexes -> - id == mangaId && (chapterIndexes == null || index in chapterIndexes) - }, - idPredate = { id, _ -> id == mangaId }, - ) - - fun asFlow(manga: Manga, index: Int) = serverListeners.combineChapters( - chapterRepository.getChapter(manga.id, index), - indexPredate = { id, chapterIndexes -> - id == manga.id && (chapterIndexes == null || index in chapterIndexes) - }, - idPredate = { id, _ -> id == manga.id }, - ) - - fun asFlow(chapter: Chapter) = serverListeners.combineChapters( - chapterRepository.getChapter(chapter.mangaId, chapter.index), - indexPredate = { id, chapterIndexes -> - id == chapter.mangaId && (chapterIndexes == null || chapter.index in chapterIndexes) - }, - idPredate = { id, _ -> id == chapter.mangaId }, - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPage.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPage.kt index 73cbbdc6..183dcecf 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPage.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapterPage.kt @@ -15,67 +15,68 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapterPage @Inject constructor(private val chapterRepository: ChapterRepository) { +class GetChapterPage + @Inject + constructor(private val chapterRepository: ChapterRepository) { + suspend fun await( + mangaId: Long, + index: Int, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index, pageNum, block) + .catch { + onError(it) + log.warn(it) { "Failed to get page $pageNum for chapter $index for $mangaId" } + } + .singleOrNull() - suspend fun await( - mangaId: Long, - index: Int, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, index, pageNum, block) - .catch { - onError(it) - log.warn(it) { "Failed to get page $pageNum for chapter $index for $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index, pageNum, block) + .catch { + onError(it) + log.warn(it) { "Failed to get page $pageNum for chapter $index for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + suspend fun await( + chapter: Chapter, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, pageNum, block) + .catch { + onError(it) + log.warn(it) { "Failed to get page $pageNum for chapter ${chapter.index} for ${chapter.mangaId}" } + } + .singleOrNull() + + fun asFlow( + mangaId: Long, + index: Int, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + ) = chapterRepository.getPage(mangaId, index, pageNum, block) + + fun asFlow( + manga: Manga, + index: Int, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + ) = chapterRepository.getPage(manga.id, index, pageNum, block) + + fun asFlow( + chapter: Chapter, + pageNum: Int, + block: HttpRequestBuilder.() -> Unit, + ) = chapterRepository.getPage(chapter.mangaId, chapter.index, pageNum, block) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await( - manga: Manga, - index: Int, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, index, pageNum, block) - .catch { - onError(it) - log.warn(it) { "Failed to get page $pageNum for chapter $index for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - suspend fun await( - chapter: Chapter, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, pageNum, block) - .catch { - onError(it) - log.warn(it) { "Failed to get page $pageNum for chapter ${chapter.index} for ${chapter.mangaId}" } - } - .singleOrNull() - - fun asFlow( - mangaId: Long, - index: Int, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - ) = chapterRepository.getPage(mangaId, index, pageNum, block) - - fun asFlow( - manga: Manga, - index: Int, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - ) = chapterRepository.getPage(manga.id, index, pageNum, block) - - fun asFlow( - chapter: Chapter, - pageNum: Int, - block: HttpRequestBuilder.() -> Unit, - ) = chapterRepository.getPage(chapter.mangaId, chapter.index, pageNum, block) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt index a45e8a44..f9995e21 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/GetChapters.kt @@ -15,40 +15,49 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetChapters @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class GetChapters + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapters for $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapters for $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + serverListeners.combineChapters( + chapterRepository.getChapters(mangaId), + indexPredate = { id, _ -> id == mangaId }, + idPredate = { id, _ -> id == mangaId }, + ) + + fun asFlow(manga: Manga) = + serverListeners.combineChapters( + chapterRepository.getChapters(manga.id), + indexPredate = { id, _ -> id == manga.id }, + idPredate = { id, _ -> id == manga.id }, + ) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get chapters for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = serverListeners.combineChapters( - chapterRepository.getChapters(mangaId), - indexPredate = { id, _ -> id == mangaId }, - idPredate = { id, _ -> id == mangaId }, - ) - - fun asFlow(manga: Manga) = serverListeners.combineChapters( - chapterRepository.getChapters(manga.id), - indexPredate = { id, _ -> id == manga.id }, - idPredate = { id, _ -> id == manga.id }, - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt index 31909809..c5e3d15a 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/RefreshChapters.kt @@ -15,32 +15,41 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RefreshChapters @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class RefreshChapters + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to refresh chapters for $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to refresh chapters for $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + chapterRepository.getChapters(mangaId, true) + .onEach { serverListeners.updateChapters(mangaId) } + + fun asFlow(manga: Manga) = + chapterRepository.getChapters(manga.id, true) + .onEach { serverListeners.updateChapters(manga.id) } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to refresh chapters for ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = chapterRepository.getChapters(mangaId, true) - .onEach { serverListeners.updateChapters(mangaId) } - - fun asFlow(manga: Manga) = chapterRepository.getChapters(manga.id, true) - .onEach { serverListeners.updateChapters(manga.id) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterBookmarked.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterBookmarked.kt index ba367ad0..4d9cffa7 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterBookmarked.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterBookmarked.kt @@ -16,76 +16,77 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterBookmarked @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class UpdateChapterBookmarked + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + bookmarked: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index, bookmarked) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapter $index of $mangaId" } + } + .collect() - suspend fun await( - mangaId: Long, - index: Int, - bookmarked: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, index, bookmarked) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapter $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + bookmarked: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index, bookmarked) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapter $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + bookmarked: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, bookmarked) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + bookmarked: Boolean, + ) = chapterRepository.updateChapter( + mangaId = mangaId, + chapterIndex = index, + bookmarked = bookmarked, + ).onEach { serverListeners.updateChapters(mangaId, index) } + + fun asFlow( + manga: Manga, + index: Int, + bookmarked: Boolean, + ) = chapterRepository.updateChapter( + mangaId = manga.id, + chapterIndex = index, + bookmarked = bookmarked, + ).onEach { serverListeners.updateChapters(manga.id, index) } + + fun asFlow( + chapter: Chapter, + bookmarked: Boolean, + ) = chapterRepository.updateChapter( + mangaId = chapter.mangaId, + chapterIndex = chapter.index, + bookmarked = bookmarked, + ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } + + companion object { + private val log = logging() } - .collect() - - suspend fun await( - manga: Manga, - index: Int, - bookmarked: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, index, bookmarked) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapter $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await( - chapter: Chapter, - bookmarked: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, bookmarked) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter bookmark for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - mangaId: Long, - index: Int, - bookmarked: Boolean, - ) = chapterRepository.updateChapter( - mangaId = mangaId, - chapterIndex = index, - bookmarked = bookmarked, - ).onEach { serverListeners.updateChapters(mangaId, index) } - - fun asFlow( - manga: Manga, - index: Int, - bookmarked: Boolean, - ) = chapterRepository.updateChapter( - mangaId = manga.id, - chapterIndex = index, - bookmarked = bookmarked, - ).onEach { serverListeners.updateChapters(manga.id, index) } - - fun asFlow( - chapter: Chapter, - bookmarked: Boolean, - ) = chapterRepository.updateChapter( - mangaId = chapter.mangaId, - chapterIndex = chapter.index, - bookmarked = bookmarked, - ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt index d346e8f8..0bf93e75 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterLastPageRead.kt @@ -16,76 +16,77 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterLastPageRead @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class UpdateChapterLastPageRead + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + lastPageRead: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter last page read for chapter $index of $mangaId" } + } + .collect() - suspend fun await( - mangaId: Long, - index: Int, - lastPageRead: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, index, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter last page read for chapter $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + lastPageRead: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter last page read for chapter $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + lastPageRead: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, lastPageRead) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + lastPageRead: Int, + ) = chapterRepository.updateChapter( + mangaId = mangaId, + chapterIndex = index, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(mangaId, index) } + + fun asFlow( + manga: Manga, + index: Int, + lastPageRead: Int, + ) = chapterRepository.updateChapter( + mangaId = manga.id, + chapterIndex = index, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(manga.id, index) } + + fun asFlow( + chapter: Chapter, + lastPageRead: Int, + ) = chapterRepository.updateChapter( + mangaId = chapter.mangaId, + chapterIndex = chapter.index, + lastPageRead = lastPageRead, + ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } + + companion object { + private val log = logging() } - .collect() - - suspend fun await( - manga: Manga, - index: Int, - lastPageRead: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, index, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter last page read for chapter $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await( - chapter: Chapter, - lastPageRead: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, lastPageRead) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter last page read for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - mangaId: Long, - index: Int, - lastPageRead: Int, - ) = chapterRepository.updateChapter( - mangaId = mangaId, - chapterIndex = index, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(mangaId, index) } - - fun asFlow( - manga: Manga, - index: Int, - lastPageRead: Int, - ) = chapterRepository.updateChapter( - mangaId = manga.id, - chapterIndex = index, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(manga.id, index) } - - fun asFlow( - chapter: Chapter, - lastPageRead: Int, - ) = chapterRepository.updateChapter( - mangaId = chapter.mangaId, - chapterIndex = chapter.index, - lastPageRead = lastPageRead, - ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMarkPreviousRead.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMarkPreviousRead.kt index 5cb3ff60..5626011c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMarkPreviousRead.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMarkPreviousRead.kt @@ -16,70 +16,70 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterMarkPreviousRead @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class UpdateChapterMarkPreviousRead + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } + } + .collect() - suspend fun await( - mangaId: Long, - index: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, index) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + ) = chapterRepository.updateChapter( + mangaId = mangaId, + chapterIndex = index, + markPreviousRead = true, + ).onEach { serverListeners.updateChapters(mangaId, index) } + + fun asFlow( + manga: Manga, + index: Int, + ) = chapterRepository.updateChapter( + mangaId = manga.id, + chapterIndex = index, + markPreviousRead = true, + ).onEach { serverListeners.updateChapters(manga.id, index) } + + fun asFlow(chapter: Chapter) = + chapterRepository.updateChapter( + mangaId = chapter.mangaId, + chapterIndex = chapter.index, + markPreviousRead = true, + ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } + + companion object { + private val log = logging() } - .collect() - - suspend fun await( - manga: Manga, - index: Int, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, index) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await( - chapter: Chapter, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - mangaId: Long, - index: Int, - ) = chapterRepository.updateChapter( - mangaId = mangaId, - chapterIndex = index, - markPreviousRead = true, - ).onEach { serverListeners.updateChapters(mangaId, index) } - - fun asFlow( - manga: Manga, - index: Int, - ) = chapterRepository.updateChapter( - mangaId = manga.id, - chapterIndex = index, - markPreviousRead = true, - ).onEach { serverListeners.updateChapters(manga.id, index) } - - fun asFlow( - chapter: Chapter, - ) = chapterRepository.updateChapter( - mangaId = chapter.mangaId, - chapterIndex = chapter.index, - markPreviousRead = true, - ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt index 09b3083f..60cbe962 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterMeta.kt @@ -15,39 +15,40 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterMeta @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class UpdateChapterMeta + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + chapter: Chapter, + pageOffset: Int = chapter.meta.juiPageOffset, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, pageOffset) + .catch { + onError(it) + log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" } + } + .collect() - suspend fun await( - chapter: Chapter, - pageOffset: Int = chapter.meta.juiPageOffset, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, pageOffset) - .catch { - onError(it) - log.warn(it) { "Failed to update ${chapter.name}(${chapter.index}) meta" } + fun asFlow( + chapter: Chapter, + pageOffset: Int = chapter.meta.juiPageOffset, + ) = flow { + if (pageOffset != chapter.meta.juiPageOffset) { + chapterRepository.updateChapterMeta( + chapter.mangaId, + chapter.index, + "juiPageOffset", + pageOffset.toString(), + ).collect() + serverListeners.updateChapters(chapter.mangaId, chapter.index) + } + emit(Unit) } - .collect() - fun asFlow( - chapter: Chapter, - pageOffset: Int = chapter.meta.juiPageOffset, - ) = flow { - if (pageOffset != chapter.meta.juiPageOffset) { - chapterRepository.updateChapterMeta( - chapter.mangaId, - chapter.index, - "juiPageOffset", - pageOffset.toString(), - ).collect() - serverListeners.updateChapters(chapter.mangaId, chapter.index) + companion object { + private val log = logging() } - emit(Unit) } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt index 45ac1639..46c12af2 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/interactor/UpdateChapterRead.kt @@ -16,76 +16,77 @@ import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChapterRead @Inject constructor( - private val chapterRepository: ChapterRepository, - private val serverListeners: ServerListeners, -) { +class UpdateChapterRead + @Inject + constructor( + private val chapterRepository: ChapterRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + index: Int, + read: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index, read) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } + } + .collect() - suspend fun await( - mangaId: Long, - index: Int, - read: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(mangaId, index, read) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + read: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index, read) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + read: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, read) + .catch { + onError(it) + log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + read: Boolean, + ) = chapterRepository.updateChapter( + mangaId = mangaId, + chapterIndex = index, + read = read, + ).onEach { serverListeners.updateChapters(mangaId, index) } + + fun asFlow( + manga: Manga, + index: Int, + read: Boolean, + ) = chapterRepository.updateChapter( + mangaId = manga.id, + chapterIndex = index, + read = read, + ).onEach { serverListeners.updateChapters(manga.id, index) } + + fun asFlow( + chapter: Chapter, + read: Boolean, + ) = chapterRepository.updateChapter( + mangaId = chapter.mangaId, + chapterIndex = chapter.index, + read = read, + ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } + + companion object { + private val log = logging() } - .collect() - - suspend fun await( - manga: Manga, - index: Int, - read: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, index, read) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await( - chapter: Chapter, - read: Boolean, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(chapter, read) - .catch { - onError(it) - log.warn(it) { "Failed to update chapter read status for chapter ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow( - mangaId: Long, - index: Int, - read: Boolean, - ) = chapterRepository.updateChapter( - mangaId = mangaId, - chapterIndex = index, - read = read, - ).onEach { serverListeners.updateChapters(mangaId, index) } - - fun asFlow( - manga: Manga, - index: Int, - read: Boolean, - ) = chapterRepository.updateChapter( - mangaId = manga.id, - chapterIndex = index, - read = read, - ).onEach { serverListeners.updateChapters(manga.id, index) } - - fun asFlow( - chapter: Chapter, - read: Boolean, - ) = chapterRepository.updateChapter( - mangaId = chapter.mangaId, - chapterIndex = chapter.index, - read = read, - ).onEach { serverListeners.updateChapters(chapter.mangaId, chapter.index) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/service/ChapterRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/service/ChapterRepository.kt index 10e60dda..940537b3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/service/ChapterRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/chapter/service/ChapterRepository.kt @@ -25,7 +25,6 @@ import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow interface ChapterRepository { - @GET("api/v1/manga/{mangaId}/chapters") fun getChapters( @Path("mangaId") mangaId: Long, diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt index 9cfbf486..38d8e446 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/BatchChapterDownload.kt @@ -13,27 +13,34 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class BatchChapterDownload @Inject constructor(private val downloadRepository: DownloadRepository) { +class BatchChapterDownload + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await( + chapterIds: List, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapterIds) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapters $chapterIds for a download" } + } + .collect() - suspend fun await(chapterIds: List, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapterIds) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapters $chapterIds for a download" } + suspend fun await( + vararg chapterIds: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(*chapterIds) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" } + } + .collect() + + fun asFlow(chapterIds: List) = downloadRepository.batchDownload(DownloadEnqueue(chapterIds)) + + fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(DownloadEnqueue(chapterIds.asList())) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(vararg chapterIds: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(*chapterIds) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapters ${chapterIds.asList()} for a download" } - } - .collect() - - fun asFlow(chapterIds: List) = downloadRepository.batchDownload(DownloadEnqueue(chapterIds)) - - fun asFlow(vararg chapterIds: Long) = downloadRepository.batchDownload(DownloadEnqueue(chapterIds.asList())) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt index a11cd6bd..f78ddb78 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ClearDownloadQueue.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ClearDownloadQueue @Inject constructor(private val downloadRepository: DownloadRepository) { +class ClearDownloadQueue + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to clear download queue" } + } + .collect() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to clear download queue" } + fun asFlow() = downloadRepository.clearDownloadQueue() + + companion object { + private val log = logging() } - .collect() - - fun asFlow() = downloadRepository.clearDownloadQueue() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt index a35a6fc3..2e23f7d8 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/QueueChapterDownload.kt @@ -14,36 +14,54 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class QueueChapterDownload @Inject constructor(private val downloadRepository: DownloadRepository) { +class QueueChapterDownload + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await( + mangaId: Long, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapter $index of $mangaId for a download" } + } + .collect() - suspend fun await(mangaId: Long, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, index) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapter $index of $mangaId for a download" } + suspend fun await( + manga: Manga, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapter $index of ${manga.title}(${manga.id}) for a download" } + } + .collect() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .catch { + onError(it) + log.warn(it) { "Failed to queue chapter ${chapter.index} of ${chapter.mangaId} for a download" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + ) = downloadRepository.queueChapterDownload(mangaId, index) + + fun asFlow( + manga: Manga, + index: Int, + ) = downloadRepository.queueChapterDownload(manga.id, index) + + fun asFlow(chapter: Chapter) = downloadRepository.queueChapterDownload(chapter.mangaId, chapter.index) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(manga: Manga, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, index) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapter $index of ${manga.title}(${manga.id}) for a download" } - } - .collect() - - suspend fun await(chapter: Chapter, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapter) - .catch { - onError(it) - log.warn(it) { "Failed to queue chapter ${chapter.index} of ${chapter.mangaId} for a download" } - } - .collect() - - fun asFlow(mangaId: Long, index: Int) = downloadRepository.queueChapterDownload(mangaId, index) - - fun asFlow(manga: Manga, index: Int) = downloadRepository.queueChapterDownload(manga.id, index) - - fun asFlow(chapter: Chapter) = downloadRepository.queueChapterDownload(chapter.mangaId, chapter.index) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt index 2850c1e7..0c75386c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/ReorderChapterDownload.kt @@ -14,36 +14,62 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReorderChapterDownload @Inject constructor(private val downloadRepository: DownloadRepository) { +class ReorderChapterDownload + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await( + mangaId: Long, + index: Int, + to: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index, to) + .catch { + onError(it) + log.warn(it) { "Failed to reorder chapter download for $index of $mangaId to $to" } + } + .collect() - suspend fun await(mangaId: Long, index: Int, to: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, index, to) - .catch { - onError(it) - log.warn(it) { "Failed to reorder chapter download for $index of $mangaId to $to" } + suspend fun await( + manga: Manga, + index: Int, + to: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index, to) + .catch { + onError(it) + log.warn(it) { "Failed to reorder chapter download for $index of ${manga.title}(${manga.id}) to $to" } + } + .collect() + + suspend fun await( + chapter: Chapter, + to: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter, to) + .catch { + onError(it) + log.warn(it) { "Failed to reorder chapter download for ${chapter.index} of ${chapter.mangaId} to $to" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + to: Int, + ) = downloadRepository.reorderChapterDownload(mangaId, index, to) + + fun asFlow( + manga: Manga, + index: Int, + to: Int, + ) = downloadRepository.reorderChapterDownload(manga.id, index, to) + + fun asFlow( + chapter: Chapter, + to: Int, + ) = downloadRepository.reorderChapterDownload(chapter.mangaId, chapter.index, to) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(manga: Manga, index: Int, to: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, index, to) - .catch { - onError(it) - log.warn(it) { "Failed to reorder chapter download for $index of ${manga.title}(${manga.id}) to $to" } - } - .collect() - - suspend fun await(chapter: Chapter, to: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapter, to) - .catch { - onError(it) - log.warn(it) { "Failed to reorder chapter download for ${chapter.index} of ${chapter.mangaId} to $to" } - } - .collect() - - fun asFlow(mangaId: Long, index: Int, to: Int) = downloadRepository.reorderChapterDownload(mangaId, index, to) - - fun asFlow(manga: Manga, index: Int, to: Int) = downloadRepository.reorderChapterDownload(manga.id, index, to) - - fun asFlow(chapter: Chapter, to: Int) = downloadRepository.reorderChapterDownload(chapter.mangaId, chapter.index, to) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt index 81a19ba0..28de377f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StartDownloading.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StartDownloading @Inject constructor(private val downloadRepository: DownloadRepository) { +class StartDownloading + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to start downloader" } + } + .collect() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to start downloader" } + fun asFlow() = downloadRepository.startDownloading() + + companion object { + private val log = logging() } - .collect() - - fun asFlow() = downloadRepository.startDownloading() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt index ccd01386..fb1ef93c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopChapterDownload.kt @@ -14,36 +14,54 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StopChapterDownload @Inject constructor(private val downloadRepository: DownloadRepository) { +class StopChapterDownload + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await( + mangaId: Long, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId, index) + .catch { + onError(it) + log.warn(it) { "Failed to stop chapter download for $index of $mangaId" } + } + .collect() - suspend fun await(mangaId: Long, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId, index) - .catch { - onError(it) - log.warn(it) { "Failed to stop chapter download for $index of $mangaId" } + suspend fun await( + manga: Manga, + index: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, index) + .catch { + onError(it) + log.warn(it) { "Failed to stop chapter download for $index of ${manga.title}(${manga.id})" } + } + .collect() + + suspend fun await( + chapter: Chapter, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(chapter) + .catch { + onError(it) + log.warn(it) { "Failed to stop chapter download for ${chapter.index} of ${chapter.mangaId}" } + } + .collect() + + fun asFlow( + mangaId: Long, + index: Int, + ) = downloadRepository.stopChapterDownload(mangaId, index) + + fun asFlow( + manga: Manga, + index: Int, + ) = downloadRepository.stopChapterDownload(manga.id, index) + + fun asFlow(chapter: Chapter) = downloadRepository.stopChapterDownload(chapter.mangaId, chapter.index) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(manga: Manga, index: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga, index) - .catch { - onError(it) - log.warn(it) { "Failed to stop chapter download for $index of ${manga.title}(${manga.id})" } - } - .collect() - - suspend fun await(chapter: Chapter, onError: suspend (Throwable) -> Unit = {}) = asFlow(chapter) - .catch { - onError(it) - log.warn(it) { "Failed to stop chapter download for ${chapter.index} of ${chapter.mangaId}" } - } - .collect() - - fun asFlow(mangaId: Long, index: Int) = downloadRepository.stopChapterDownload(mangaId, index) - - fun asFlow(manga: Manga, index: Int) = downloadRepository.stopChapterDownload(manga.id, index) - - fun asFlow(chapter: Chapter) = downloadRepository.stopChapterDownload(chapter.mangaId, chapter.index) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt index 9e3f2bb2..b8af6c35 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/interactor/StopDownloading.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class StopDownloading @Inject constructor(private val downloadRepository: DownloadRepository) { +class StopDownloading + @Inject + constructor(private val downloadRepository: DownloadRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to stop downloader" } + } + .collect() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to stop downloader" } + fun asFlow() = downloadRepository.stopDownloading() + + companion object { + private val log = logging() } - .collect() - - fun asFlow() = downloadRepository.stopDownloading() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt index 84d17964..e8f440d3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/download/service/DownloadService.kt @@ -19,36 +19,38 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString import me.tatarka.inject.annotations.Inject -class DownloadService @Inject constructor( - serverPreferences: ServerPreferences, - client: Http, -) : WebsocketService(serverPreferences, client) { - override val _status: MutableStateFlow - get() = status +class DownloadService + @Inject + constructor( + serverPreferences: ServerPreferences, + client: Http, + ) : WebsocketService(serverPreferences, client) { + override val _status: MutableStateFlow + get() = status - override val query: String - get() = "/api/v1/downloads" + override val query: String + get() = "/api/v1/downloads" - override suspend fun onReceived(frame: Frame.Text) { - val status = json.decodeFromString(frame.readText()) - downloaderStatus.value = status.status - downloadQueue.value = status.queue + override suspend fun onReceived(frame: Frame.Text) { + val status = json.decodeFromString(frame.readText()) + downloaderStatus.value = status.status + downloadQueue.value = status.queue + } + + companion object { + val status = MutableStateFlow(Status.STARTING) + val downloadQueue = MutableStateFlow(emptyList()) + val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped) + + fun registerWatch(mangaId: Long) = + downloadQueue + .map { + it.filter { it.mangaId == mangaId } + } + fun registerWatches(mangaIds: Set) = + downloadQueue + .map { + it.filter { it.mangaId in mangaIds } + } + } } - - companion object { - val status = MutableStateFlow(Status.STARTING) - val downloadQueue = MutableStateFlow(emptyList()) - val downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped) - - fun registerWatch(mangaId: Long) = - downloadQueue - .map { - it.filter { it.mangaId == mangaId } - } - fun registerWatches(mangaIds: Set) = - downloadQueue - .map { - it.filter { it.mangaId in mangaIds } - } - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt index 89bdfbb1..7d623425 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/GetExtensionList.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetExtensionList @Inject constructor(private val extensionRepository: ExtensionRepository) { +class GetExtensionList + @Inject + constructor(private val extensionRepository: ExtensionRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get extension list" } + } + .singleOrNull() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get extension list" } + fun asFlow() = extensionRepository.getExtensionList() + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow() = extensionRepository.getExtensionList() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt index 49e99b0b..0a3a22e2 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtension.kt @@ -13,18 +13,22 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class InstallExtension @Inject constructor(private val extensionRepository: ExtensionRepository) { +class InstallExtension + @Inject + constructor(private val extensionRepository: ExtensionRepository) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to install extension ${extension.apkName}" } + } + .collect() - suspend fun await(extension: Extension, onError: suspend (Throwable) -> Unit = {}) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to install extension ${extension.apkName}" } + fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.installExtension(extension.pkgName) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt index 489e5d58..5a937e5f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/InstallExtensionFile.kt @@ -13,18 +13,22 @@ import me.tatarka.inject.annotations.Inject import okio.Path import org.lighthousegames.logging.logging -class InstallExtensionFile @Inject constructor(private val extensionRepository: ExtensionRepository) { +class InstallExtensionFile + @Inject + constructor(private val extensionRepository: ExtensionRepository) { + suspend fun await( + path: Path, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(path) + .catch { + onError(it) + log.warn(it) { "Failed to install extension from $path" } + } + .collect() - suspend fun await(path: Path, onError: suspend (Throwable) -> Unit = {}) = asFlow(path) - .catch { - onError(it) - log.warn(it) { "Failed to install extension from $path" } + fun asFlow(path: Path) = extensionRepository.installExtension(ExtensionRepository.buildExtensionFormData(path)) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(path: Path) = extensionRepository.installExtension(ExtensionRepository.buildExtensionFormData(path)) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt index 37396709..9c332b2a 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UninstallExtension.kt @@ -13,18 +13,22 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UninstallExtension @Inject constructor(private val extensionRepository: ExtensionRepository) { +class UninstallExtension + @Inject + constructor(private val extensionRepository: ExtensionRepository) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to uninstall extension ${extension.apkName}" } + } + .collect() - suspend fun await(extension: Extension, onError: suspend (Throwable) -> Unit = {}) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to uninstall extension ${extension.apkName}" } + fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.uninstallExtension(extension.pkgName) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt index e21f7b3f..328b70fb 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/interactor/UpdateExtension.kt @@ -13,18 +13,22 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateExtension @Inject constructor(private val extensionRepository: ExtensionRepository) { +class UpdateExtension + @Inject + constructor(private val extensionRepository: ExtensionRepository) { + suspend fun await( + extension: Extension, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(extension) + .catch { + onError(it) + log.warn(it) { "Failed to update extension ${extension.apkName}" } + } + .collect() - suspend fun await(extension: Extension, onError: suspend (Throwable) -> Unit = {}) = asFlow(extension) - .catch { - onError(it) - log.warn(it) { "Failed to update extension ${extension.apkName}" } + fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName) + + companion object { + private val log = logging() } - .collect() - - fun asFlow(extension: Extension) = extensionRepository.updateExtension(extension.pkgName) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt index 992d9839..ca211cb9 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/extension/service/ExtensionRepository.kt @@ -58,15 +58,16 @@ interface ExtensionRepository { ): Flow companion object { - fun buildExtensionFormData(file: okio.Path) = formData { - append( - "file", - FileSystem.SYSTEM.source(file).buffer().readByteArray(), - Headers.build { - append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString()) - append(HttpHeaders.ContentDisposition, "filename=file") - }, - ) - } + fun buildExtensionFormData(file: okio.Path) = + formData { + append( + "file", + FileSystem.SYSTEM.source(file).buffer().readByteArray(), + Headers.build { + append(HttpHeaders.ContentType, ContentType.MultiPart.FormData.toString()) + append(HttpHeaders.ContentDisposition, "filename=file") + }, + ) + } } } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt index 8b679d2c..b2a9de7c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/GetGlobalMeta.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetGlobalMeta @Inject constructor(private val globalRepository: GlobalRepository) { +class GetGlobalMeta + @Inject + constructor(private val globalRepository: GlobalRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get global meta" } + } + .singleOrNull() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get global meta" } + fun asFlow() = globalRepository.getGlobalMeta() + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow() = globalRepository.getGlobalMeta() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt index cad6f5d0..436567c7 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/global/interactor/UpdateGlobalMeta.kt @@ -14,33 +14,34 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateGlobalMeta @Inject constructor(private val globalRepository: GlobalRepository) { +class UpdateGlobalMeta + @Inject + constructor(private val globalRepository: GlobalRepository) { + suspend fun await( + globalMeta: GlobalMeta, + example: Int = globalMeta.example, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(globalMeta, example) + .catch { + onError(it) + log.warn(it) { "Failed to update global meta" } + } + .collect() - suspend fun await( - globalMeta: GlobalMeta, - example: Int = globalMeta.example, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(globalMeta, example) - .catch { - onError(it) - log.warn(it) { "Failed to update global meta" } + fun asFlow( + globalMeta: GlobalMeta, + example: Int = globalMeta.example, + ) = flow { + if (example != globalMeta.example) { + globalRepository.updateGlobalMeta( + "example", + example.toString(), + ).collect() + } + emit(Unit) } - .collect() - fun asFlow( - globalMeta: GlobalMeta, - example: Int = globalMeta.example, - ) = flow { - if (example != globalMeta.example) { - globalRepository.updateGlobalMeta( - "example", - example.toString(), - ).collect() + companion object { + private val log = logging() } - emit(Unit) } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt index 9e0ae32f..dab8756f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/AddMangaToLibrary.kt @@ -15,32 +15,41 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AddMangaToLibrary @Inject constructor( - private val libraryRepository: LibraryRepository, - private val serverListeners: ServerListeners, -) { +class AddMangaToLibrary + @Inject + constructor( + private val libraryRepository: LibraryRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to add $mangaId to library" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to add $mangaId to library" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + libraryRepository.addMangaToLibrary(mangaId) + .onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = + libraryRepository.addMangaToLibrary(manga.id) + .onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to add ${manga.title}(${manga.id}) to library" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = libraryRepository.addMangaToLibrary(mangaId) - .onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = libraryRepository.addMangaToLibrary(manga.id) - .onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt index f77ab478..7f68749d 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/interactor/RemoveMangaFromLibrary.kt @@ -15,32 +15,41 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RemoveMangaFromLibrary @Inject constructor( - private val libraryRepository: LibraryRepository, - private val serverListeners: ServerListeners, -) { +class RemoveMangaFromLibrary + @Inject + constructor( + private val libraryRepository: LibraryRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .catch { + onError(it) + log.warn(it) { "Failed to remove $mangaId from library" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .catch { - onError(it) - log.warn(it) { "Failed to remove $mangaId from library" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .catch { + onError(it) + log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + libraryRepository.removeMangaFromLibrary(mangaId) + .onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = + libraryRepository.removeMangaFromLibrary(manga.id) + .onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .catch { - onError(it) - log.warn(it) { "Failed to remove ${manga.title}(${manga.id}) from library" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = libraryRepository.removeMangaFromLibrary(mangaId) - .onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = libraryRepository.removeMangaFromLibrary(manga.id) - .onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/DisplayMode.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/DisplayMode.kt index 336bbbc8..c4e7665c 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/DisplayMode.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/DisplayMode.kt @@ -14,7 +14,9 @@ import kotlinx.serialization.Transient @Serializable @Stable -enum class DisplayMode(@Transient val res: StringResource) { +enum class DisplayMode( + @Transient val res: StringResource, +) { CompactGrid(MR.strings.display_compact), ComfortableGrid(MR.strings.display_comfortable), CoverOnlyGrid(MR.strings.display_cover_only), diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/Sort.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/Sort.kt index d4e71ecd..f7d1deb3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/Sort.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/model/Sort.kt @@ -14,7 +14,9 @@ import kotlinx.serialization.Transient @Serializable @Stable -enum class Sort(@Transient val res: StringResource) { +enum class Sort( + @Transient val res: StringResource, +) { ALPHABETICAL(MR.strings.sort_alphabetical), // LAST_READ, diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryPreferences.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryPreferences.kt index d29651cb..d14d902e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryPreferences.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryPreferences.kt @@ -12,7 +12,6 @@ import ca.gosyer.jui.domain.library.model.FilterState import ca.gosyer.jui.domain.library.model.Sort class LibraryPreferences(private val preferenceStore: PreferenceStore) { - fun showAllCategory(): Preference { return preferenceStore.getBoolean("show_all_category", false) } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryRepository.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryRepository.kt index 266ac1d8..ff3e40f2 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryRepository.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryRepository.kt @@ -13,7 +13,6 @@ import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow interface LibraryRepository { - @GET("api/v1/manga/{mangaId}/library") fun addMangaToLibrary( @Path("mangaId") mangaId: Long, diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt index 75440809..2b7ef1eb 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/library/service/LibraryUpdateService.kt @@ -17,25 +17,26 @@ import kotlinx.serialization.decodeFromString import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class LibraryUpdateService @Inject constructor( - serverPreferences: ServerPreferences, - client: Http, -) : WebsocketService(serverPreferences, client) { +class LibraryUpdateService + @Inject + constructor( + serverPreferences: ServerPreferences, + client: Http, + ) : WebsocketService(serverPreferences, client) { + override val _status: MutableStateFlow + get() = status - override val _status: MutableStateFlow - get() = status + override val query: String + get() = "/api/v1/update" - override val query: String - get() = "/api/v1/update" + override suspend fun onReceived(frame: Frame.Text) { + updateStatus.value = json.decodeFromString(frame.readText()) + } - override suspend fun onReceived(frame: Frame.Text) { - updateStatus.value = json.decodeFromString(frame.readText()) + companion object { + private val log = logging() + + val status = MutableStateFlow(Status.STARTING) + val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), false)) + } } - - companion object { - private val log = logging() - - val status = MutableStateFlow(Status.STARTING) - val updateStatus = MutableStateFlow(UpdateStatus(emptyMap(), false)) - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt index 6127e46a..c3a93b38 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetManga.kt @@ -15,36 +15,45 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetManga @Inject constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, -) { +class GetManga + @Inject + constructor( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + serverListeners.combineMangaUpdates( + mangaRepository.getManga(mangaId), + ) { mangaId in it } + + fun asFlow(manga: Manga) = + serverListeners.combineMangaUpdates( + mangaRepository.getManga(manga.id), + ) { manga.id in it } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = serverListeners.combineMangaUpdates( - mangaRepository.getManga(mangaId), - ) { mangaId in it } - - fun asFlow(manga: Manga) = serverListeners.combineMangaUpdates( - mangaRepository.getManga(manga.id), - ) { manga.id in it } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetMangaFull.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetMangaFull.kt index 1171bf43..b03628e1 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetMangaFull.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/GetMangaFull.kt @@ -15,36 +15,45 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetMangaFull @Inject constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, -) { +class GetMangaFull + @Inject + constructor( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get full manga $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get full manga $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to get full manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = + serverListeners.combineMangaUpdates( + mangaRepository.getMangaFull(mangaId), + ) { mangaId in it } + + fun asFlow(manga: Manga) = + serverListeners.combineMangaUpdates( + mangaRepository.getMangaFull(manga.id), + ) { manga.id in it } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to get full manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = serverListeners.combineMangaUpdates( - mangaRepository.getMangaFull(mangaId), - ) { mangaId in it } - - fun asFlow(manga: Manga) = serverListeners.combineMangaUpdates( - mangaRepository.getMangaFull(manga.id), - ) { manga.id in it } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt index 3757650a..436a34dc 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshManga.kt @@ -16,34 +16,39 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RefreshManga @Inject constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, -) { +class RefreshManga + @Inject + constructor( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh manga $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh manga $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = mangaRepository.getManga(mangaId, true).onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = mangaRepository.getManga(manga.id, true).onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - mangaRepository.getManga(mangaId, true).onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = - mangaRepository.getManga(manga.id, true).onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshMangaFull.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshMangaFull.kt index eabf13e3..c4bc532b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshMangaFull.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/RefreshMangaFull.kt @@ -16,34 +16,39 @@ import kotlinx.coroutines.flow.take import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class RefreshMangaFull @Inject constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, -) { +class RefreshMangaFull + @Inject + constructor( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + mangaId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(mangaId) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh full manga $mangaId" } + } + .singleOrNull() - suspend fun await(mangaId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(mangaId) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh full manga $mangaId" } + suspend fun await( + manga: Manga, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga) + .take(1) + .catch { + onError(it) + log.warn(it) { "Failed to refresh full manga ${manga.title}(${manga.id})" } + } + .singleOrNull() + + fun asFlow(mangaId: Long) = mangaRepository.getMangaFull(mangaId, true).onEach { serverListeners.updateManga(mangaId) } + + fun asFlow(manga: Manga) = mangaRepository.getMangaFull(manga.id, true).onEach { serverListeners.updateManga(manga.id) } + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(manga: Manga, onError: suspend (Throwable) -> Unit = {}) = asFlow(manga) - .take(1) - .catch { - onError(it) - log.warn(it) { "Failed to refresh full manga ${manga.title}(${manga.id})" } - } - .singleOrNull() - - fun asFlow(mangaId: Long) = - mangaRepository.getMangaFull(mangaId, true).onEach { serverListeners.updateManga(mangaId) } - - fun asFlow(manga: Manga) = - mangaRepository.getMangaFull(manga.id, true).onEach { serverListeners.updateManga(manga.id) } - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt index 5f81d616..5f6af7f7 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/interactor/UpdateMangaMeta.kt @@ -17,38 +17,39 @@ import kotlinx.coroutines.flow.flow import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateMangaMeta @Inject constructor( - private val mangaRepository: MangaRepository, - private val serverListeners: ServerListeners, -) { +class UpdateMangaMeta + @Inject + constructor( + private val mangaRepository: MangaRepository, + private val serverListeners: ServerListeners, + ) { + suspend fun await( + manga: Manga, + readerMode: String = manga.meta.juiReaderMode, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manga, readerMode) + .catch { + onError(it) + log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" } + } + .collect() - suspend fun await( - manga: Manga, - readerMode: String = manga.meta.juiReaderMode, - onError: suspend (Throwable) -> Unit = {}, - ) = asFlow(manga, readerMode) - .catch { - onError(it) - log.warn(it) { "Failed to update ${manga.title}(${manga.id}) meta" } + fun asFlow( + manga: Manga, + readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(), + ) = flow { + if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) { + mangaRepository.updateMangaMeta( + manga.id, + "juiReaderMode", + readerMode, + ).collect() + serverListeners.updateManga(manga.id) + } + emit(Unit) } - .collect() - fun asFlow( - manga: Manga, - readerMode: String = manga.meta.juiReaderMode.decodeURLQueryComponent(), - ) = flow { - if (readerMode.encodeURLQueryComponent() != manga.meta.juiReaderMode) { - mangaRepository.updateMangaMeta( - manga.id, - "juiReaderMode", - readerMode, - ).collect() - serverListeners.updateManga(manga.id) + companion object { + private val log = logging() } - emit(Unit) } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/model/Manga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/model/Manga.kt index a33df1c3..29a60857 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/model/Manga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/manga/model/Manga.kt @@ -59,7 +59,9 @@ data class MangaMeta( @Serializable @Stable -enum class MangaStatus(@Transient val res: StringResource) { +enum class MangaStatus( + @Transient val res: StringResource, +) { UNKNOWN(MR.strings.status_unknown), ONGOING(MR.strings.status_ongoing), COMPLETED(MR.strings.status_completed), diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt index 3e78efd3..be43add6 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/migration/interactor/RunMigrations.kt @@ -11,19 +11,20 @@ import ca.gosyer.jui.domain.migration.service.MigrationPreferences import ca.gosyer.jui.domain.reader.service.ReaderPreferences import me.tatarka.inject.annotations.Inject -class RunMigrations @Inject constructor( - private val migrationPreferences: MigrationPreferences, - private val readerPreferences: ReaderPreferences, -) { - - fun runMigrations() { - val code = migrationPreferences.version().get() - if (code <= 0) { - readerPreferences.modes().get().forEach { - readerPreferences.getMode(it).direction().delete() +class RunMigrations + @Inject + constructor( + private val migrationPreferences: MigrationPreferences, + private val readerPreferences: ReaderPreferences, + ) { + fun runMigrations() { + val code = migrationPreferences.version().get() + if (code <= 0) { + readerPreferences.modes().get().forEach { + readerPreferences.getMode(it).direction().delete() + } + migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE) + return } - migrationPreferences.version().set(BuildKonfig.MIGRATION_CODE) - return } } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/Direction.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/Direction.kt index ef655913..7a753cca 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/Direction.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/Direction.kt @@ -14,7 +14,9 @@ import kotlinx.serialization.Transient @Serializable @Stable -enum class Direction(@Transient val res: StringResource) { +enum class Direction( + @Transient val res: StringResource, +) { Down(MR.strings.dir_down), Left(MR.strings.dir_rtl), Right(MR.strings.dir_ltr), diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/ImageScale.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/ImageScale.kt index e18ad722..abdee0de 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/ImageScale.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/ImageScale.kt @@ -14,7 +14,9 @@ import kotlinx.serialization.Transient @Serializable @Stable -enum class ImageScale(@Transient val res: StringResource) { +enum class ImageScale( + @Transient val res: StringResource, +) { FitScreen(MR.strings.scale_fit_screen), Stretch(MR.strings.scale_stretch), FitWidth(MR.strings.scale_fit_width), diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/NavigationMode.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/NavigationMode.kt index 1e2eb573..927b0dd3 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/NavigationMode.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/model/NavigationMode.kt @@ -14,7 +14,9 @@ import kotlinx.serialization.Transient @Serializable @Stable -enum class NavigationMode(@Transient val res: StringResource) { +enum class NavigationMode( + @Transient val res: StringResource, +) { Disabled(MR.strings.disabled), LNavigation(MR.strings.nav_l_shaped), KindlishNavigation(MR.strings.nav_kindle_ish), diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/service/ReaderPreferences.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/service/ReaderPreferences.kt index f2bbf5b3..2053e61f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/service/ReaderPreferences.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/reader/service/ReaderPreferences.kt @@ -13,7 +13,6 @@ import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.serializer class ReaderPreferences(private val preferenceStore: PreferenceStore, private val factory: (String) -> PreferenceStore) { - fun preload(): Preference { return preferenceStore.getInt("preload", 3) } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt index b2b48a54..805b8295 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/HttpClient.kt @@ -40,7 +40,10 @@ expect val Engine: HttpClientEngineFactory expect fun HttpClientConfig.configurePlatform() -fun httpClient(serverPreferences: ServerPreferences, json: Json): Http { +fun httpClient( + serverPreferences: ServerPreferences, + json: Json, +): Http { return HttpClient(Engine) { configurePlatform() diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerPreferences.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerPreferences.kt index 8ad4b6ff..f1f7437e 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerPreferences.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/server/service/ServerPreferences.kt @@ -14,7 +14,6 @@ import ca.gosyer.jui.domain.server.model.ServerUrlPreference import io.ktor.http.Url class ServerPreferences(private val preferenceStore: PreferenceStore) { - fun server(): Preference { return preferenceStore.getString("server_url", "http://localhost") } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt index e627a5e1..3c83807f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/AboutServer.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AboutServer @Inject constructor(private val settingsRepository: SettingsRepository) { +class AboutServer + @Inject + constructor(private val settingsRepository: SettingsRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get server information" } + } + .singleOrNull() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get server information" } + fun asFlow() = settingsRepository.aboutServer() + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow() = settingsRepository.aboutServer() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt index ded04209..4de2aa77 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/settings/interactor/CheckUpdate.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class CheckUpdate @Inject constructor(private val settingsRepository: SettingsRepository) { +class CheckUpdate + @Inject + constructor(private val settingsRepository: SettingsRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to check for server updates" } + } + .singleOrNull() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to check for server updates" } + fun asFlow() = settingsRepository.checkUpdate() + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow() = settingsRepository.checkUpdate() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt index d66e3c1a..3555b216 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetFilterList.kt @@ -13,27 +13,42 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetFilterList @Inject constructor(private val sourceRepository: SourceRepository) { +class GetFilterList + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + reset: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, reset) + .catch { + onError(it) + log.warn(it) { "Failed to get filter list for ${source.displayName} with reset = $reset" } + } + .singleOrNull() - suspend fun await(source: Source, reset: Boolean, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, reset) - .catch { - onError(it) - log.warn(it) { "Failed to get filter list for ${source.displayName} with reset = $reset" } + suspend fun await( + sourceId: Long, + reset: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, reset) + .catch { + onError(it) + log.warn(it) { "Failed to get filter list for $sourceId with reset = $reset" } + } + .singleOrNull() + + fun asFlow( + source: Source, + reset: Boolean, + ) = sourceRepository.getFilterList(source.id, reset) + + fun asFlow( + sourceId: Long, + reset: Boolean, + ) = sourceRepository.getFilterList(sourceId, reset) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, reset: Boolean, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, reset) - .catch { - onError(it) - log.warn(it) { "Failed to get filter list for $sourceId with reset = $reset" } - } - .singleOrNull() - - fun asFlow(source: Source, reset: Boolean) = sourceRepository.getFilterList(source.id, reset) - - fun asFlow(sourceId: Long, reset: Boolean) = sourceRepository.getFilterList(sourceId, reset) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt index f7c5667c..be0ce6e9 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetLatestManga.kt @@ -13,27 +13,42 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetLatestManga @Inject constructor(private val sourceRepository: SourceRepository) { +class GetLatestManga + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, page) + .catch { + onError(it) + log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" } + } + .singleOrNull() - suspend fun await(source: Source, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, page) - .catch { - onError(it) - log.warn(it) { "Failed to get latest manga from ${source.displayName} on page $page" } + suspend fun await( + sourceId: Long, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, page) + .catch { + onError(it) + log.warn(it) { "Failed to get latest manga from $sourceId on page $page" } + } + .singleOrNull() + + fun asFlow( + source: Source, + page: Int, + ) = sourceRepository.getLatestManga(source.id, page) + + fun asFlow( + sourceId: Long, + page: Int, + ) = sourceRepository.getLatestManga(sourceId, page) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, page) - .catch { - onError(it) - log.warn(it) { "Failed to get latest manga from $sourceId on page $page" } - } - .singleOrNull() - - fun asFlow(source: Source, page: Int) = sourceRepository.getLatestManga(source.id, page) - - fun asFlow(sourceId: Long, page: Int) = sourceRepository.getLatestManga(sourceId, page) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt index 2afecf22..a87c5d90 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetPopularManga.kt @@ -13,27 +13,42 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetPopularManga @Inject constructor(private val sourceRepository: SourceRepository) { +class GetPopularManga + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, page) + .catch { + onError(it) + log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" } + } + .singleOrNull() - suspend fun await(source: Source, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, page) - .catch { - onError(it) - log.warn(it) { "Failed to get popular manga from ${source.displayName} on page $page" } + suspend fun await( + sourceId: Long, + page: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, page) + .catch { + onError(it) + log.warn(it) { "Failed to get popular manga from $sourceId on page $page" } + } + .singleOrNull() + + fun asFlow( + source: Source, + page: Int, + ) = sourceRepository.getPopularManga(source.id, page) + + fun asFlow( + sourceId: Long, + page: Int, + ) = sourceRepository.getPopularManga(sourceId, page) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, page) - .catch { - onError(it) - log.warn(it) { "Failed to get popular manga from $sourceId on page $page" } - } - .singleOrNull() - - fun asFlow(source: Source, page: Int) = sourceRepository.getPopularManga(source.id, page) - - fun asFlow(sourceId: Long, page: Int) = sourceRepository.getPopularManga(sourceId, page) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetQuickSearchManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetQuickSearchManga.kt index 8bc23499..b250e066 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetQuickSearchManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetQuickSearchManga.kt @@ -15,41 +15,64 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetQuickSearchManga @Inject constructor(private val sourceRepository: SourceRepository) { +class GetQuickSearchManga + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + searchTerm: String?, + page: Int, + filters: List?, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id, searchTerm, page, filters) + .catch { + onError(it) + log.warn(it) { "Failed to get quick search results from ${source.displayName} on page $page with query '$searchTerm'" } + } + .singleOrNull() - suspend fun await(source: Source, searchTerm: String?, page: Int, filters: List?, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, searchTerm, page, filters) - .catch { - onError(it) - log.warn(it) { "Failed to get quick search results from ${source.displayName} on page $page with query '$searchTerm'" } + suspend fun await( + sourceId: Long, + searchTerm: String?, + page: Int, + filters: List?, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId, searchTerm, page, filters) + .catch { + onError(it) + log.warn(it) { "Failed to get quick search results from $sourceId on page $page with query '$searchTerm'" } + } + .singleOrNull() + + fun asFlow( + source: Source, + searchTerm: String?, + page: Int, + filters: List?, + ) = sourceRepository.getQuickSearchResults( + source.id, + page, + SourceFilterData( + searchTerm?.ifBlank { null }, + filters?.ifEmpty { null }, + ), + ) + + fun asFlow( + sourceId: Long, + searchTerm: String?, + page: Int, + filters: List?, + ) = sourceRepository.getQuickSearchResults( + sourceId, + page, + SourceFilterData( + searchTerm?.ifBlank { null }, + filters?.ifEmpty { null }, + ), + ) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, searchTerm: String?, page: Int, filters: List?, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, searchTerm, page, filters) - .catch { - onError(it) - log.warn(it) { "Failed to get quick search results from $sourceId on page $page with query '$searchTerm'" } - } - .singleOrNull() - - fun asFlow(source: Source, searchTerm: String?, page: Int, filters: List?) = sourceRepository.getQuickSearchResults( - source.id, - page, - SourceFilterData( - searchTerm?.ifBlank { null }, - filters?.ifEmpty { null }, - ), - ) - - fun asFlow(sourceId: Long, searchTerm: String?, page: Int, filters: List?) = sourceRepository.getQuickSearchResults( - sourceId, - page, - SourceFilterData( - searchTerm?.ifBlank { null }, - filters?.ifEmpty { null }, - ), - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt index c70f1251..d3cd447f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSearchManga.kt @@ -13,35 +13,56 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSearchManga @Inject constructor(private val sourceRepository: SourceRepository) { +class GetSearchManga + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + searchTerm: String?, + page: Int, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(source.id, searchTerm, page) + .catch { + onError(it) + log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" } + } + .singleOrNull() - suspend fun await(source: Source, searchTerm: String?, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id, searchTerm, page) - .catch { - onError(it) - log.warn(it) { "Failed to get search results from ${source.displayName} on page $page with query '$searchTerm'" } + suspend fun await( + sourceId: Long, + searchTerm: String?, + page: Int, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, searchTerm, page) + .catch { + onError(it) + log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" } + } + .singleOrNull() + + fun asFlow( + source: Source, + searchTerm: String?, + page: Int, + ) = sourceRepository.getSearchResults( + source.id, + searchTerm?.ifBlank { null }, + page, + ) + + fun asFlow( + sourceId: Long, + searchTerm: String?, + page: Int, + ) = sourceRepository.getSearchResults( + sourceId, + searchTerm?.ifBlank { null }, + page, + ) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, searchTerm: String?, page: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, searchTerm, page) - .catch { - onError(it) - log.warn(it) { "Failed to get search results from $sourceId on page $page with query '$searchTerm'" } - } - .singleOrNull() - - fun asFlow(source: Source, searchTerm: String?, page: Int) = sourceRepository.getSearchResults( - source.id, - searchTerm?.ifBlank { null }, - page, - ) - - fun asFlow(sourceId: Long, searchTerm: String?, page: Int) = sourceRepository.getSearchResults( - sourceId, - searchTerm?.ifBlank { null }, - page, - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt index c2d18713..31514f77 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceList.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSourceList @Inject constructor(private val sourceRepository: SourceRepository) { +class GetSourceList + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to get source list" } + } + .singleOrNull() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to get source list" } + fun asFlow() = sourceRepository.getSourceList() + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow() = sourceRepository.getSourceList() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt index 26811138..125513d5 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/GetSourceSettings.kt @@ -13,27 +13,34 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetSourceSettings @Inject constructor(private val sourceRepository: SourceRepository) { +class GetSourceSettings + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(source.id) + .catch { + onError(it) + log.warn(it) { "Failed to get source settings for ${source.displayName}" } + } + .singleOrNull() - suspend fun await(source: Source, onError: suspend (Throwable) -> Unit = {}) = asFlow(source.id) - .catch { - onError(it) - log.warn(it) { "Failed to get source settings for ${source.displayName}" } + suspend fun await( + sourceId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(sourceId) + .catch { + onError(it) + log.warn(it) { "Failed to get source settings for $sourceId" } + } + .singleOrNull() + + fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id) + + fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId) + + companion object { + private val log = logging() } - .singleOrNull() - - suspend fun await(sourceId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId) - .catch { - onError(it) - log.warn(it) { "Failed to get source settings for $sourceId" } - } - .singleOrNull() - - fun asFlow(source: Source) = sourceRepository.getSourceSettings(source.id) - - fun asFlow(sourceId: Long) = sourceRepository.getSourceSettings(sourceId) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceFilter.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceFilter.kt index 951ca77a..81f62c0b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceFilter.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceFilter.kt @@ -16,57 +16,104 @@ import kotlinx.serialization.json.Json import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SetSourceFilter @Inject constructor(private val sourceRepository: SourceRepository) { +class SetSourceFilter + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + filterIndex: Int, + filter: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(source, filterIndex, filter) + .catch { + onError(it) + log.warn(it) { "Failed to set filter for ${source.displayName} with index = $filterIndex and value = $filter" } + } + .collect() - suspend fun await(source: Source, filterIndex: Int, filter: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(source, filterIndex, filter) - .catch { - onError(it) - log.warn(it) { "Failed to set filter for ${source.displayName} with index = $filterIndex and value = $filter" } + suspend fun await( + sourceId: Long, + filterIndex: Int, + filter: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, filterIndex, filter) + .catch { + onError(it) + log.warn(it) { "Failed to set filter for $sourceId with index = $filterIndex and value = $filter" } + } + .collect() + + suspend fun await( + source: Source, + filterIndex: Int, + childFilterIndex: Int, + filter: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(source, filterIndex, childFilterIndex, filter) + .catch { + onError(it) + log.warn(it) { + "Failed to set filter for ${source.displayName} with index = $filterIndex and childIndex = $childFilterIndex and value = $filter" + } + } + .collect() + + suspend fun await( + sourceId: Long, + filterIndex: Int, + childFilterIndex: Int, + filter: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, filterIndex, childFilterIndex, filter) + .catch { + onError(it) + log.warn(it) { "Failed to set filter for $sourceId with index = $filterIndex and childIndex = $childFilterIndex and value = $filter" } + } + .collect() + + fun asFlow( + source: Source, + filterIndex: Int, + filter: Any, + ) = sourceRepository.setFilter( + source.id, + SourceFilterChange(filterIndex, filter), + ) + + fun asFlow( + sourceId: Long, + filterIndex: Int, + filter: Any, + ) = sourceRepository.setFilter( + sourceId, + SourceFilterChange(filterIndex, filter), + ) + + fun asFlow( + source: Source, + filterIndex: Int, + childFilterIndex: Int, + filter: Any, + ) = sourceRepository.setFilter( + source.id, + SourceFilterChange(filterIndex, Json.encodeToString(SourceFilterChange(childFilterIndex, filter))), + ) + + fun asFlow( + sourceId: Long, + filterIndex: Int, + childFilterIndex: Int, + filter: Any, + ) = sourceRepository.setFilter( + sourceId, + SourceFilterChange(filterIndex, Json.encodeToString(SourceFilterChange(childFilterIndex, filter))), + ) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(sourceId: Long, filterIndex: Int, filter: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, filterIndex, filter) - .catch { - onError(it) - log.warn(it) { "Failed to set filter for $sourceId with index = $filterIndex and value = $filter" } - } - .collect() - - suspend fun await(source: Source, filterIndex: Int, childFilterIndex: Int, filter: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(source, filterIndex, childFilterIndex, filter) - .catch { - onError(it) - log.warn(it) { "Failed to set filter for ${source.displayName} with index = $filterIndex and childIndex = $childFilterIndex and value = $filter" } - } - .collect() - - suspend fun await(sourceId: Long, filterIndex: Int, childFilterIndex: Int, filter: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, filterIndex, childFilterIndex, filter) - .catch { - onError(it) - log.warn(it) { "Failed to set filter for $sourceId with index = $filterIndex and childIndex = $childFilterIndex and value = $filter" } - } - .collect() - - fun asFlow(source: Source, filterIndex: Int, filter: Any) = sourceRepository.setFilter( - source.id, - SourceFilterChange(filterIndex, filter), - ) - - fun asFlow(sourceId: Long, filterIndex: Int, filter: Any) = sourceRepository.setFilter( - sourceId, - SourceFilterChange(filterIndex, filter), - ) - - fun asFlow(source: Source, filterIndex: Int, childFilterIndex: Int, filter: Any) = sourceRepository.setFilter( - source.id, - SourceFilterChange(filterIndex, Json.encodeToString(SourceFilterChange(childFilterIndex, filter))), - ) - - fun asFlow(sourceId: Long, filterIndex: Int, childFilterIndex: Int, filter: Any) = sourceRepository.setFilter( - sourceId, - SourceFilterChange(filterIndex, Json.encodeToString(SourceFilterChange(childFilterIndex, filter))), - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt index 70e0d82c..4b2f7bb5 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SetSourceSetting.kt @@ -14,33 +14,54 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SetSourceSetting @Inject constructor(private val sourceRepository: SourceRepository) { +class SetSourceSetting + @Inject + constructor(private val sourceRepository: SourceRepository) { + suspend fun await( + source: Source, + settingIndex: Int, + setting: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(source, settingIndex, setting) + .catch { + onError(it) + log.warn(it) { "Failed to set setting for ${source.displayName} with index = $settingIndex and value = $setting" } + } + .collect() - suspend fun await(source: Source, settingIndex: Int, setting: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(source, settingIndex, setting) - .catch { - onError(it) - log.warn(it) { "Failed to set setting for ${source.displayName} with index = $settingIndex and value = $setting" } + suspend fun await( + sourceId: Long, + settingIndex: Int, + setting: Any, + onError: suspend (Throwable) -> Unit = { + }, + ) = asFlow(sourceId, settingIndex, setting) + .catch { + onError(it) + log.warn(it) { "Failed to set setting for $sourceId with index = $settingIndex and value = $setting" } + } + .collect() + + fun asFlow( + source: Source, + settingIndex: Int, + setting: Any, + ) = sourceRepository.setSourceSetting( + source.id, + SourcePreferenceChange(settingIndex, setting), + ) + + fun asFlow( + sourceId: Long, + settingIndex: Int, + setting: Any, + ) = sourceRepository.setSourceSetting( + sourceId, + SourcePreferenceChange(settingIndex, setting), + ) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(sourceId: Long, settingIndex: Int, setting: Any, onError: suspend (Throwable) -> Unit = {}) = asFlow(sourceId, settingIndex, setting) - .catch { - onError(it) - log.warn(it) { "Failed to set setting for $sourceId with index = $settingIndex and value = $setting" } - } - .collect() - - fun asFlow(source: Source, settingIndex: Int, setting: Any) = sourceRepository.setSourceSetting( - source.id, - SourcePreferenceChange(settingIndex, setting), - ) - - fun asFlow(sourceId: Long, settingIndex: Int, setting: Any) = sourceRepository.setSourceSetting( - sourceId, - SourcePreferenceChange(settingIndex, setting), - ) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt index bdc68835..788bcd60 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/source/interactor/SourcePager.kt @@ -31,59 +31,61 @@ import org.lighthousegames.logging.logging typealias GetMangaPage = @param:Assisted suspend (page: Int) -> MangaPage? -class SourcePager @Inject constructor( - private val getManga: GetManga, - private val serverListeners: ServerListeners, - private val fetcher: GetMangaPage, -) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { - private val sourceMutex = Mutex() +class SourcePager + @Inject + constructor( + private val getManga: GetManga, + private val serverListeners: ServerListeners, + private val fetcher: GetMangaPage, + ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { + private val sourceMutex = Mutex() - private val _sourceManga = MutableStateFlow>(emptyList()) + private val _sourceManga = MutableStateFlow>(emptyList()) - private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } } - .stateIn(this, SharingStarted.Eagerly, emptyList()) + private val mangaIds = _sourceManga.map { mangas -> mangas.map { it.id } } + .stateIn(this, SharingStarted.Eagerly, emptyList()) - private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> - coroutineScope { - manga + updatedMangaIds.filter { it in mangaIds.value }.map { - async { - getManga.await(it) - } - }.awaitAll().filterNotNull().associateBy { it.id } - } - }.stateIn(this, SharingStarted.Eagerly, emptyMap()) - - val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga -> - sourceManga.map { changedManga[it.id] ?: it } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - - private val _pageNum = MutableStateFlow(0) - val pageNum = _pageNum.asStateFlow() - - private val _hasNextPage = MutableStateFlow(true) - val hasNextPage = _hasNextPage.asStateFlow() - - private val _loading = MutableStateFlow(true) - val loading = _loading.asStateFlow() - - fun loadNextPage() { - launch { - if (hasNextPage.value && sourceMutex.tryLock()) { - _pageNum.value++ - val page = fetcher(_pageNum.value) - if (page != null) { - _sourceManga.value = _sourceManga.value + page.mangaList - _hasNextPage.value = page.hasNextPage - } else { - _pageNum.value-- - } - sourceMutex.unlock() + private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> + coroutineScope { + manga + updatedMangaIds.filter { it in mangaIds.value }.map { + async { + getManga.await(it) + } + }.awaitAll().filterNotNull().associateBy { it.id } } - _loading.value = false + }.stateIn(this, SharingStarted.Eagerly, emptyMap()) + + val mangas = combine(_sourceManga, changedManga) { sourceManga, changedManga -> + sourceManga.map { changedManga[it.id] ?: it } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + + private val _pageNum = MutableStateFlow(0) + val pageNum = _pageNum.asStateFlow() + + private val _hasNextPage = MutableStateFlow(true) + val hasNextPage = _hasNextPage.asStateFlow() + + private val _loading = MutableStateFlow(true) + val loading = _loading.asStateFlow() + + fun loadNextPage() { + launch { + if (hasNextPage.value && sourceMutex.tryLock()) { + _pageNum.value++ + val page = fetcher(_pageNum.value) + if (page != null) { + _sourceManga.value = _sourceManga.value + page.mangaList + _hasNextPage.value = page.hasNextPage + } else { + _pageNum.value-- + } + sourceMutex.unlock() + } + _loading.value = false + } + } + + companion object { + private val log = logging() } } - - companion object { - private val log = logging() - } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/service/UiPreferences.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/service/UiPreferences.kt index 7c5acb07..f213e73b 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/service/UiPreferences.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/ui/service/UiPreferences.kt @@ -13,7 +13,6 @@ import ca.gosyer.jui.domain.ui.model.ThemeMode import ca.gosyer.jui.domain.ui.model.WindowSettings class UiPreferences(private val preferenceStore: PreferenceStore) { - fun themeMode(): Preference { return preferenceStore.getJsonObject("theme_mode", ThemeMode.System, ThemeMode.serializer()) } diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt index 5172907a..f4ed0935 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/GetRecentUpdates.kt @@ -12,18 +12,22 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GetRecentUpdates @Inject constructor(private val updatesRepository: UpdatesRepository) { +class GetRecentUpdates + @Inject + constructor(private val updatesRepository: UpdatesRepository) { + suspend fun await( + pageNum: Int, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(pageNum) + .catch { + onError(it) + log.warn(it) { "Failed to get updates for page $pageNum" } + } + .singleOrNull() - suspend fun await(pageNum: Int, onError: suspend (Throwable) -> Unit = {}) = asFlow(pageNum) - .catch { - onError(it) - log.warn(it) { "Failed to get updates for page $pageNum" } + fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum) + + companion object { + private val log = logging() } - .singleOrNull() - - fun asFlow(pageNum: Int) = updatesRepository.getRecentUpdates(pageNum) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt index e9dd4c7a..c810c51f 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateCategory.kt @@ -13,27 +13,34 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateCategory @Inject constructor(private val updatesRepository: UpdatesRepository) { +class UpdateCategory + @Inject + constructor(private val updatesRepository: UpdatesRepository) { + suspend fun await( + categoryId: Long, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(categoryId) + .catch { + onError(it) + log.warn(it) { "Failed to update category $categoryId" } + } + .collect() - suspend fun await(categoryId: Long, onError: suspend (Throwable) -> Unit = {}) = asFlow(categoryId) - .catch { - onError(it) - log.warn(it) { "Failed to update category $categoryId" } + suspend fun await( + category: Category, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(category) + .catch { + onError(it) + log.warn(it) { "Failed to update category ${category.name}(${category.id})" } + } + .collect() + + fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId) + + fun asFlow(category: Category) = updatesRepository.updateCategory(category.id) + + companion object { + private val log = logging() } - .collect() - - suspend fun await(category: Category, onError: suspend (Throwable) -> Unit = {}) = asFlow(category) - .catch { - onError(it) - log.warn(it) { "Failed to update category ${category.name}(${category.id})" } - } - .collect() - - fun asFlow(categoryId: Long) = updatesRepository.updateCategory(categoryId) - - fun asFlow(category: Category) = updatesRepository.updateCategory(category.id) - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt index e4b7fef2..43e53cac 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateChecker.kt @@ -21,68 +21,74 @@ import kotlinx.coroutines.flow.singleOrNull import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateChecker @Inject constructor( - private val updatePreferences: UpdatePreferences, - private val client: Http, -) { - suspend fun await(manualFetch: Boolean, onError: suspend (Throwable) -> Unit = {}) = asFlow(manualFetch) - .catch { - onError(it) - log.warn(it) { "Failed to check for updates" } - } - .singleOrNull() +class UpdateChecker + @Inject + constructor( + private val updatePreferences: UpdatePreferences, + private val client: Http, + ) { + suspend fun await( + manualFetch: Boolean, + onError: suspend (Throwable) -> Unit = {}, + ) = asFlow(manualFetch) + .catch { + onError(it) + log.warn(it) { "Failed to check for updates" } + } + .singleOrNull() - fun asFlow(manualFetch: Boolean) = flow { - if (!manualFetch && !updatePreferences.enabled().get()) return@flow - val latestRelease = client.get( - "https://api.github.com/repos/$GITHUB_REPO/releases/latest", - ).body() + fun asFlow(manualFetch: Boolean) = + flow { + if (!manualFetch && !updatePreferences.enabled().get()) return@flow + val latestRelease = client.get( + "https://api.github.com/repos/$GITHUB_REPO/releases/latest", + ).body() - if (isNewVersion(latestRelease.version)) { - emit(Update.UpdateFound(latestRelease)) - } else { - emit(Update.NoUpdatesFound) - } - }.flowOn(Dispatchers.IO) + if (isNewVersion(latestRelease.version)) { + emit(Update.UpdateFound(latestRelease)) + } else { + emit(Update.NoUpdatesFound) + } + }.flowOn(Dispatchers.IO) - sealed class Update { - data class UpdateFound(val release: GithubRelease) : Update() - object NoUpdatesFound : Update() - } - - // Thanks to Tachiyomi for inspiration - private fun isNewVersion(versionTag: String): Boolean { - // Removes prefixes like "r" or "v" - val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") - - return if (BuildKonfig.IS_PREVIEW) { - // Preview builds: based on releases in "Suwayomi/Tachidesk-JUI-preview" repo - // tagged as something like "r123" - newVersion.toInt() > BuildKonfig.PREVIEW_BUILD - } else { - // Release builds: based on releases in "Suwayomi/Tachidesk-JUI" repo - // tagged as something like "v1.1.2" - newVersion != BuildKonfig.VERSION - } - } - - companion object { - private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) { - "Suwayomi/Tachidesk-JUI-preview" - } else { - "Suwayomi/Tachidesk-JUI" + sealed class Update { + data class UpdateFound(val release: GithubRelease) : Update() + object NoUpdatesFound : Update() } - private val RELEASE_TAG: String by lazy { - if (BuildKonfig.IS_PREVIEW) { - "r${BuildKonfig.PREVIEW_BUILD}" + // Thanks to Tachiyomi for inspiration + private fun isNewVersion(versionTag: String): Boolean { + // Removes prefixes like "r" or "v" + val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") + + return if (BuildKonfig.IS_PREVIEW) { + // Preview builds: based on releases in "Suwayomi/Tachidesk-JUI-preview" repo + // tagged as something like "r123" + newVersion.toInt() > BuildKonfig.PREVIEW_BUILD } else { - "v${BuildKonfig.VERSION}" + // Release builds: based on releases in "Suwayomi/Tachidesk-JUI" repo + // tagged as something like "v1.1.2" + newVersion != BuildKonfig.VERSION } } - val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG" + companion object { + private val GITHUB_REPO = if (BuildKonfig.IS_PREVIEW) { + "Suwayomi/Tachidesk-JUI-preview" + } else { + "Suwayomi/Tachidesk-JUI" + } - private val log = logging() + private val RELEASE_TAG: String by lazy { + if (BuildKonfig.IS_PREVIEW) { + "r${BuildKonfig.PREVIEW_BUILD}" + } else { + "v${BuildKonfig.VERSION}" + } + } + + val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG" + + private val log = logging() + } } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt index 74d7fddb..0f9e9112 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdateLibrary.kt @@ -12,18 +12,20 @@ import kotlinx.coroutines.flow.collect import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdateLibrary @Inject constructor(private val updatesRepository: UpdatesRepository) { +class UpdateLibrary + @Inject + constructor(private val updatesRepository: UpdatesRepository) { + suspend fun await(onError: suspend (Throwable) -> Unit = {}) = + asFlow() + .catch { + onError(it) + log.warn(it) { "Failed to update library" } + } + .collect() - suspend fun await(onError: suspend (Throwable) -> Unit = {}) = asFlow() - .catch { - onError(it) - log.warn(it) { "Failed to update library" } + fun asFlow() = updatesRepository.updateLibrary() + + companion object { + private val log = logging() } - .collect() - - fun asFlow() = updatesRepository.updateLibrary() - - companion object { - private val log = logging() } -} diff --git a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt index 55347cd7..f353e846 100644 --- a/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt +++ b/domain/src/commonMain/kotlin/ca/gosyer/jui/domain/updates/interactor/UpdatesPager.kt @@ -37,152 +37,157 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import me.tatarka.inject.annotations.Inject -class UpdatesPager @Inject constructor( - private val getRecentUpdates: GetRecentUpdates, - private val getManga: GetManga, - private val getChapter: GetChapter, - private val serverListeners: ServerListeners, -) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { - private val updatesMutex = Mutex() +class UpdatesPager + @Inject + constructor( + private val getRecentUpdates: GetRecentUpdates, + private val getManga: GetManga, + private val getChapter: GetChapter, + private val serverListeners: ServerListeners, + ) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { + private val updatesMutex = Mutex() - private val fetchedUpdates = MutableSharedFlow>() - private val foldedUpdates = fetchedUpdates.runningFold(emptyList()) { updates, newUpdates -> - updates.ifEmpty { - val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates - listOf( - Updates.Date( - Instant.fromEpochSeconds(first.fetchedAt) + private val fetchedUpdates = MutableSharedFlow>() + private val foldedUpdates = fetchedUpdates.runningFold(emptyList()) { updates, newUpdates -> + updates.ifEmpty { + val first = newUpdates.firstOrNull()?.chapter ?: return@runningFold updates + listOf( + Updates.Date( + Instant.fromEpochSeconds(first.fetchedAt) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date, + ), + ) + } + newUpdates.fold(emptyList()) { list, (manga, chapter) -> + val date = (list.lastOrNull() as? Updates.Update)?.let { + val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt) .toLocalDateTime(TimeZone.currentSystemDefault()) - .date, - ), - ) - } + newUpdates.fold(emptyList()) { list, (manga, chapter) -> - val date = (list.lastOrNull() as? Updates.Update)?.let { - val lastUpdateDate = Instant.fromEpochSeconds(it.chapter.fetchedAt) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt) - .toLocalDateTime(TimeZone.currentSystemDefault()) - .date - chapterDate.takeUnless { it == lastUpdateDate } - } - - if (date == null) { - list + Updates.Update(manga, chapter) - } else { - list + Updates.Date(date) + Updates.Update(manga, chapter) - } - } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - - private val mangaIds = foldedUpdates.map { updates -> - updates.filterIsInstance().map { it.manga.id } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val chapterIds = foldedUpdates.map { updates -> - updates.filterIsInstance().map { Triple(it.manga.id, it.chapter.index, it.chapter.id) } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - - private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> - coroutineScope { - manga + updatedMangaIds.filter { it in mangaIds.value }.map { - async { - getManga.await(it) + .date + val chapterDate = Instant.fromEpochSeconds(chapter.fetchedAt) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + chapterDate.takeUnless { it == lastUpdateDate } } - }.awaitAll().filterNotNull().associateBy { it.id } - } - }.stateIn(this, SharingStarted.Eagerly, emptyMap()) - private val changedChapters = MutableStateFlow(emptyMap()) + if (date == null) { + list + Updates.Update(manga, chapter) + } else { + list + Updates.Date(date) + Updates.Update(manga, chapter) + } + } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) - init { - serverListeners.chapterIndexesListener - .onEach { (mangaId, chapterIndexes) -> - if (chapterIndexes == null) { + private val mangaIds = foldedUpdates.map { updates -> + updates.filterIsInstance().map { it.manga.id } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + private val chapterIds = foldedUpdates.map { updates -> + updates.filterIsInstance().map { Triple(it.manga.id, it.chapter.index, it.chapter.id) } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + + private val changedManga = serverListeners.mangaListener.runningFold(emptyMap()) { manga, updatedMangaIds -> + coroutineScope { + manga + updatedMangaIds.filter { it in mangaIds.value }.map { + async { + getManga.await(it) + } + }.awaitAll().filterNotNull().associateBy { it.id } + } + }.stateIn(this, SharingStarted.Eagerly, emptyMap()) + + private val changedChapters = MutableStateFlow(emptyMap()) + + init { + serverListeners.chapterIndexesListener + .onEach { (mangaId, chapterIndexes) -> + if (chapterIndexes == null) { + val chapters = coroutineScope { + foldedUpdates.value.filterIsInstance().filter { it.manga.id == mangaId }.map { + async { + getChapter.await(it.manga.id, it.chapter.index) + } + }.awaitAll().filterNotNull().associateBy { it.id } + } + changedChapters.update { it + chapters } + } else { + val chapters = coroutineScope { + chapterIndexes.mapNotNull { index -> chapterIds.value.find { it.first == mangaId && it.second == index } } + .map { + async { + getChapter.await(it.first, it.second) + } + }.awaitAll().filterNotNull().associateBy { it.id } + } + changedChapters.update { it + chapters } + } + } + .launchIn(this) + serverListeners.chapterIdsListener + .onEach { (_, updatedChapterIds) -> val chapters = coroutineScope { - foldedUpdates.value.filterIsInstance().filter { it.manga.id == mangaId }.map { + updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it.third == id } }.map { async { - getChapter.await(it.manga.id, it.chapter.index) + getChapter.await(it.first, it.second) } }.awaitAll().filterNotNull().associateBy { it.id } } changedChapters.update { it + chapters } - } else { - val chapters = coroutineScope { - chapterIndexes.mapNotNull { index -> chapterIds.value.find { it.first == mangaId && it.second == index } } - .map { - async { - getChapter.await(it.first, it.second) - } - }.awaitAll().filterNotNull().associateBy { it.id } + } + .launchIn(this) + } + + val updates = combine( + foldedUpdates, + changedManga, + changedChapters, + ) { updates, changedManga, changedChapters -> + updates.map { + when (it) { + is Updates.Date -> it + is Updates.Update -> it.copy( + manga = changedManga[it.manga.id] ?: it.manga, + chapter = changedChapters[it.chapter.id] ?: it.chapter, + ) + } + } + }.stateIn(this, SharingStarted.Eagerly, emptyList()) + + private val currentPage = MutableStateFlow(0) + private val hasNextPage = MutableStateFlow(true) + + @Immutable + sealed class Updates { + @Immutable + data class Update(val manga: Manga, val chapter: Chapter) : Updates() + + @Immutable + data class Date(val date: String) : Updates() { + constructor(date: LocalDate) : this(date.toString()) + } + } + + fun loadNextPage( + onComplete: (() -> Unit)? = null, + onError: suspend (Throwable) -> Unit, + ) { + launch { + if (hasNextPage.value && updatesMutex.tryLock()) { + currentPage.value++ + if (!getUpdates(currentPage.value, onError)) { + currentPage.value-- } - changedChapters.update { it + chapters } + updatesMutex.unlock() } - } - .launchIn(this) - serverListeners.chapterIdsListener - .onEach { (_, updatedChapterIds) -> - val chapters = coroutineScope { - updatedChapterIds.mapNotNull { id -> chapterIds.value.find { it.third == id } }.map { - async { - getChapter.await(it.first, it.second) - } - }.awaitAll().filterNotNull().associateBy { it.id } - } - changedChapters.update { it + chapters } - } - .launchIn(this) - } - - val updates = combine( - foldedUpdates, - changedManga, - changedChapters, - ) { updates, changedManga, changedChapters -> - updates.map { - when (it) { - is Updates.Date -> it - is Updates.Update -> it.copy( - manga = changedManga[it.manga.id] ?: it.manga, - chapter = changedChapters[it.chapter.id] ?: it.chapter, - ) + onComplete?.invoke() } } - }.stateIn(this, SharingStarted.Eagerly, emptyList()) - private val currentPage = MutableStateFlow(0) - private val hasNextPage = MutableStateFlow(true) - - @Immutable - sealed class Updates { - @Immutable - data class Update(val manga: Manga, val chapter: Chapter) : Updates() - - @Immutable - data class Date(val date: String) : Updates() { - constructor(date: LocalDate) : this(date.toString()) + private suspend fun getUpdates( + page: Int, + onError: suspend (Throwable) -> Unit, + ): Boolean { + val updates = getRecentUpdates.await(page, onError) ?: return false + hasNextPage.value = updates.hasNextPage + fetchedUpdates.emit(updates.page) + return true } } - - fun loadNextPage( - onComplete: (() -> Unit)? = null, - onError: suspend (Throwable) -> Unit, - ) { - launch { - if (hasNextPage.value && updatesMutex.tryLock()) { - currentPage.value++ - if (!getUpdates(currentPage.value, onError)) { - currentPage.value-- - } - updatesMutex.unlock() - } - onComplete?.invoke() - } - } - - private suspend fun getUpdates(page: Int, onError: suspend (Throwable) -> Unit): Boolean { - val updates = getRecentUpdates.await(page, onError) ?: return false - hasNextPage.value = updates.hasNextPage - fetchedUpdates.emit(updates.page) - return true - } -} diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt index f056358f..2be72ffa 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt @@ -12,7 +12,6 @@ import ca.gosyer.jui.domain.server.service.ServerService import me.tatarka.inject.annotations.Provides actual interface DomainComponent : SharedDomainComponent { - // Singletons val serverService: ServerService diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt index abb9cad7..08674a4f 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerHostPreferences.kt @@ -11,7 +11,6 @@ import ca.gosyer.jui.core.prefs.PreferenceStore import ca.gosyer.jui.domain.server.service.host.ServerHostPreference class ServerHostPreferences(private val preferenceStore: PreferenceStore) { - fun host(): Preference { return preferenceStore.getBoolean("host", true) } diff --git a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt index 26d54f35..df340039 100644 --- a/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt +++ b/domain/src/desktopMain/kotlin/ca/gosyer/jui/domain/server/service/ServerService.kt @@ -39,167 +39,169 @@ import kotlin.io.path.exists import kotlin.io.path.isExecutable @OptIn(DelicateCoroutinesApi::class) -class ServerService @Inject constructor( - private val serverHostPreferences: ServerHostPreferences, -) { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) +class ServerService + @Inject + constructor( + private val serverHostPreferences: ServerHostPreferences, + ) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private val host = serverHostPreferences.host().stateIn(GlobalScope) - private val _initialized = MutableStateFlow( - if (host.value) { - ServerResult.STARTING - } else { - ServerResult.UNUSED - }, - ) - val initialized = _initialized.asStateFlow() - private var process: Process? = null - - fun startAnyway() { - _initialized.value = ServerResult.UNUSED - } - - @Throws(IOException::class) - private suspend fun copyJar(jarFile: Path) { - javaClass.getResourceAsStream("/Tachidesk.jar")?.source() - ?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer()) - } - - private fun getJavaFromPath(javaPath: Path): String? { - val javaExeFile = javaPath.resolve("java.exe").toNioPath() - val javaUnixFile = javaPath.resolve("java").toNioPath() - return when { - javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString() - javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString() - else -> null - } - } - - private fun getRuntimeJava(): String? { - return System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) } - } - - private fun getPossibleJava(): String? { - return System.getProperty("java.library.path")?.split(pathSeparatorChar) - .orEmpty() - .asSequence() - .mapNotNull { - val file = it.toPath() - if (file.toString().contains("java") || file.toString().contains("jdk")) { - if (file.name.equals("bin", true)) { - file - } else { - file.resolve("bin") - } - } else { - null - } - } - .mapNotNull { getJavaFromPath(it) } - .firstOrNull() - } - - private suspend fun runService() { - process?.destroy() - process?.waitFor() - _initialized.value = if (host.value) { - ServerResult.STARTING - } else { - ServerResult.UNUSED - return - } - - val jarFile = userDataDir / "Tachidesk.jar" - if (!FileSystem.SYSTEM.exists(jarFile)) { - log.info { "Copying server to resources" } - withIOContext { copyJar(jarFile) } - } else { - try { - val jarVersion = withIOContext { - JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar -> - jar.manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)?.toIntOrNull() - } - } - - if (jarVersion != BuildKonfig.SERVER_CODE) { - log.info { "Updating server file from resources" } - withIOContext { copyJar(jarFile) } - } - } catch (e: IOException) { - log.error(e) { - "Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly" - } - } - } - - val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java" - log.info { "Starting server with $javaPath" } - val properties = serverHostPreferences.properties() - log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") } - - withIOContext { - val reader: Reader - process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString()) - .redirectErrorStream(true) - .start() - .also { - reader = it.inputStream.reader() - } - log.info { "Server started successfully" } - val log = logging("Server") - reader.forEachLine { - if (_initialized.value == ServerResult.STARTING) { - when { - it.contains("Javalin started") -> - _initialized.value = ServerResult.STARTED - it.contains("Javalin has stopped") -> - _initialized.value = ServerResult.FAILED - } - } - log.info { it } - } - if (_initialized.value == ServerResult.STARTING) { - _initialized.value = ServerResult.FAILED - } - log.info { "Server closed" } - val exitVal = process?.waitFor() - log.info { "Process exitValue: $exitVal" } - process = null - } - } - - fun startServer() { - scope.coroutineContext.cancelChildren() - host - .mapLatest { - runService() - } - .catch { - log.error(it) { "Error launching Tachidesk.jar" } - if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) { - _initialized.value = ServerResult.FAILED - } - } - .launchIn(scope) - } - - init { - Runtime.getRuntime().addShutdownHook( - thread(start = false) { - process?.destroy() - process = null + private val host = serverHostPreferences.host().stateIn(GlobalScope) + private val _initialized = MutableStateFlow( + if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED }, ) - } + val initialized = _initialized.asStateFlow() + private var process: Process? = null - enum class ServerResult { - UNUSED, - STARTING, - STARTED, - FAILED, - } + fun startAnyway() { + _initialized.value = ServerResult.UNUSED + } - private companion object { - private val log = logging() + @Throws(IOException::class) + private suspend fun copyJar(jarFile: Path) { + javaClass.getResourceAsStream("/Tachidesk.jar")?.source() + ?.copyTo(FileSystem.SYSTEM.sink(jarFile).buffer()) + } + + private fun getJavaFromPath(javaPath: Path): String? { + val javaExeFile = javaPath.resolve("java.exe").toNioPath() + val javaUnixFile = javaPath.resolve("java").toNioPath() + return when { + javaExeFile.exists() && javaExeFile.isExecutable() -> javaExeFile.absolutePathString() + javaUnixFile.exists() && javaUnixFile.isExecutable() -> javaUnixFile.absolutePathString() + else -> null + } + } + + private fun getRuntimeJava(): String? { + return System.getProperty("java.home")?.let { getJavaFromPath(it.toPath().resolve("bin")) } + } + + private fun getPossibleJava(): String? { + return System.getProperty("java.library.path")?.split(pathSeparatorChar) + .orEmpty() + .asSequence() + .mapNotNull { + val file = it.toPath() + if (file.toString().contains("java") || file.toString().contains("jdk")) { + if (file.name.equals("bin", true)) { + file + } else { + file.resolve("bin") + } + } else { + null + } + } + .mapNotNull { getJavaFromPath(it) } + .firstOrNull() + } + + private suspend fun runService() { + process?.destroy() + process?.waitFor() + _initialized.value = if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED + return + } + + val jarFile = userDataDir / "Tachidesk.jar" + if (!FileSystem.SYSTEM.exists(jarFile)) { + log.info { "Copying server to resources" } + withIOContext { copyJar(jarFile) } + } else { + try { + val jarVersion = withIOContext { + JarInputStream(FileSystem.SYSTEM.source(jarFile).buffer().inputStream()).use { jar -> + jar.manifest?.mainAttributes?.getValue(Attributes.Name.IMPLEMENTATION_VERSION)?.toIntOrNull() + } + } + + if (jarVersion != BuildKonfig.SERVER_CODE) { + log.info { "Updating server file from resources" } + withIOContext { copyJar(jarFile) } + } + } catch (e: IOException) { + log.error(e) { + "Error accessing server jar, cannot update server, ${BuildKonfig.NAME} may not work properly" + } + } + } + + val javaPath = getRuntimeJava() ?: getPossibleJava() ?: "java" + log.info { "Starting server with $javaPath" } + val properties = serverHostPreferences.properties() + log.info { "Using server properties:\n" + properties.joinToString(separator = "\n") } + + withIOContext { + val reader: Reader + process = ProcessBuilder(javaPath, *properties, "-jar", jarFile.toString()) + .redirectErrorStream(true) + .start() + .also { + reader = it.inputStream.reader() + } + log.info { "Server started successfully" } + val log = logging("Server") + reader.forEachLine { + if (_initialized.value == ServerResult.STARTING) { + when { + it.contains("Javalin started") -> + _initialized.value = ServerResult.STARTED + it.contains("Javalin has stopped") -> + _initialized.value = ServerResult.FAILED + } + } + log.info { it } + } + if (_initialized.value == ServerResult.STARTING) { + _initialized.value = ServerResult.FAILED + } + log.info { "Server closed" } + val exitVal = process?.waitFor() + log.info { "Process exitValue: $exitVal" } + process = null + } + } + + fun startServer() { + scope.coroutineContext.cancelChildren() + host + .mapLatest { + runService() + } + .catch { + log.error(it) { "Error launching Tachidesk.jar" } + if (_initialized.value == ServerResult.STARTING || _initialized.value == ServerResult.STARTED) { + _initialized.value = ServerResult.FAILED + } + } + .launchIn(scope) + } + + init { + Runtime.getRuntime().addShutdownHook( + thread(start = false) { + process?.destroy() + process = null + }, + ) + } + + enum class ServerResult { + UNUSED, + STARTING, + STARTED, + FAILED, + } + + private companion object { + private val log = logging() + } } -} diff --git a/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt b/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt index 15875a16..c0b847a0 100644 --- a/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt +++ b/domain/src/iosMain/kotlin/ca/gosyer/jui/domain/DomainComponent.kt @@ -7,6 +7,5 @@ package ca.gosyer.jui.domain actual interface DomainComponent : SharedDomainComponent { - companion object } diff --git a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppComponent.kt b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppComponent.kt index c313fc72..e667bc55 100644 --- a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppComponent.kt +++ b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppComponent.kt @@ -21,7 +21,6 @@ abstract class AppComponent( @get:Provides val context: ContextWrapper, ) : ViewModelComponent, DataComponent, DomainComponent, UiComponent { - abstract val appMigrations: AppMigrations @get:AppScope @@ -35,7 +34,8 @@ abstract class AppComponent( companion object { private var appComponentInstance: AppComponent? = null - fun getInstance(context: ContextWrapper) = appComponentInstance ?: create(context) - .also { appComponentInstance = it } + fun getInstance(context: ContextWrapper) = + appComponentInstance ?: create(context) + .also { appComponentInstance = it } } } diff --git a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppMigrations.kt b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppMigrations.kt index 3972dc20..bfa66f83 100644 --- a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppMigrations.kt +++ b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/AppMigrations.kt @@ -11,22 +11,23 @@ import ca.gosyer.jui.ios.build.BuildKonfig import ca.gosyer.jui.uicore.vm.ContextWrapper import me.tatarka.inject.annotations.Inject -class AppMigrations @Inject constructor( - private val migrationPreferences: MigrationPreferences, - private val contextWrapper: ContextWrapper, -) { +class AppMigrations + @Inject + constructor( + private val migrationPreferences: MigrationPreferences, + private val contextWrapper: ContextWrapper, + ) { + fun runMigrations(): Boolean { + val oldVersion = migrationPreferences.appVersion().get() + if (oldVersion < BuildKonfig.MIGRATION_CODE) { + migrationPreferences.appVersion().set(BuildKonfig.MIGRATION_CODE) - fun runMigrations(): Boolean { - val oldVersion = migrationPreferences.appVersion().get() - if (oldVersion < BuildKonfig.MIGRATION_CODE) { - migrationPreferences.appVersion().set(BuildKonfig.MIGRATION_CODE) - - // Fresh install - if (oldVersion == 0) { - return false + // Fresh install + if (oldVersion == 0) { + return false + } + return true } - return true + return false } - return false } -} diff --git a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/Main.kt b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/Main.kt index 7a8547d4..c5339fa0 100644 --- a/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/Main.kt +++ b/ios/src/uikitMain/kotlin/ca/gosyer/jui/ios/Main.kt @@ -61,58 +61,66 @@ fun main() { } } -class SkikoAppDelegate @OverrideInit constructor() : UIResponder(), UIApplicationDelegateProtocol { - companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta +class SkikoAppDelegate + @OverrideInit + constructor() : UIResponder(), UIApplicationDelegateProtocol { + companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta - private var _window: UIWindow? = null - override fun window() = _window - override fun setWindow(window: UIWindow?) { - _window = window - } + private var _window: UIWindow? = null + override fun window() = _window + override fun setWindow(window: UIWindow?) { + _window = window + } - private val context = ContextWrapper() + private val context = ContextWrapper() - private val appComponent = AppComponent.getInstance(context) + private val appComponent = AppComponent.getInstance(context) - init { - appComponent.migrations.runMigrations() - appComponent.appMigrations.runMigrations() + init { + appComponent.migrations.runMigrations() + appComponent.appMigrations.runMigrations() - appComponent.downloadService.init() - appComponent.libraryUpdateService.init() - } + appComponent.downloadService.init() + appComponent.libraryUpdateService.init() + } - val uiHooks = appComponent.hooks + val uiHooks = appComponent.hooks - override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map?): Boolean { - window = UIWindow(frame = UIScreen.mainScreen.bounds).apply { - val insets = safeAreaInsets.useContents { - WindowInsets(left.dp, top.dp, right.dp, bottom.dp) - } + override fun application( + application: UIApplication, + didFinishLaunchingWithOptions: Map?, + ): Boolean { + window = UIWindow(frame = UIScreen.mainScreen.bounds).apply { + val insets = safeAreaInsets.useContents { + WindowInsets(left.dp, top.dp, right.dp, bottom.dp) + } - rootViewController = Application("Tachidesk-JUI") { - CompositionLocalProvider(*uiHooks) { - AppTheme { - Box(Modifier.fillMaxSize().windowInsetsPadding(insets)) { - MainMenu() - ToastOverlay( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = 64.dp), - context = context, - ) + rootViewController = Application("Tachidesk-JUI") { + CompositionLocalProvider(*uiHooks) { + AppTheme { + Box(Modifier.fillMaxSize().windowInsetsPadding(insets)) { + MainMenu() + ToastOverlay( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 64.dp), + context = context, + ) + } } } } + makeKeyAndVisible() } - makeKeyAndVisible() + return true } - return true } -} @Composable -fun ToastOverlay(modifier: Modifier, context: ContextWrapper) { +fun ToastOverlay( + modifier: Modifier, + context: ContextWrapper, +) { var toast by remember { mutableStateOf?>(null) } LaunchedEffect(Unit) { context.toasts diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt index 48ea1190..81e1cc60 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/image/AndroidImageLoaderBuilder.kt @@ -27,11 +27,17 @@ actual fun OptionsBuilder.configure(contextWrapper: ContextWrapper) { androidContext(contextWrapper) } -actual fun ComponentRegistryBuilder.register(contextWrapper: ContextWrapper, http: Http) { +actual fun ComponentRegistryBuilder.register( + contextWrapper: ContextWrapper, + http: Http, +) { setupDefaultComponents(contextWrapper, httpClient = { http }) } -actual fun DiskCacheBuilder.configure(contextWrapper: ContextWrapper, cacheDir: String) { +actual fun DiskCacheBuilder.configure( + contextWrapper: ContextWrapper, + cacheDir: String, +) { directory(contextWrapper.cacheDir.toOkioPath() / cacheDir) maxSizeBytes(1024 * 1024 * 150) // 150 MB } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt index 1d87a0dc..b4797335 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt @@ -13,7 +13,11 @@ import androidx.compose.ui.graphics.vector.ImageVector // todo @Composable -actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { +actual fun ActionIcon( + onClick: () -> Unit, + contentDescription: String, + icon: ImageVector, +) { IconButton(onClick = onClick) { Icon(icon, contentDescription) } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/AndroidBackHandler.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/AndroidBackHandler.kt index 786abe51..7a313bb5 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/AndroidBackHandler.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/navigation/AndroidBackHandler.kt @@ -10,6 +10,9 @@ import androidx.compose.runtime.Composable import androidx.activity.compose.BackHandler as AndroidBackHandler @Composable -internal actual fun RealBackHandler(enabled: Boolean, onBack: () -> Unit) { +internal actual fun RealBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) { AndroidBackHandler(enabled, onBack) } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleSupport.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleSupport.kt index fdc249ba..2b2c2365 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleSupport.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleSupport.kt @@ -115,7 +115,10 @@ internal class SavedStateHandlesProvider( } } -inline fun CreationExtras.addScreenModelKey(screen: Screen, tag: String?): CreationExtras { +inline fun CreationExtras.addScreenModelKey( + screen: Screen, + tag: String?, +): CreationExtras { return MutableCreationExtras(this).apply { set( VIEW_MODEL_KEY, diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt index c0a3cebc..d9564fcc 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt @@ -12,7 +12,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator actual class CategoriesLauncher(private val navigator: Navigator?) { - actual fun open() { navigator?.push(CategoriesScreen()) } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/library/components/AndroidLibraryGrid.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/library/components/AndroidLibraryGrid.kt index 8e704065..b85753fc 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/library/components/AndroidLibraryGrid.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/library/components/AndroidLibraryGrid.kt @@ -22,30 +22,31 @@ import ca.gosyer.jui.uicore.resources.stringResource actual fun Modifier.libraryMangaModifier( onClickManga: () -> Unit, onClickRemoveManga: () -> Unit, -): Modifier = composed { - var expanded by remember { mutableStateOf(false) } - DropdownMenu( - expanded, - onDismissRequest = { expanded = false }, - ) { - listOf( - stringResource(MR.strings.action_remove_favorite) to onClickRemoveManga, - ).forEach { (label, onClick) -> - DropdownMenuItem( - onClick = { - expanded = false - onClick() - }, - ) { - Text(text = label) +): Modifier = + composed { + var expanded by remember { mutableStateOf(false) } + DropdownMenu( + expanded, + onDismissRequest = { expanded = false }, + ) { + listOf( + stringResource(MR.strings.action_remove_favorite) to onClickRemoveManga, + ).forEach { (label, onClick) -> + DropdownMenuItem( + onClick = { + expanded = false + onClick() + }, + ) { + Text(text = label) + } } } - } - Modifier.combinedClickable( - onClick = { onClickManga() }, - onLongClick = { - expanded = true - }, - ) -} + Modifier.combinedClickable( + onClick = { onClickManga() }, + onLongClick = { + expanded = true + }, + ) + } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt index e0cfdc53..0602af88 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt @@ -20,5 +20,5 @@ actual fun getDebugInfo(): String { Device name: ${Build.DEVICE} Device model: ${Build.MODEL} Device product name: ${Build.PRODUCT} - """.trimIndent() + """.trimIndent() } diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/manga/components/AndroidChapterItem.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/manga/components/AndroidChapterItem.kt index d45c8c95..98a4b13a 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/manga/components/AndroidChapterItem.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/manga/components/AndroidChapterItem.kt @@ -18,7 +18,8 @@ actual fun Modifier.chapterItemModifier( markPreviousAsRead: () -> Unit, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = combinedClickable( - onClick = onUnselectChapter ?: onClick, - onLongClick = onSelectChapter, -) +): Modifier = + combinedClickable( + onClick = onUnselectChapter ?: onClick, + onLongClick = onSelectChapter, + ) diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt index 47bfbdb4..9f6e7c9b 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/settings/AndroidSettingsServerScreen.kt @@ -17,4 +17,6 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie return {} } -actual class SettingsServerHostViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) +actual class SettingsServerHostViewModel + @Inject + constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/sources/components/AndroidSourcesMenu.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/sources/components/AndroidSourcesMenu.kt index ec473745..3a99e1e1 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/sources/components/AndroidSourcesMenu.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/sources/components/AndroidSourcesMenu.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier actual fun Modifier.sourceSideMenuItem( onSourceTabClick: () -> Unit, onSourceCloseTabClick: () -> Unit, -): Modifier = clickable( - onClick = onSourceTabClick, -) +): Modifier = + clickable( + onClick = onSourceTabClick, + ) diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/updates/components/AndroidUpdatesItem.kt b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/updates/components/AndroidUpdatesItem.kt index f5f23e0d..557540ea 100644 --- a/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/updates/components/AndroidUpdatesItem.kt +++ b/presentation/src/androidMain/kotlin/ca/gosyer/jui/ui/updates/components/AndroidUpdatesItem.kt @@ -17,7 +17,8 @@ actual fun Modifier.updatesItemModifier( unBookmarkChapter: (() -> Unit)?, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = combinedClickable( - onClick = onUnselectChapter ?: onClick, - onLongClick = onSelectChapter, -) +): Modifier = + combinedClickable( + onClick = onUnselectChapter ?: onClick, + onLongClick = onSelectChapter, + ) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt index e9027bfe..51662512 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/UiComponent.kt @@ -39,26 +39,29 @@ interface UiComponent { @Provides fun imageLoaderFactory( imageLoaderProvider: ImageLoaderProvider, - imageCache: ImageCache + imageCache: ImageCache, ): ImageLoader = imageLoaderProvider.get(imageCache) @AppScope @Provides - fun imageCacheFactory(): ImageCache = DiskCache(FileSystem.SYSTEM) { - configure(contextWrapper, "image_cache") - } + fun imageCacheFactory(): ImageCache = + DiskCache(FileSystem.SYSTEM) { + configure(contextWrapper, "image_cache") + } @AppScope @Provides - fun chapterCacheFactory(): ChapterCache = DiskCache(FileSystem.SYSTEM) { - configure(contextWrapper, "chapter_cache") - } + fun chapterCacheFactory(): ChapterCache = + DiskCache(FileSystem.SYSTEM) { + configure(contextWrapper, "chapter_cache") + } @Provides - fun getHooks(viewModelComponent: ViewModelComponent) = arrayOf( - LocalViewModels provides viewModelComponent, - LocalImageLoader provides imageLoader, - ) + fun getHooks(viewModelComponent: ViewModelComponent) = + arrayOf( + LocalViewModels provides viewModelComponent, + LocalImageLoader provides imageLoader, + ) } val LocalViewModels = diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt index 888210d3..80a86cc3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/chapter/ChapterDownloadButtons.kt @@ -154,7 +154,10 @@ private fun DownloadIconButton(onClick: () -> Unit) { } @Composable -private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: () -> Unit) { +private fun DownloadingIconButton( + downloadChapter: DownloadChapter?, + onClick: () -> Unit, +) { DropdownIconButton( downloadChapter?.mangaId to downloadChapter?.chapterIndex, { @@ -226,7 +229,10 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: () } @Composable -private fun DownloadedIconButton(chapter: Pair, onClick: () -> Unit) { +private fun DownloadedIconButton( + chapter: Pair, + onClick: () -> Unit, +) { DropdownIconButton( chapter, { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/components/LocaleToString.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/components/LocaleToString.kt index 35b702ff..9876f333 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/components/LocaleToString.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/components/LocaleToString.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toUpperCase import ca.gosyer.jui.core.lang.getDisplayLanguage -fun localeToString(locale: String) = Locale(locale) - .getDisplayLanguage(Locale.current) - .ifBlank { locale.toUpperCase(Locale.current) } +fun localeToString(locale: String) = + Locale(locale) + .getDisplayLanguage(Locale.current) + .ifBlank { locale.toUpperCase(Locale.current) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileSaver.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileSaver.kt index 736ab0b6..baaaa1ee 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileSaver.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/file/FileSaver.kt @@ -18,11 +18,12 @@ fun rememberFileSaver( onFileSelected: (Sink) -> Unit, onCancel: () -> Unit = {}, onError: () -> Unit = {}, -): FileSaver = realRememberFileSaver( - onFileSelected = onFileSelected, - onCancel = onCancel, - onError = onError, -) +): FileSaver = + realRememberFileSaver( + onFileSelected = onFileSelected, + onCancel = onCancel, + onError = onError, + ) @Composable internal expect fun realRememberFileSaver( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt index 88a25948..36d47fe1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/image/ImageLoaderProvider.kt @@ -27,86 +27,115 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import me.tatarka.inject.annotations.Inject -class ImageLoaderProvider @Inject constructor( - private val http: Http, - serverPreferences: ServerPreferences, - private val context: ContextWrapper, -) { - @OptIn(DelicateCoroutinesApi::class) - val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) +class ImageLoaderProvider + @Inject + constructor( + private val http: Http, + serverPreferences: ServerPreferences, + private val context: ContextWrapper, + ) { + @OptIn(DelicateCoroutinesApi::class) + val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope) - fun get(imageCache: ImageCache): ImageLoader { - return ImageLoader { - components { - register(context, http) - add(MokoResourceFetcher.Factory()) - add(MangaCoverMapper()) - add(MangaCoverKeyer()) - add(ExtensionIconMapper()) - add(ExtensionIconKeyer()) - add(SourceIconMapper()) - add(SourceIconKeyer()) + fun get(imageCache: ImageCache): ImageLoader { + return ImageLoader { + components { + register(context, http) + add(MokoResourceFetcher.Factory()) + add(MangaCoverMapper()) + add(MangaCoverKeyer()) + add(ExtensionIconMapper()) + add(ExtensionIconKeyer()) + add(SourceIconMapper()) + add(SourceIconKeyer()) + } + options { + configure(context) + } + interceptor { + diskCache { imageCache } + memoryCacheConfig { configure(context) } + } } - options { - configure(context) + } + + inner class MangaCoverMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Manga) return null + if (data.thumbnailUrl.isNullOrBlank()) return null + return Url(serverUrl.value.toString() + data.thumbnailUrl) } - interceptor { - diskCache { imageCache } - memoryCacheConfig { configure(context) } + } + + class MangaCoverKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Manga) return null + return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" + } + } + + inner class ExtensionIconMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Extension) return null + if (data.iconUrl.isBlank()) return null + return Url("${serverUrl.value}${data.iconUrl}") + } + } + + class ExtensionIconKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Extension) return null + return data.iconUrl + } + } + + inner class SourceIconMapper : Mapper { + override fun map( + data: Any, + options: Options, + ): Url? { + if (data !is Source) return null + if (data.iconUrl.isBlank()) return null + return Url(serverUrl.value.toString() + data.iconUrl) + } + } + + class SourceIconKeyer : Keyer { + override fun key( + data: Any, + options: Options, + type: Keyer.Type, + ): String? { + if (data !is Source) return null + return data.iconUrl } } } - inner class MangaCoverMapper : Mapper { - override fun map(data: Any, options: Options): Url? { - if (data !is Manga) return null - if (data.thumbnailUrl.isNullOrBlank()) return null - return Url(serverUrl.value.toString() + data.thumbnailUrl) - } - } - - class MangaCoverKeyer : Keyer { - override fun key(data: Any, options: Options, type: Keyer.Type): String? { - if (data !is Manga) return null - return "${data.sourceId}-${data.thumbnailUrl}-${data.thumbnailUrlLastFetched}" - } - } - - inner class ExtensionIconMapper : Mapper { - override fun map(data: Any, options: Options): Url? { - if (data !is Extension) return null - if (data.iconUrl.isBlank()) return null - return Url("${serverUrl.value}${data.iconUrl}") - } - } - - class ExtensionIconKeyer : Keyer { - override fun key(data: Any, options: Options, type: Keyer.Type): String? { - if (data !is Extension) return null - return data.iconUrl - } - } - - inner class SourceIconMapper : Mapper { - override fun map(data: Any, options: Options): Url? { - if (data !is Source) return null - if (data.iconUrl.isBlank()) return null - return Url(serverUrl.value.toString() + data.iconUrl) - } - } - - class SourceIconKeyer : Keyer { - override fun key(data: Any, options: Options, type: Keyer.Type): String? { - if (data !is Source) return null - return data.iconUrl - } - } -} - expect fun OptionsBuilder.configure(contextWrapper: ContextWrapper) -expect fun ComponentRegistryBuilder.register(contextWrapper: ContextWrapper, http: Http) +expect fun ComponentRegistryBuilder.register( + contextWrapper: ContextWrapper, + http: Http, +) -expect fun DiskCacheBuilder.configure(contextWrapper: ContextWrapper, cacheDir: String) +expect fun DiskCacheBuilder.configure( + contextWrapper: ContextWrapper, + cacheDir: String, +) expect fun MemoryCacheBuilder.configure(contextWrapper: ContextWrapper) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt index 4a5fa580..9b27e60f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt @@ -10,4 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector @Composable -expect fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) +expect fun ActionIcon( + onClick: () -> Unit, + contentDescription: String, + icon: ImageVector, +) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionMenu.kt index 6fce7d3f..41e1b1b4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionMenu.kt @@ -65,7 +65,10 @@ data class ActionItem( // Whether action items are allowed to overflow into a dropdown menu - or NOT SHOWN to hide enum class OverflowMode { - NEVER_OVERFLOW, IF_NECESSARY, ALWAYS_OVERFLOW, NOT_SHOWN + NEVER_OVERFLOW, + IF_NECESSARY, + ALWAYS_OVERFLOW, + NOT_SHOWN, } // Note: should be used in a RowScope diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/BackHandler.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/BackHandler.kt index 4cc44bc2..333fd3cb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/BackHandler.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/BackHandler.kt @@ -9,7 +9,13 @@ package ca.gosyer.jui.ui.base.navigation import androidx.compose.runtime.Composable @Composable -fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) = RealBackHandler(enabled, onBack) +fun BackHandler( + enabled: Boolean = true, + onBack: () -> Unit, +) = RealBackHandler(enabled, onBack) @Composable -internal expect fun RealBackHandler(enabled: Boolean, onBack: () -> Unit) +internal expect fun RealBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/DisplayController.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/DisplayController.kt index 89b4e24b..944075a2 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/DisplayController.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/navigation/DisplayController.kt @@ -30,7 +30,10 @@ class DisplayController( } @Composable -fun withDisplayController(controller: DisplayController, content: @Composable () -> Unit) { +fun withDisplayController( + controller: DisplayController, + content: @Composable () -> Unit, +) { CompositionLocalProvider( LocalDisplayController provides controller, content = content, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/ColorPickerDialog.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/ColorPickerDialog.kt index bd014bc7..1a43080b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/ColorPickerDialog.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/prefs/ColorPickerDialog.kt @@ -215,7 +215,10 @@ private fun getColorShades(color: Color): ImmutableList { ).toImmutableList() } -private fun shadeColor(f: Long, percent: Double): Color { +private fun shadeColor( + f: Long, + percent: Double, +): Color { val t = if (percent < 0) 0.0 else 255.0 val p = if (percent < 0) percent * -1 else percent val r = f shr 16 @@ -261,7 +264,10 @@ fun ColorPalette( val cursorStroke = Stroke(4f) val borderStroke = Stroke(1f) - fun setSelectedColor(color: Color, invalidate: Boolean = false) { + fun setSelectedColor( + color: Color, + invalidate: Boolean = false, + ) { selectedColor = color textFieldHex = color.toHexString() if (invalidate) { @@ -393,29 +399,47 @@ private suspend fun PointerInputScope.detectMove(onMove: (Offset) -> Unit) { // Coordinates <-> Color -private fun matrixCoordinatesToColor(hue: Float, position: Offset, size: IntSize): Color { +private fun matrixCoordinatesToColor( + hue: Float, + position: Offset, + size: IntSize, +): Color { val saturation = 1f / size.width * position.x val value = 1f - (1f / size.height * position.y) return hsvToColor(hue, saturation, value) } -private fun hueCoordinatesToHue(y: Float, size: IntSize): Float { +private fun hueCoordinatesToHue( + y: Float, + size: IntSize, +): Float { val hue = 360f - y * 360f / size.height return hsvToColor(hue, 1f, 1f).toHsv()[0] } -private fun satValToCoordinates(saturation: Float, value: Float, size: IntSize): Offset { +private fun satValToCoordinates( + saturation: Float, + value: Float, + size: IntSize, +): Offset { return Offset(saturation * size.width, ((1f - value) * size.height)) } -private fun hueToCoordinate(hue: Float, size: IntSize): Float { +private fun hueToCoordinate( + hue: Float, + size: IntSize, +): Float { return size.height - (hue * size.height / 360f) } // Color space conversions @OptIn(ExperimentalGraphicsApi::class) -fun hsvToColor(hue: Float, saturation: Float, value: Float): Color { +fun hsvToColor( + hue: Float, + saturation: Float, + value: Float, +): Color { return Color.hsv(hue, saturation.coerceIn(0F, 1F), value.coerceIn(0F, 1F)) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandle.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandle.kt index 58860afc..26fcc1fd 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandle.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandle.kt @@ -11,9 +11,15 @@ import kotlinx.coroutines.flow.StateFlow expect class SavedStateHandle { operator fun get(key: String): T? - operator fun set(key: String, value: T?) + operator fun set( + key: String, + value: T?, + ) fun remove(key: String): T? - fun getStateFlow(key: String, initialValue: T): StateFlow + fun getStateFlow( + key: String, + initialValue: T, + ): StateFlow } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleFlow.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleFlow.kt index 7dc9cddb..5fc57f3e 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleFlow.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/state/SavedStateHandleFlow.kt @@ -14,9 +14,7 @@ import kotlinx.coroutines.internal.synchronized import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -fun SavedStateHandle.getStateFlow( - initialValue: () -> T, -): SavedStateHandleDelegate { +fun SavedStateHandle.getStateFlow(initialValue: () -> T): SavedStateHandleDelegate { return SavedStateHandleDelegate(this, initialValue) } @@ -29,7 +27,10 @@ class SavedStateHandleDelegate( private var item: SavedStateHandleStateFlow? = null - override fun getValue(thisRef: ViewModel, property: KProperty<*>): SavedStateHandleStateFlow { + override fun getValue( + thisRef: ViewModel, + property: KProperty<*>, + ): SavedStateHandleStateFlow { return item ?: synchronized(synchronizedObject) { if (item == null) { savedStateHandle.getSavedStateFlow(property.name, initialValue) @@ -46,7 +47,6 @@ class SavedStateHandleStateFlow( private val savedStateHandle: SavedStateHandle, private val stateFlow: StateFlow, ) : StateFlow by stateFlow { - override var value: T get() = stateFlow.value diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt index 5352e926..ce9bbc43 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/base/theme/AppTheme.kt @@ -44,7 +44,7 @@ fun AppTheme(content: @Composable () -> Unit) { val viewModels = LocalViewModels.current val vm = remember { viewModels.appThemeViewModel() } val (colors, extraColors) = vm.getColors() - /*val systemUiController = rememberSystemUiController()*/ + // val systemUiController = rememberSystemUiController() DisposableEffect(vm) { onDispose(vm::onDispose) } @@ -59,94 +59,99 @@ fun AppTheme(content: @Composable () -> Unit) { } } -class AppThemeViewModel @Inject constructor( - private val uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - override val scope = MainScope() +class AppThemeViewModel + @Inject + constructor( + private val uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + override val scope = MainScope() - private val themeMode = uiPreferences.themeMode().asStateFlow() - private val lightTheme = uiPreferences.lightTheme().asStateFlow() - private val darkTheme = uiPreferences.darkTheme().asStateFlow() + private val themeMode = uiPreferences.themeMode().asStateFlow() + private val lightTheme = uiPreferences.lightTheme().asStateFlow() + private val darkTheme = uiPreferences.darkTheme().asStateFlow() - private val baseThemeJob = SupervisorJob() - private val baseThemeScope = CoroutineScope(baseThemeJob) + private val baseThemeJob = SupervisorJob() + private val baseThemeScope = CoroutineScope(baseThemeJob) - @Composable - fun getColors(): Pair { - val themeMode by themeMode.collectAsState() - val lightTheme by lightTheme.collectAsState() - val darkTheme by darkTheme.collectAsState() + @Composable + fun getColors(): Pair { + val themeMode by themeMode.collectAsState() + val lightTheme by lightTheme.collectAsState() + val darkTheme by darkTheme.collectAsState() - val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme) - val colors = remember(baseTheme.colors.isLight) { - baseThemeJob.cancelChildren() + val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme) + val colors = remember(baseTheme.colors.isLight) { + baseThemeJob.cancelChildren() - if (baseTheme.colors.isLight) { - uiPreferences.getLightColors().asStateFlow(baseThemeScope) - } else { - uiPreferences.getDarkColors().asStateFlow(baseThemeScope) + if (baseTheme.colors.isLight) { + uiPreferences.getLightColors().asStateFlow(baseThemeScope) + } else { + uiPreferences.getDarkColors().asStateFlow(baseThemeScope) + } + } + + val primary by colors.primaryStateFlow.collectAsState() + val secondary by colors.secondaryStateFlow.collectAsState() + val tertiary by colors.tertiaryStateFlow.collectAsState() + + return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(baseTheme.extraColors, tertiary) + } + + @Composable + private fun getBaseTheme( + themeMode: ThemeMode, + lightTheme: Int, + darkTheme: Int, + ): Theme { + fun getTheme( + id: Int, + isLight: Boolean, + ): Theme { + return themes.find { it.id == id && it.colors.isLight == isLight } + ?: themes.first { it.colors.isLight == isLight } + } + + return when (themeMode) { + ThemeMode.System -> if (!isSystemInDarkTheme()) { + getTheme(lightTheme, true) + } else { + getTheme(darkTheme, false) + } + ThemeMode.Light -> getTheme(lightTheme, true) + ThemeMode.Dark -> getTheme(darkTheme, false) } } - val primary by colors.primaryStateFlow.collectAsState() - val secondary by colors.secondaryStateFlow.collectAsState() - val tertiary by colors.tertiaryStateFlow.collectAsState() - - return getMaterialColors(baseTheme.colors, primary, secondary) to getExtraColors(baseTheme.extraColors, tertiary) - } - - @Composable - private fun getBaseTheme( - themeMode: ThemeMode, - lightTheme: Int, - darkTheme: Int, - ): Theme { - fun getTheme(id: Int, isLight: Boolean): Theme { - return themes.find { it.id == id && it.colors.isLight == isLight } - ?: themes.first { it.colors.isLight == isLight } + private fun getMaterialColors( + baseColors: Colors, + colorPrimary: Color, + colorSecondary: Color, + ): Colors { + val primary = colorPrimary.takeOrElse { baseColors.primary } + val secondary = colorSecondary.takeOrElse { baseColors.secondary } + return baseColors.copy( + primary = primary, + primaryVariant = primary, + secondary = secondary, + secondaryVariant = secondary, + onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White, + onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White, + ) } - return when (themeMode) { - ThemeMode.System -> if (!isSystemInDarkTheme()) { - getTheme(lightTheme, true) - } else { - getTheme(darkTheme, false) - } - ThemeMode.Light -> getTheme(lightTheme, true) - ThemeMode.Dark -> getTheme(darkTheme, false) + private fun getExtraColors( + baseExtraColors: ExtraColors, + colorTertiary: Color, + ): ExtraColors { + val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary } + return baseExtraColors.copy( + tertiary = tertiary, + ) + } + + override fun onDispose() { + baseThemeScope.cancel() + scope.cancel() } } - - private fun getMaterialColors( - baseColors: Colors, - colorPrimary: Color, - colorSecondary: Color, - ): Colors { - val primary = colorPrimary.takeOrElse { baseColors.primary } - val secondary = colorSecondary.takeOrElse { baseColors.secondary } - return baseColors.copy( - primary = primary, - primaryVariant = primary, - secondary = secondary, - secondaryVariant = secondary, - onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White, - onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White, - ) - } - - private fun getExtraColors( - baseExtraColors: ExtraColors, - colorTertiary: Color, - ): ExtraColors { - val tertiary = colorTertiary.takeOrElse { baseExtraColors.tertiary } - return baseExtraColors.copy( - tertiary = tertiary, - ) - } - - override fun onDispose() { - baseThemeScope.cancel() - scope.cancel() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreen.kt index d1919f6d..ba2742df 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreen.kt @@ -16,7 +16,6 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey import kotlin.jvm.Transient expect class CategoriesLauncher { - fun open() @Composable @@ -30,7 +29,6 @@ class CategoriesScreen( @Transient private val notifyFinished: (() -> Unit)? = null, ) : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt index ba45fe34..dd14ab1d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesScreenViewModel.kt @@ -27,110 +27,115 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class CategoriesScreenViewModel @Inject constructor( - private val getCategories: GetCategories, - private val createCategory: CreateCategory, - private val deleteCategory: DeleteCategory, - private val modifyCategory: ModifyCategory, - private val reorderCategory: ReorderCategory, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - private var originalCategories = emptyList() - private val _categories = MutableStateFlow>(persistentListOf()) - val categories = _categories.asStateFlow() +class CategoriesScreenViewModel + @Inject + constructor( + private val getCategories: GetCategories, + private val createCategory: CreateCategory, + private val deleteCategory: DeleteCategory, + private val modifyCategory: ModifyCategory, + private val reorderCategory: ReorderCategory, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + private var originalCategories = emptyList() + private val _categories = MutableStateFlow>(persistentListOf()) + val categories = _categories.asStateFlow() - init { - scope.launch { - getCategories() + init { + scope.launch { + getCategories() + } } - } - private suspend fun getCategories() { - _categories.value = persistentListOf() - val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) - if (categories != null) { - _categories.value = categories - .sortedBy { it.order } - .also { originalCategories = it } - .map { it.toMenuCategory() } + private suspend fun getCategories() { + _categories.value = persistentListOf() + val categories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + if (categories != null) { + _categories.value = categories + .sortedBy { it.order } + .also { originalCategories = it } + .map { it.toMenuCategory() } + .toImmutableList() + } + } + + suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { + val categories = _categories.value + val newCategories = categories.filter { it.id == null } + newCategories.forEach { + createCategory.await(it.name, onError = { toast(it.message.orEmpty()) }) + } + originalCategories.forEach { originalCategory -> + val category = categories.find { it.id == originalCategory.id } + if (category == null) { + deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) }) + } else if (category.name != originalCategory.name) { + modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) }) + } + } + var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + categories.forEach { category -> + val updatedCategory = updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach + if (category.order != updatedCategory.order) { + log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } + reorderCategory.await(category.order, updatedCategory.order, onError = { toast(it.message.orEmpty()) }) + } + updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + } + + if (manualUpdate) { + getCategories() + } + } + + fun renameCategory( + category: MenuCategory, + newName: String, + ) { + _categories.value = (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order } .toImmutableList() } - } - suspend fun updateRemoteCategories(manualUpdate: Boolean = false) { - val categories = _categories.value - val newCategories = categories.filter { it.id == null } - newCategories.forEach { - createCategory.await(it.name, onError = { toast(it.message.orEmpty()) }) - } - originalCategories.forEach { originalCategory -> - val category = categories.find { it.id == originalCategory.id } - if (category == null) { - deleteCategory.await(originalCategory, onError = { toast(it.message.orEmpty()) }) - } else if (category.name != originalCategory.name) { - modifyCategory.await(originalCategory, category.name, onError = { toast(it.message.orEmpty()) }) - } - } - var updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) - categories.forEach { category -> - val updatedCategory = updatedCategories?.find { it.id == category.id || it.name == category.name } ?: return@forEach - if (category.order != updatedCategory.order) { - log.debug { "${category.name}: ${updatedCategory.order} to ${category.order}" } - reorderCategory.await(category.order, updatedCategory.order, onError = { toast(it.message.orEmpty()) }) - } - updatedCategories = getCategories.await(true, onError = { toast(it.message.orEmpty()) }) + fun deleteCategory(category: MenuCategory) { + _categories.value = _categories.value.toPersistentList() - category } - if (manualUpdate) { - getCategories() + fun createCategory(name: String) { + _categories.value = _categories.value.toPersistentList() + MenuCategory(order = categories.value.size + 1, name = name, default = false) + } + + fun moveUp(category: MenuCategory) { + val categories = _categories.value.toMutableList() + val index = categories.indexOf(category) + if (index == -1) throw Exception("Invalid index") + categories.add(index - 1, categories.removeAt(index)) + _categories.value = categories + .mapIndexed { i, menuCategory -> + menuCategory.copy(order = i + 1) + } + .sortedBy { it.order } + .toImmutableList() + } + + fun moveDown(category: MenuCategory) { + val categories = _categories.value.toMutableList() + val index = categories.indexOf(category) + if (index == -1) throw Exception("Invalid index") + categories.add(index + 1, categories.removeAt(index)) + _categories.value = categories + .mapIndexed { i, menuCategory -> + menuCategory.copy(order = i + 1) + } + .sortedBy { it.order } + .toImmutableList() + } + + private fun Category.toMenuCategory() = MenuCategory(id, order, name, default) + + @Stable + data class MenuCategory(val id: Long? = null, val order: Int, val name: String, val default: Boolean = false) + + private companion object { + private val log = logging() } } - - fun renameCategory(category: MenuCategory, newName: String) { - _categories.value = (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order } - .toImmutableList() - } - - fun deleteCategory(category: MenuCategory) { - _categories.value = _categories.value.toPersistentList() - category - } - - fun createCategory(name: String) { - _categories.value = _categories.value.toPersistentList() + MenuCategory(order = categories.value.size + 1, name = name, default = false) - } - - fun moveUp(category: MenuCategory) { - val categories = _categories.value.toMutableList() - val index = categories.indexOf(category) - if (index == -1) throw Exception("Invalid index") - categories.add(index - 1, categories.removeAt(index)) - _categories.value = categories - .mapIndexed { i, menuCategory -> - menuCategory.copy(order = i + 1) - } - .sortedBy { it.order } - .toImmutableList() - } - - fun moveDown(category: MenuCategory) { - val categories = _categories.value.toMutableList() - val index = categories.indexOf(category) - if (index == -1) throw Exception("Invalid index") - categories.add(index + 1, categories.removeAt(index)) - _categories.value = categories - .mapIndexed { i, menuCategory -> - menuCategory.copy(order = i + 1) - } - .sortedBy { it.order } - .toImmutableList() - } - - private fun Category.toMenuCategory() = MenuCategory(id, order, name, default) - - @Stable - data class MenuCategory(val id: Long? = null, val order: Int, val name: String, val default: Boolean = false) - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt index 16da489f..10fcd174 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/downloads/DownloadsScreenViewModel.kt @@ -31,83 +31,85 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class DownloadsScreenViewModel @Inject constructor( - private val downloadService: DownloadService, - private val startDownloading: StartDownloading, - private val stopDownloading: StopDownloading, - private val clearDownloadQueue: ClearDownloadQueue, - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val reorderChapterDownload: ReorderChapterDownload, - private val contextWrapper: ContextWrapper, - @Assisted standalone: Boolean, -) : ViewModel(contextWrapper) { - private val uiScope = if (standalone) { - MainScope() - } else { - null - } +class DownloadsScreenViewModel + @Inject + constructor( + private val downloadService: DownloadService, + private val startDownloading: StartDownloading, + private val stopDownloading: StopDownloading, + private val clearDownloadQueue: ClearDownloadQueue, + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val reorderChapterDownload: ReorderChapterDownload, + private val contextWrapper: ContextWrapper, + @Assisted standalone: Boolean, + ) : ViewModel(contextWrapper) { + private val uiScope = if (standalone) { + MainScope() + } else { + null + } - override val scope: CoroutineScope - get() = uiScope ?: super.scope + override val scope: CoroutineScope + get() = uiScope ?: super.scope - val serviceStatus = DownloadService.status.asStateFlow() - val downloaderStatus = DownloadService.downloaderStatus.asStateFlow() - val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val serviceStatus = DownloadService.status.asStateFlow() + val downloaderStatus = DownloadService.downloaderStatus.asStateFlow() + val downloadQueue = DownloadService.downloadQueue.map { it.toImmutableList() } + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - fun start() { - scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) } - } + fun start() { + scope.launch { startDownloading.await(onError = { toast(it.message.orEmpty()) }) } + } - fun pause() { - scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) } - } + fun pause() { + scope.launch { stopDownloading.await(onError = { toast(it.message.orEmpty()) }) } + } - fun clear() { - scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } - } + fun clear() { + scope.launch { clearDownloadQueue.await(onError = { toast(it.message.orEmpty()) }) } + } - fun stopDownload(chapter: Chapter) { - scope.launch { stopChapterDownload.await(chapter, onError = { toast(it.message.orEmpty()) }) } - } + fun stopDownload(chapter: Chapter) { + scope.launch { stopChapterDownload.await(chapter, onError = { toast(it.message.orEmpty()) }) } + } - fun moveUp(chapter: Chapter) { - scope.launch { - val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } - if (index == -1 || index <= 0) return@launch - reorderChapterDownload.await(chapter, index - 1, onError = { toast(it.message.orEmpty()) }) + fun moveUp(chapter: Chapter) { + scope.launch { + val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + if (index == -1 || index <= 0) return@launch + reorderChapterDownload.await(chapter, index - 1, onError = { toast(it.message.orEmpty()) }) + } + } + + fun moveDown(chapter: Chapter) { + scope.launch { + val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } + if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch + reorderChapterDownload.await(chapter, index + 1, onError = { toast(it.message.orEmpty()) }) + } + } + + fun moveToTop(chapter: Chapter) { + scope.launch { + reorderChapterDownload.await(chapter, 0, onError = { toast(it.message.orEmpty()) }) + } + } + + fun moveToBottom(chapter: Chapter) { + scope.launch { + reorderChapterDownload.await(chapter, downloadQueue.value.lastIndex, onError = { toast(it.message.orEmpty()) }) + } + } + + fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART) + + override fun onDispose() { + super.onDispose() + uiScope?.cancel() + } + + private companion object { + private val log = logging() } } - - fun moveDown(chapter: Chapter) { - scope.launch { - val index = downloadQueue.value.indexOfFirst { it.mangaId == chapter.mangaId && it.chapterIndex == chapter.index } - if (index == -1 || index >= downloadQueue.value.lastIndex) return@launch - reorderChapterDownload.await(chapter, index + 1, onError = { toast(it.message.orEmpty()) }) - } - } - - fun moveToTop(chapter: Chapter) { - scope.launch { - reorderChapterDownload.await(chapter, 0, onError = { toast(it.message.orEmpty()) }) - } - } - - fun moveToBottom(chapter: Chapter) { - scope.launch { - reorderChapterDownload.await(chapter, downloadQueue.value.lastIndex, onError = { toast(it.message.orEmpty()) }) - } - } - - fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART) - - override fun onDispose() { - super.onDispose() - uiScope?.cancel() - } - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt index 473aa081..7e7e8be8 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreen.kt @@ -15,7 +15,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class ExtensionsScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt index 4671b02b..4719a1c0 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/extensions/ExtensionsScreenViewModel.kt @@ -41,185 +41,187 @@ import okio.Source import org.lighthousegames.logging.logging import kotlin.random.Random -class ExtensionsScreenViewModel @Inject constructor( - private val getExtensionList: GetExtensionList, - private val installExtensionFile: InstallExtensionFile, - private val installExtension: InstallExtension, - private val updateExtension: UpdateExtension, - private val uninstallExtension: UninstallExtension, - extensionPreferences: ExtensionPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - private val extensionList = MutableStateFlow?>(null) +class ExtensionsScreenViewModel + @Inject + constructor( + private val getExtensionList: GetExtensionList, + private val installExtensionFile: InstallExtensionFile, + private val installExtension: InstallExtension, + private val updateExtension: UpdateExtension, + private val uninstallExtension: UninstallExtension, + extensionPreferences: ExtensionPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + private val extensionList = MutableStateFlow?>(null) - private val _enabledLangs = extensionPreferences.languages().asStateFlow() - val enabledLangs = _enabledLangs.map { it.toImmutableSet() } - .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) + private val _enabledLangs = extensionPreferences.languages().asStateFlow() + val enabledLangs = _enabledLangs.map { it.toImmutableSet() } + .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) - private val _searchQuery = MutableStateFlow(null) - val searchQuery = _searchQuery.asStateFlow() + private val _searchQuery = MutableStateFlow(null) + val searchQuery = _searchQuery.asStateFlow() - private val workingExtensions = MutableStateFlow>(emptyList()) + private val workingExtensions = MutableStateFlow>(emptyList()) - val extensions = combine( - searchQuery, - extensionList, - enabledLangs, - workingExtensions, - ) { searchQuery, extensions, enabledLangs, workingExtensions -> - search(searchQuery, extensions, enabledLangs, workingExtensions) - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val extensions = combine( + searchQuery, + extensionList, + enabledLangs, + workingExtensions, + ) { searchQuery, extensions, enabledLangs, workingExtensions -> + search(searchQuery, extensions, enabledLangs, workingExtensions) + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val availableLangs = extensionList.filterNotNull().map { langs -> - langs.map { it.lang }.distinct().toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val availableLangs = extensionList.filterNotNull().map { langs -> + langs.map { it.lang }.distinct().toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true) + val isLoading = extensionList.map { it == null }.stateIn(scope, SharingStarted.Eagerly, true) - init { - scope.launch { - getExtensions() - } - } - - private suspend fun getExtensions() { - extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty() - } - - fun install(source: Source) { - log.info { "Install file clicked" } - scope.launch { - try { - val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY - .resolve("tachidesk.${Random.nextLong()}.proto.gz") - .also { file -> - source.saveTo(file) - } - installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) }) - } catch (e: Exception) { - log.warn(e) { "Error creating apk file" } - // todo toast if error - e.throwIfCancellation() + init { + scope.launch { + getExtensions() } - - getExtensions() } - } - fun install(extension: Extension) { - log.info { "Install clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - installExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } + private suspend fun getExtensions() { + extensionList.value = getExtensionList.await(onError = { toast(it.message.orEmpty()) }).orEmpty() } - } - fun update(extension: Extension) { - log.info { "Update clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - updateExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } - } - } + fun install(source: Source) { + log.info { "Install file clicked" } + scope.launch { + try { + val file = FileSystem.SYSTEM_TEMPORARY_DIRECTORY + .resolve("tachidesk.${Random.nextLong()}.proto.gz") + .also { file -> + source.saveTo(file) + } + installExtensionFile.await(file, onError = { toast(it.message.orEmpty()) }) + } catch (e: Exception) { + log.warn(e) { "Error creating apk file" } + // todo toast if error + e.throwIfCancellation() + } - fun uninstall(extension: Extension) { - log.info { "Uninstall clicked" } - scope.launch { - workingExtensions.update { it + extension.apkName } - uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) }) - getExtensions() - workingExtensions.update { it - extension.apkName } - } - } - - fun setEnabledLanguages(langs: Set) { - _enabledLangs.value = langs - } - - fun setQuery(query: String) { - _searchQuery.value = query - } - - private fun search( - searchQuery: String?, - extensionList: List?, - enabledLangs: Set, - workingExtensions: List, - ): ImmutableList { - val extensions = extensionList?.filter { it.lang in enabledLangs } - .orEmpty() - return if (searchQuery.isNullOrBlank()) { - extensions.splitSort(workingExtensions) - } else { - val queries = searchQuery.split(" ") - val filteredExtensions = extensions.toMutableList() - queries.forEach { query -> - filteredExtensions.removeAll { !it.name.contains(query, true) } + getExtensions() } - filteredExtensions.toList().splitSort(workingExtensions) } - } - private fun List.splitSort(workingExtensions: List): ImmutableList { - val all = MR.strings.all.toPlatformString() - return this - .filter(Extension::installed) - .sortedWith( - compareBy { - when { - it.obsolete -> 1 - it.hasUpdate -> 2 - else -> 3 - } + fun install(extension: Extension) { + log.info { "Install clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + installExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun update(extension: Extension) { + log.info { "Update clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + updateExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun uninstall(extension: Extension) { + log.info { "Uninstall clicked" } + scope.launch { + workingExtensions.update { it + extension.apkName } + uninstallExtension.await(extension, onError = { toast(it.message.orEmpty()) }) + getExtensions() + workingExtensions.update { it - extension.apkName } + } + } + + fun setEnabledLanguages(langs: Set) { + _enabledLangs.value = langs + } + + fun setQuery(query: String) { + _searchQuery.value = query + } + + private fun search( + searchQuery: String?, + extensionList: List?, + enabledLangs: Set, + workingExtensions: List, + ): ImmutableList { + val extensions = extensionList?.filter { it.lang in enabledLangs } + .orEmpty() + return if (searchQuery.isNullOrBlank()) { + extensions.splitSort(workingExtensions) + } else { + val queries = searchQuery.split(" ") + val filteredExtensions = extensions.toMutableList() + queries.forEach { query -> + filteredExtensions.removeAll { !it.name.contains(query, true) } } - .thenBy(Extension::lang) - .thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name), - ) - .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } - .let { - if (it.isNotEmpty()) { - listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it - } else { - it - } - }.plus( - filterNot(Extension::installed) - .groupBy(Extension::lang) - .mapKeys { (key) -> - when (key) { - "all" -> all - else -> Locale(key).displayName + filteredExtensions.toList().splitSort(workingExtensions) + } + } + + private fun List.splitSort(workingExtensions: List): ImmutableList { + val all = MR.strings.all.toPlatformString() + return this + .filter(Extension::installed) + .sortedWith( + compareBy { + when { + it.obsolete -> 1 + it.hasUpdate -> 2 + else -> 3 } } - .mapValues { - it.value - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name)) - .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } + .thenBy(Extension::lang) + .thenBy(String.CASE_INSENSITIVE_ORDER, Extension::name), + ) + .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } + .let { + if (it.isNotEmpty()) { + listOf(ExtensionUI.Header(MR.strings.installed.toPlatformString())) + it + } else { + it } - .toList() - .sortedWith( - compareBy> { (key) -> + }.plus( + filterNot(Extension::installed) + .groupBy(Extension::lang) + .mapKeys { (key) -> when (key) { - all -> 1 - else -> 2 + "all" -> all + else -> Locale(key).displayName } - }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), - ) - .flatMap { (key, value) -> - listOf(ExtensionUI.Header(key)) + value - }, - ) - .toImmutableList() - } + } + .mapValues { + it.value + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Extension::name)) + .map { ExtensionUI.ExtensionItem(it, it.apkName in workingExtensions) } + } + .toList() + .sortedWith( + compareBy> { (key) -> + when (key) { + all -> 1 + else -> 2 + } + }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), + ) + .flatMap { (key, value) -> + listOf(ExtensionUI.Header(key)) + value + }, + ) + .toImmutableList() + } - private companion object { - private val log = logging() + private companion object { + private val log = logging() + } } -} @Immutable sealed class ExtensionUI { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt index 60832961..7f8d57a4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreen.kt @@ -20,7 +20,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow class LibraryScreen : BaseScreen() { - @Composable override fun Content() { val vm = stateViewModel { libraryViewModel(it) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt index 8022a6cb..2bf47022 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/LibraryScreenViewModel.kt @@ -62,7 +62,7 @@ sealed class LibraryState { @Stable data class Loaded( - val categories: ImmutableList + val categories: ImmutableList, ) : LibraryState() } @@ -88,10 +88,17 @@ private fun LibraryMap.getManga(id: Long) = getOrPut(id) { MutableStateFlow(CategoryState.Loading) } -private fun LibraryMap.setError(id: Long, e: Throwable) { +private fun LibraryMap.setError( + id: Long, + e: Throwable, +) { getManga(id).value = CategoryState.Failed(e) } -private fun LibraryMap.setManga(id: Long, manga: ImmutableList, getItemsFlow: (StateFlow>) -> StateFlow>) { +private fun LibraryMap.setManga( + id: Long, + manga: ImmutableList, + getItemsFlow: (StateFlow>) -> StateFlow>, +) { val flow = getManga(id) when (val state = flow.value) { is CategoryState.Loaded -> state.unfilteredItems.value = manga @@ -102,207 +109,215 @@ private fun LibraryMap.setManga(id: Long, manga: ImmutableList, getItemsF } } -class LibraryScreenViewModel @Inject constructor( - private val getCategories: GetCategories, - private val getMangaListFromCategory: GetMangaListFromCategory, - private val removeMangaFromLibrary: RemoveMangaFromLibrary, - private val updateLibrary: UpdateLibrary, - private val updateCategory: UpdateCategory, - libraryPreferences: LibraryPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, -) : ViewModel(contextWrapper) { - private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf()) - val categories = library.categories.asStateFlow() +class LibraryScreenViewModel + @Inject + constructor( + private val getCategories: GetCategories, + private val getMangaListFromCategory: GetMangaListFromCategory, + private val removeMangaFromLibrary: RemoveMangaFromLibrary, + private val updateLibrary: UpdateLibrary, + private val updateCategory: UpdateCategory, + libraryPreferences: LibraryPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, + ) : ViewModel(contextWrapper) { + private val library = Library(MutableStateFlow(LibraryState.Loading), mutableMapOf()) + val categories = library.categories.asStateFlow() - private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 } - val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() + private val _selectedCategoryIndex by savedStateHandle.getStateFlow { 0 } + val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() - private val _showingMenu by savedStateHandle.getStateFlow { false } - val showingMenu = _showingMenu.asStateFlow() + private val _showingMenu by savedStateHandle.getStateFlow { false } + val showingMenu = _showingMenu.asStateFlow() - val displayMode = libraryPreferences.displayMode().stateIn(scope) - val gridColumns = libraryPreferences.gridColumns().stateIn(scope) - val gridSize = libraryPreferences.gridSize().stateIn(scope) - val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) - val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) - val languageBadges = libraryPreferences.languageBadge().stateIn(scope) - val localBadges = libraryPreferences.localBadge().stateIn(scope) + val displayMode = libraryPreferences.displayMode().stateIn(scope) + val gridColumns = libraryPreferences.gridColumns().stateIn(scope) + val gridSize = libraryPreferences.gridSize().stateIn(scope) + val unreadBadges = libraryPreferences.unreadBadge().stateIn(scope) + val downloadBadges = libraryPreferences.downloadBadge().stateIn(scope) + val languageBadges = libraryPreferences.languageBadge().stateIn(scope) + val localBadges = libraryPreferences.localBadge().stateIn(scope) - private val sortMode = libraryPreferences.sortMode().stateIn(scope) - private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) + private val sortMode = libraryPreferences.sortMode().stateIn(scope) + private val sortAscending = libraryPreferences.sortAscending().stateIn(scope) - private val filter: Flow<(Manga) -> Boolean> = combine( - libraryPreferences.filterDownloaded().getAsFlow(), - libraryPreferences.filterUnread().getAsFlow(), - libraryPreferences.filterCompleted().getAsFlow(), - ) { downloaded, unread, completed -> - { manga -> - when (downloaded) { - FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 - FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0 - FilterState.IGNORED -> true - } && when (unread) { - FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 - FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 - FilterState.IGNORED -> true - } && when (completed) { - FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED - FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED - FilterState.IGNORED -> true + private val filter: Flow<(Manga) -> Boolean> = combine( + libraryPreferences.filterDownloaded().getAsFlow(), + libraryPreferences.filterUnread().getAsFlow(), + libraryPreferences.filterCompleted().getAsFlow(), + ) { downloaded, unread, completed -> + { manga -> + when (downloaded) { + FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0 + FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0 + FilterState.IGNORED -> true + } && when (unread) { + FilterState.EXCLUDED -> manga.unreadCount == null || manga.unreadCount == 0 + FilterState.INCLUDED -> manga.unreadCount != null && (manga.unreadCount ?: 0) > 0 + FilterState.IGNORED -> true + } && when (completed) { + FilterState.EXCLUDED -> manga.status != MangaStatus.COMPLETED + FilterState.INCLUDED -> manga.status == MangaStatus.COMPLETED + FilterState.IGNORED -> true + } } } - } - private val _query by savedStateHandle.getStateFlow { "" } - val query = _query.asStateFlow() + private val _query by savedStateHandle.getStateFlow { "" } + val query = _query.asStateFlow() - private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending -> - getComparator(sortMode, sortAscending) - }.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title }) + private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending -> + getComparator(sortMode, sortAscending) + }.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title }) - init { - getLibrary() - } + init { + getLibrary() + } - private fun getLibrary() { - library.categories.value = LibraryState.Loading - getCategories.asFlow() - .onEach { categories -> - if (categories.isEmpty()) { - throw Exception(MR.strings.library_empty.toPlatformString()) + private fun getLibrary() { + library.categories.value = LibraryState.Loading + getCategories.asFlow() + .onEach { categories -> + if (categories.isEmpty()) { + throw Exception(MR.strings.library_empty.toPlatformString()) + } + library.categories.value = LibraryState.Loaded( + categories.sortedBy { it.order } + .toImmutableList(), + ) + categories.forEach { category -> + getMangaListFromCategory.asFlow(category) + .onEach { + library.mangaMap.setManga( + id = category.id, + manga = it.toImmutableList(), + getItemsFlow = ::getMangaItemsFlow, + ) + } + .catch { + log.warn(it) { "Failed to get manga list from category ${category.name}" } + library.mangaMap.setError(category.id, it) + } + .launchIn(coroutineScope) + } } - library.categories.value = LibraryState.Loaded( - categories.sortedBy { it.order } - .toImmutableList() - ) - categories.forEach { category -> - getMangaListFromCategory.asFlow(category) - .onEach { - library.mangaMap.setManga( - id = category.id, - manga = it.toImmutableList(), - getItemsFlow = ::getMangaItemsFlow, - ) + .catch { + library.categories.value = LibraryState.Failed(it) + log.warn(it) { "Failed to get categories" } + } + .launchIn(scope) + } + + fun setSelectedPage(page: Int) { + _selectedCategoryIndex.value = page + } + + fun setShowingMenu(showingMenu: Boolean) { + _showingMenu.value = showingMenu + } + + private fun getComparator( + sortMode: Sort, + ascending: Boolean, + ): Comparator { + val sortFn: (Manga, Manga) -> Int = when (sortMode) { + Sort.ALPHABETICAL -> { + val locale = Locale.current + val collator = CollatorComparator(locale); + + { a, b -> + collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale)) + } + } + Sort.UNREAD -> { + { a, b -> + when { + // Ensure unread content comes first + (a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0 + a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1 + b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1 + else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0) } - .catch { - log.warn(it) { "Failed to get manga list from category ${category.name}" } - library.mangaMap.setError(category.id, it) - } - .launchIn(coroutineScope) + } } - } - .catch { - library.categories.value = LibraryState.Failed(it) - log.warn(it) { "Failed to get categories" } - } - .launchIn(scope) - } - - fun setSelectedPage(page: Int) { - _selectedCategoryIndex.value = page - } - - fun setShowingMenu(showingMenu: Boolean) { - _showingMenu.value = showingMenu - } - - private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator { - val sortFn: (Manga, Manga) -> Int = when (sortMode) { - Sort.ALPHABETICAL -> { - val locale = Locale.current - val collator = CollatorComparator(locale); - - { a, b -> - collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale)) - } - } - Sort.UNREAD -> { - { a, b -> - when { - // Ensure unread content comes first - (a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0 - a.unreadCount == null || a.unreadCount == 0 -> if (ascending) 1 else -1 - b.unreadCount == null || b.unreadCount == 0 -> if (ascending) -1 else 1 - else -> (a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0) + Sort.DATE_ADDED -> { + { a, b -> + a.inLibraryAt.compareTo(b.inLibraryAt) } } } - Sort.DATE_ADDED -> { - { a, b -> - a.inLibraryAt.compareTo(b.inLibraryAt) + return if (ascending) { + Comparator(sortFn) + } else { + Comparator(sortFn).reversed() + } + } + + private suspend fun filterManga( + query: String, + mangaList: List, + ): List { + if (query.isBlank()) return mangaList + val queries = query.split(" ") + return mangaList.asFlow() + .filter { manga -> + queries.all { query -> + manga.title.contains(query, true) || + manga.author.orEmpty().contains(query, true) || + manga.artist.orEmpty().contains(query, true) || + manga.genre.any { it.contains(query, true) } || + manga.description.orEmpty().contains(query, true) || + manga.status.name.contains(query, true) + } } - } + .cancellable() + .buffer() + .toList() } - return if (ascending) { - Comparator(sortFn) - } else { - Comparator(sortFn).reversed() - } - } - private suspend fun filterManga(query: String, mangaList: List): List { - if (query.isBlank()) return mangaList - val queries = query.split(" ") - return mangaList.asFlow() - .filter { manga -> - queries.all { query -> - manga.title.contains(query, true) || - manga.author.orEmpty().contains(query, true) || - manga.artist.orEmpty().contains(query, true) || - manga.genre.any { it.contains(query, true) } || - manga.description.orEmpty().contains(query, true) || - manga.status.name.contains(query, true) + private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow>): StateFlow> { + return combine(unfilteredItemsFlow, query) { unfilteredItems, query -> + filterManga(query, unfilteredItems) + }.combine(filter) { filteredManga, filterer -> + filteredManga.filter(filterer) + }.combine(comparator) { filteredManga, comparator -> + filteredManga.sortedWith(comparator) + }.map { + it.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + } + + fun getLibraryForCategoryId(id: Long): StateFlow { + return library.mangaMap.getManga(id) + } + + private fun getCategoriesToUpdate(mangaId: Long): List { + return library.mangaMap + .filter { mangaMapEntry -> + (mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null } + .mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } } + } + + fun removeManga(mangaId: Long) { + scope.launch { + removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) }) } - .cancellable() - .buffer() - .toList() - } + } - private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow>): StateFlow> { - return combine(unfilteredItemsFlow, query) { unfilteredItems, query -> - filterManga(query, unfilteredItems) - }.combine(filter) { filteredManga, filterer -> - filteredManga.filter(filterer) - }.combine(comparator) { filteredManga, comparator -> - filteredManga.sortedWith(comparator) - }.map { - it.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - } + fun updateQuery(query: String) { + _query.value = query + } - fun getLibraryForCategoryId(id: Long): StateFlow { - return library.mangaMap.getManga(id) - } + fun updateLibrary() { + scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } + } - private fun getCategoriesToUpdate(mangaId: Long): List { - return library.mangaMap - .filter { mangaMapEntry -> - (mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null - } - .mapNotNull { (id) -> (library.categories.value as? LibraryState.Loaded)?.categories?.first { it.id == id } } - } + fun updateCategory(category: Category) { + scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) } + } - fun removeManga(mangaId: Long) { - scope.launch { - removeMangaFromLibrary.await(mangaId, onError = { toast(it.message.orEmpty()) }) + private companion object { + private val log = logging() } } - - fun updateQuery(query: String) { - _query.value = query - } - - fun updateLibrary() { - scope.launch { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } - } - - fun updateCategory(category: Category) { - scope.launch { updateCategory.await(category, onError = { toast(it.message.orEmpty()) }) } - } - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt index b00ae437..287c185b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryDisplay.kt @@ -25,22 +25,23 @@ import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction import ca.gosyer.jui.uicore.resources.stringResource @Composable -fun getLibraryDisplay(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { - @Composable { - LibraryDisplay( - displayMode = vm.displayMode.collectAsState().value, - unreadBadges = vm.unreadBadges.collectAsState().value, - downloadBadges = vm.downloadBadges.collectAsState().value, - languageBadges = vm.languageBadges.collectAsState().value, - localBadges = vm.localBadges.collectAsState().value, - setDisplayMode = { vm.displayMode.value = it }, - setUnreadBadges = { vm.unreadBadges.value = it }, - setDownloadBadges = { vm.downloadBadges.value = it }, - setLanguageBadges = { vm.languageBadges.value = it }, - setLocalBadges = { vm.localBadges.value = it }, - ) +fun getLibraryDisplay(vm: LibrarySettingsViewModel): @Composable () -> Unit = + remember(vm) { + @Composable { + LibraryDisplay( + displayMode = vm.displayMode.collectAsState().value, + unreadBadges = vm.unreadBadges.collectAsState().value, + downloadBadges = vm.downloadBadges.collectAsState().value, + languageBadges = vm.languageBadges.collectAsState().value, + localBadges = vm.localBadges.collectAsState().value, + setDisplayMode = { vm.displayMode.value = it }, + setUnreadBadges = { vm.unreadBadges.value = it }, + setDownloadBadges = { vm.downloadBadges.value = it }, + setLanguageBadges = { vm.languageBadges.value = it }, + setLocalBadges = { vm.localBadges.value = it }, + ) + } } -} @Composable fun LibraryDisplay( @@ -99,7 +100,11 @@ private fun TitleText(text: String) { } @Composable -private fun RadioSelectionItem(text: String, selected: Boolean, onClick: () -> Unit) { +private fun RadioSelectionItem( + text: String, + selected: Boolean, + onClick: () -> Unit, +) { SourceFilterAction( name = text, onClick = onClick, @@ -113,7 +118,11 @@ private fun RadioSelectionItem(text: String, selected: Boolean, onClick: () -> U } @Composable -private fun CheckboxItem(text: String, checked: Boolean, onClick: () -> Unit) { +private fun CheckboxItem( + text: String, + checked: Boolean, + onClick: () -> Unit, +) { SourceFilterAction( name = text, onClick = onClick, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt index ad15112f..a148a5d1 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibraryFilters.kt @@ -20,18 +20,19 @@ import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction import ca.gosyer.jui.uicore.resources.stringResource @Composable -fun getLibraryFilters(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { - @Composable { - LibraryFilters( - downloaded = vm.filterDownloaded.collectAsState().value, - unread = vm.filterUnread.collectAsState().value, - completed = vm.filterCompleted.collectAsState().value, - setDownloadedFilter = { vm.filterDownloaded.value = it }, - setUnreadFilter = { vm.filterUnread.value = it }, - setCompletedFilter = { vm.filterCompleted.value = it }, - ) +fun getLibraryFilters(vm: LibrarySettingsViewModel): @Composable () -> Unit = + remember(vm) { + @Composable { + LibraryFilters( + downloaded = vm.filterDownloaded.collectAsState().value, + unread = vm.filterUnread.collectAsState().value, + completed = vm.filterCompleted.collectAsState().value, + setDownloadedFilter = { vm.filterDownloaded.value = it }, + setUnreadFilter = { vm.filterUnread.value = it }, + setCompletedFilter = { vm.filterCompleted.value = it }, + ) + } } -} @Composable fun LibraryFilters( @@ -61,14 +62,19 @@ fun LibraryFilters( } } -fun toggleState(filterState: FilterState) = when (filterState) { - FilterState.IGNORED -> FilterState.INCLUDED - FilterState.INCLUDED -> FilterState.EXCLUDED - FilterState.EXCLUDED -> FilterState.IGNORED -} +fun toggleState(filterState: FilterState) = + when (filterState) { + FilterState.IGNORED -> FilterState.INCLUDED + FilterState.INCLUDED -> FilterState.EXCLUDED + FilterState.EXCLUDED -> FilterState.IGNORED + } @Composable -private fun Filter(text: String, state: FilterState, onClick: () -> Unit) { +private fun Filter( + text: String, + state: FilterState, + onClick: () -> Unit, +) { SourceFilterAction( text, onClick = onClick, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt index e6bbf4d8..d4e7f8b2 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySettingsViewModel.kt @@ -11,20 +11,22 @@ import ca.gosyer.jui.uicore.vm.ContextWrapper import ca.gosyer.jui.uicore.vm.ViewModel import me.tatarka.inject.annotations.Inject -class LibrarySettingsViewModel @Inject constructor( - libraryPreferences: LibraryPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() - val filterUnread = libraryPreferences.filterUnread().asStateFlow() - val filterCompleted = libraryPreferences.filterCompleted().asStateFlow() +class LibrarySettingsViewModel + @Inject + constructor( + libraryPreferences: LibraryPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val filterDownloaded = libraryPreferences.filterDownloaded().asStateFlow() + val filterUnread = libraryPreferences.filterUnread().asStateFlow() + val filterCompleted = libraryPreferences.filterCompleted().asStateFlow() - val sortMode = libraryPreferences.sortMode().asStateFlow() - val sortAscending = libraryPreferences.sortAscending().asStateFlow() + val sortMode = libraryPreferences.sortMode().asStateFlow() + val sortAscending = libraryPreferences.sortAscending().asStateFlow() - val displayMode = libraryPreferences.displayMode().asStateFlow() - val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() - val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() - val languageBadges = libraryPreferences.languageBadge().asStateFlow() - val localBadges = libraryPreferences.localBadge().asStateFlow() -} + val displayMode = libraryPreferences.displayMode().asStateFlow() + val unreadBadges = libraryPreferences.unreadBadge().asStateFlow() + val downloadBadges = libraryPreferences.downloadBadge().asStateFlow() + val languageBadges = libraryPreferences.languageBadge().asStateFlow() + val localBadges = libraryPreferences.localBadge().asStateFlow() + } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt index 6a40965d..b6b059a4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/library/settings/LibrarySort.kt @@ -26,19 +26,20 @@ import ca.gosyer.jui.ui.sources.browse.filter.SourceFilterAction import ca.gosyer.jui.uicore.resources.stringResource @Composable -fun getLibrarySort(vm: LibrarySettingsViewModel): @Composable () -> Unit = remember(vm) { - @Composable { - LibrarySort( - mode = vm.sortMode.collectAsState().value, - ascending = vm.sortAscending.collectAsState().value, - setMode = { - vm.sortMode.value = it - vm.sortAscending.value = true - }, - setAscending = { vm.sortAscending.value = it }, - ) +fun getLibrarySort(vm: LibrarySettingsViewModel): @Composable () -> Unit = + remember(vm) { + @Composable { + LibrarySort( + mode = vm.sortMode.collectAsState().value, + ascending = vm.sortAscending.collectAsState().value, + setMode = { + vm.sortMode.value = it + vm.sortAscending.value = true + }, + setAscending = { vm.sortAscending.value = it }, + ) + } } -} @Composable fun LibrarySort( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt index 4791c250..4f81393d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainMenu.kt @@ -84,9 +84,7 @@ fun MainMenu() { } @Composable -fun SkinnyMainMenu( - navigator: Navigator, -) { +fun SkinnyMainMenu(navigator: Navigator) { WithBottomNav(navigator) { MainWindow(navigator, Modifier) } @@ -118,7 +116,10 @@ fun WideMainMenu( } @Composable -fun MainWindow(navigator: Navigator, modifier: Modifier) { +fun MainWindow( + navigator: Navigator, + modifier: Modifier, +) { Surface(Modifier.fillMaxSize() then modifier) { FadeTransition(navigator) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt index 11b72cb7..736d63d6 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/MainViewModel.kt @@ -14,21 +14,23 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import me.tatarka.inject.annotations.Inject -class MainViewModel @Inject constructor( - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - override val scope = MainScope() +class MainViewModel + @Inject + constructor( + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + override val scope = MainScope() - val startScreen = uiPreferences.startScreen().get() - val confirmExit = uiPreferences.confirmExit().stateIn(scope) + val startScreen = uiPreferences.startScreen().get() + val confirmExit = uiPreferences.confirmExit().stateIn(scope) - override fun onDispose() { - super.onDispose() - scope.cancel() + override fun onDispose() { + super.onDispose() + scope.cancel() + } + + fun confirmExitToast() { + toast(MR.strings.confirm_exit_toast.toPlatformString()) + } } - - fun confirmExitToast() { - toast(MR.strings.confirm_exit_toast.toPlatformString()) - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/Routes.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/Routes.kt index 4efc4756..2160d939 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/Routes.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/Routes.kt @@ -12,16 +12,18 @@ import ca.gosyer.jui.ui.library.LibraryScreen import ca.gosyer.jui.ui.sources.SourcesScreen import ca.gosyer.jui.ui.updates.UpdatesScreen -fun StartScreen.toScreen() = when (this) { - StartScreen.Library -> LibraryScreen() - StartScreen.Updates -> UpdatesScreen() - StartScreen.Sources -> SourcesScreen() - StartScreen.Extensions -> ExtensionsScreen() -} +fun StartScreen.toScreen() = + when (this) { + StartScreen.Library -> LibraryScreen() + StartScreen.Updates -> UpdatesScreen() + StartScreen.Sources -> SourcesScreen() + StartScreen.Extensions -> ExtensionsScreen() + } -fun StartScreen.toScreenClazz() = when (this) { - StartScreen.Library -> LibraryScreen::class - StartScreen.Updates -> UpdatesScreen::class - StartScreen.Sources -> SourcesScreen::class - StartScreen.Extensions -> ExtensionsScreen::class -} +fun StartScreen.toScreenClazz() = + when (this) { + StartScreen.Library -> LibraryScreen::class + StartScreen.Updates -> UpdatesScreen::class + StartScreen.Sources -> SourcesScreen::class + StartScreen.Extensions -> ExtensionsScreen::class + } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/TopLevelMenus.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/TopLevelMenus.kt index 4eb41e2b..3a2d5a31 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/TopLevelMenus.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/TopLevelMenus.kt @@ -60,7 +60,9 @@ enum class TopLevelMenus( override val createScreen: () -> Screen, override val extraInfo: (@Composable () -> Unit)? = null, ) : Menu { - Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, extraInfo = { LibraryUpdatesExtraInfo() }), + Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { + LibraryScreen() + }, extraInfo = { LibraryUpdatesExtraInfo() }), Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }), Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }), Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }), @@ -75,7 +77,9 @@ enum class MoreMenus( override val createScreen: () -> Screen, override val extraInfo: (@Composable () -> Unit)? = null, ) : Menu { - Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { DownloadsScreen() }, extraInfo = { DownloadsExtraInfo() }), + Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { + DownloadsScreen() + }, extraInfo = { DownloadsExtraInfo() }), Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, SettingsScreen::class, { SettingsScreen() }), About(MR.strings.location_about, Icons.Outlined.Info, Icons.Rounded.Info, AboutScreen::class, { AboutScreen() }), } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutScreen.kt index 51a7f83b..e3f40fa9 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutScreen.kt @@ -21,7 +21,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow import kotlinx.coroutines.launch class AboutScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt index eb0e0e33..d02d4c5d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/AboutViewModel.kt @@ -26,50 +26,51 @@ import kotlinx.datetime.Instant import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class AboutViewModel @Inject constructor( - private val dateHandler: DateHandler, - private val aboutServer: AboutServer, - private val updateChecker: UpdateChecker, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { +class AboutViewModel + @Inject + constructor( + private val dateHandler: DateHandler, + private val aboutServer: AboutServer, + private val updateChecker: UpdateChecker, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + private val _aboutHolder = MutableStateFlow(null) + val aboutHolder = _aboutHolder.asStateFlow() - private val _aboutHolder = MutableStateFlow(null) - val aboutHolder = _aboutHolder.asStateFlow() + val formattedBuildTime = aboutHolder.map { about -> + about ?: return@map "" + getFormattedDate(Instant.fromEpochSeconds(about.buildTime)) + }.stateIn(scope, SharingStarted.Eagerly, "") - val formattedBuildTime = aboutHolder.map { about -> - about ?: return@map "" - getFormattedDate(Instant.fromEpochSeconds(about.buildTime)) - }.stateIn(scope, SharingStarted.Eagerly, "") + private val _updates = MutableSharedFlow() + val updates = _updates.asSharedFlow() - private val _updates = MutableSharedFlow() - val updates = _updates.asSharedFlow() - - init { - getAbout() - } - - private fun getAbout() { - scope.launch { - _aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) }) + init { + getAbout() } - } - fun checkForUpdates() { - scope.launch { - toast(MR.strings.update_check_look_for_updates.toPlatformString()) - when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) { - is Update.UpdateFound -> _updates.emit(update) - is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString()) - null -> Unit + private fun getAbout() { + scope.launch { + _aboutHolder.value = aboutServer.await(onError = { toast(it.message.orEmpty()) }) } } - } - private fun getFormattedDate(time: Instant): String { - return dateHandler.dateTimeFormat(time) - } + fun checkForUpdates() { + scope.launch { + toast(MR.strings.update_check_look_for_updates.toPlatformString()) + when (val update = updateChecker.await(true, onError = { toast(it.message.orEmpty()) })) { + is Update.UpdateFound -> _updates.emit(update) + is Update.NoUpdatesFound -> toast(MR.strings.update_check_no_new_updates.toPlatformString()) + null -> Unit + } + } + } - companion object { - private val log = logging() + private fun getFormattedDate(time: Instant): String { + return dateHandler.dateTimeFormat(time) + } + + companion object { + private val log = logging() + } } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/components/AboutContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/components/AboutContent.kt index 0ec480a0..e11f85ec 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/components/AboutContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/components/AboutContent.kt @@ -176,7 +176,10 @@ private fun ClientVersionInfo() { } @Composable -private fun ServerVersionInfo(about: About?, formattedBuildTime: String) { +private fun ServerVersionInfo( + about: About?, + formattedBuildTime: String, +) { if (about == null) { Box(Modifier.fillMaxWidth().height(48.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/LicensesScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/LicensesScreen.kt index 1620bcde..0eea12ed 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/LicensesScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/about/licenses/LicensesScreen.kt @@ -13,7 +13,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class LicensesScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/BottomNav.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/BottomNav.kt index 9d4ecce2..3ce3b6b4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/BottomNav.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/BottomNav.kt @@ -53,7 +53,10 @@ val WindowInsets.Companion.bottomNav @Composable get() = WindowInsets(bottom = BottomNavHeightLocal.current) @Composable -fun WithBottomNav(navigator: Navigator, content: @Composable () -> Unit) { +fun WithBottomNav( + navigator: Navigator, + content: @Composable () -> Unit, +) { Box { val isBottomNavVisible = navigator.size <= 1 CompositionLocalProvider( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt index 2b979a27..3a31d654 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/LibraryUpdatesViewModel.kt @@ -18,31 +18,33 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class LibraryUpdatesViewModel @Inject constructor( - private val libraryUpdateService: LibraryUpdateService, - private val contextWrapper: ContextWrapper, - @Assisted standalone: Boolean, -) : ViewModel(contextWrapper) { - private val uiScope = if (standalone) { - MainScope() - } else { - null +class LibraryUpdatesViewModel + @Inject + constructor( + private val libraryUpdateService: LibraryUpdateService, + private val contextWrapper: ContextWrapper, + @Assisted standalone: Boolean, + ) : ViewModel(contextWrapper) { + private val uiScope = if (standalone) { + MainScope() + } else { + null + } + + override val scope: CoroutineScope + get() = uiScope ?: super.scope + + val serviceStatus = LibraryUpdateService.status.asStateFlow() + val updateStatus = LibraryUpdateService.updateStatus.asStateFlow() + + fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART) + + override fun onDispose() { + super.onDispose() + uiScope?.cancel() + } + + private companion object { + private val log = logging() + } } - - override val scope: CoroutineScope - get() = uiScope ?: super.scope - - val serviceStatus = LibraryUpdateService.status.asStateFlow() - val updateStatus = LibraryUpdateService.updateStatus.asStateFlow() - - fun restartLibraryUpdates() = startLibraryUpdatesService(contextWrapper, libraryUpdateService, Actions.RESTART) - - override fun onDispose() { - super.onDispose() - uiScope?.cancel() - } - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenu.kt index 97fb7775..dc56718b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenu.kt @@ -43,7 +43,11 @@ import ca.gosyer.jui.uicore.insets.systemBars import cafe.adriel.voyager.navigator.Navigator @Composable -fun SideMenu(modifier: Modifier, controller: DisplayController, navigator: Navigator) { +fun SideMenu( + modifier: Modifier, + controller: DisplayController, + navigator: Navigator, +) { Surface( Modifier.fillMaxHeight() .windowInsetsPadding( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenuItem.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenuItem.kt index a81c51fe..9cc7d9db 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenuItem.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/components/SideMenuItem.kt @@ -30,7 +30,11 @@ import ca.gosyer.jui.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen @Composable -fun SideMenuItem(selected: Boolean, topLevelMenu: Menu, newRoot: (Screen) -> Unit) { +fun SideMenuItem( + selected: Boolean, + topLevelMenu: Menu, + newRoot: (Screen) -> Unit, +) { SideMenuItem( selected, stringResource(topLevelMenu.textKey), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/more/MoreScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/more/MoreScreen.kt index e6d6dea3..ac5fd683 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/more/MoreScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/main/more/MoreScreen.kt @@ -13,7 +13,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class MoreScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreen.kt index b35b54ca..8683aa50 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreen.kt @@ -15,7 +15,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class MangaScreen(private val mangaId: Long) : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt index 4f5ab4ea..ef44b0d2 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/MangaScreenViewModel.kt @@ -56,327 +56,339 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class MangaScreenViewModel @Inject constructor( - private val dateHandler: DateHandler, - private val getManga: GetManga, - private val refreshManga: RefreshManga, - private val getChapters: GetChapters, - private val refreshChapters: RefreshChapters, - private val batchUpdateChapter: BatchUpdateChapter, - private val updateChapterMarkPreviousRead: UpdateChapterMarkPreviousRead, - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val deleteChapterDownload: DeleteChapterDownload, - private val getCategories: GetCategories, - private val getMangaCategories: GetMangaCategories, - private val addMangaToCategory: AddMangaToCategory, - private val removeMangaFromCategory: RemoveMangaFromCategory, - private val addMangaToLibrary: AddMangaToLibrary, - private val removeMangaFromLibrary: RemoveMangaFromLibrary, - private val batchChapterDownload: BatchChapterDownload, - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, -) : ViewModel(contextWrapper) { - private val _manga = MutableStateFlow(null) - val manga = _manga.asStateFlow() +class MangaScreenViewModel + @Inject + constructor( + private val dateHandler: DateHandler, + private val getManga: GetManga, + private val refreshManga: RefreshManga, + private val getChapters: GetChapters, + private val refreshChapters: RefreshChapters, + private val batchUpdateChapter: BatchUpdateChapter, + private val updateChapterMarkPreviousRead: UpdateChapterMarkPreviousRead, + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val deleteChapterDownload: DeleteChapterDownload, + private val getCategories: GetCategories, + private val getMangaCategories: GetMangaCategories, + private val addMangaToCategory: AddMangaToCategory, + private val removeMangaFromCategory: RemoveMangaFromCategory, + private val addMangaToLibrary: AddMangaToLibrary, + private val removeMangaFromLibrary: RemoveMangaFromLibrary, + private val batchChapterDownload: BatchChapterDownload, + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, + ) : ViewModel(contextWrapper) { + private val _manga = MutableStateFlow(null) + val manga = _manga.asStateFlow() - private val _chapters = MutableStateFlow>(persistentListOf()) - val chapters = _chapters.asStateFlow() + private val _chapters = MutableStateFlow>(persistentListOf()) + val chapters = _chapters.asStateFlow() - private val _selectedIds = MutableStateFlow>(persistentListOf()) - val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems -> - chapters.filter { it.isSelected(selecteditems) }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + private val _selectedIds = MutableStateFlow>(persistentListOf()) + val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems -> + chapters.filter { it.isSelected(selecteditems) }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val loadingManga = MutableStateFlow(true) - private val loadingChapters = MutableStateFlow(true) - val isLoading = combine(loadingManga, loadingChapters) { a, b -> a || b } - .stateIn(coroutineScope, SharingStarted.Eagerly, true) + private val loadingManga = MutableStateFlow(true) + private val loadingChapters = MutableStateFlow(true) + val isLoading = combine(loadingManga, loadingChapters) { a, b -> a || b } + .stateIn(coroutineScope, SharingStarted.Eagerly, true) - val categories = getCategories.asFlow(true) - .map { it.toImmutableList() } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Failed to get categories" } - } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - private val _mangaCategories = MutableStateFlow>(persistentListOf()) - val mangaCategories = _mangaCategories.asStateFlow() - - val categoriesExist = categories.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, true) - - val inActionMode = _selectedIds.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, false) - - private val chooseCategoriesFlow = MutableSharedFlow() - val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow()) - - private val reloadManga = MutableSharedFlow() - private val reloadChapters = MutableSharedFlow() - - val dateTimeFormatter = uiPreferences.dateFormat().changes() - .map { - dateHandler.getDateFormat(it) - } - .asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get())) - - init { - DownloadService.registerWatch(params.mangaId) - .mapLatest { downloadingChapters -> - chapters.value.forEach { chapter -> - chapter.updateFrom(downloadingChapters) - } - } - .launchIn(scope) - - reloadManga.onStart { emit(Unit) }.flatMapLatest { - loadingManga.value = true - getManga.asFlow(params.mangaId) - } - .onEach { - _manga.value = it - loadingManga.value = false - } + val categories = getCategories.asFlow(true) + .map { it.toImmutableList() } .catch { toast(it.message.orEmpty()) - loadingManga.value = false + log.warn(it) { "Failed to get categories" } } - .launchIn(scope) + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - reloadChapters.onStart { emit(Unit) }.flatMapLatest { - loadingChapters.value = true - getChapters.asFlow(params.mangaId) - } - .onEach { - _chapters.value = it.toDownloadChapters() - loadingChapters.value = false + private val _mangaCategories = MutableStateFlow>(persistentListOf()) + val mangaCategories = _mangaCategories.asStateFlow() + + val categoriesExist = categories.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, true) + + val inActionMode = _selectedIds.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, false) + + private val chooseCategoriesFlow = MutableSharedFlow() + val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow()) + + private val reloadManga = MutableSharedFlow() + private val reloadChapters = MutableSharedFlow() + + val dateTimeFormatter = uiPreferences.dateFormat().changes() + .map { + dateHandler.getDateFormat(it) } - .catch { - toast(it.message.orEmpty()) - loadingChapters.value = false - } - .launchIn(scope) + .asStateFlow(dateHandler.getDateFormat(uiPreferences.dateFormat().get())) - scope.launch { - val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) - if (mangaCategories != null) { - _mangaCategories.value = mangaCategories.toImmutableList() - } - } - } - - fun loadManga() { - scope.launch { - reloadManga.emit(Unit) - } - } - - fun loadChapters() { - scope.launch { - reloadChapters.emit(Unit) - } - } - - fun refreshManga() { - scope.launch { - loadingManga.value = true - refreshManga.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) - } - scope.launch { - loadingChapters.value = true - refreshChapters.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) - } - } - - fun setCategories() { - scope.launch { - manga.value ?: return@launch - chooseCategoriesFlow.emit(Unit) - } - } - - fun toggleFavorite() { - scope.launch { - manga.value?.let { manga -> - if (manga.inLibrary) { - removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) - } else { - if (categories.value.isEmpty()) { - addFavorite(emptyList(), emptyList()) - } else { - chooseCategoriesFlow.emit(Unit) + init { + DownloadService.registerWatch(params.mangaId) + .mapLatest { downloadingChapters -> + chapters.value.forEach { chapter -> + chapter.updateFrom(downloadingChapters) } } + .launchIn(scope) + + reloadManga.onStart { emit(Unit) }.flatMapLatest { + loadingManga.value = true + getManga.asFlow(params.mangaId) } - } - } - - fun addFavorite(categories: List, oldCategories: List) { - scope.launch { - manga.value?.let { manga -> - if (manga.inLibrary) { - if (oldCategories.isEmpty()) { - removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) - } else { - oldCategories.filterNot { it in categories }.forEach { - removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) - } - } - } else { - addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) + .onEach { + _manga.value = it + loadingManga.value = false } - if (categories.isEmpty()) { - addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) - } else { - categories.filterNot { it in oldCategories }.forEach { - addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) - } + .catch { + toast(it.message.orEmpty()) + loadingManga.value = false } + .launchIn(scope) - val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) }) + reloadChapters.onStart { emit(Unit) }.flatMapLatest { + loadingChapters.value = true + getChapters.asFlow(params.mangaId) + } + .onEach { + _chapters.value = it.toDownloadChapters() + loadingChapters.value = false + } + .catch { + toast(it.message.orEmpty()) + loadingChapters.value = false + } + .launchIn(scope) + + scope.launch { + val mangaCategories = getMangaCategories.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) if (mangaCategories != null) { _mangaCategories.value = mangaCategories.toImmutableList() } } } - } - private fun setRead(chapterIds: List, read: Boolean) { - scope.launch { - manga.value?.let { manga -> - batchUpdateChapter.await(manga, chapterIds, isRead = read, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() + fun loadManga() { + scope.launch { + reloadManga.emit(Unit) } } - } - fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - private fun setBookmarked(chapterIds: List, bookmark: Boolean) { - scope.launch { - manga.value?.let { manga -> - batchUpdateChapter.await(manga, chapterIds, isBookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() + fun loadChapters() { + scope.launch { + reloadChapters.emit(Unit) } } - } - fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - fun markPreviousRead(index: Int) { - scope.launch { - manga.value?.let { manga -> - updateChapterMarkPreviousRead.await(manga, index, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() + fun refreshManga() { + scope.launch { + loadingManga.value = true + refreshManga.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) + } + scope.launch { + loadingChapters.value = true + refreshChapters.await(params.mangaId, onError = { toast(it.message.orEmpty()) }) } } - } - fun downloadChapter(index: Int) { - manga.value?.let { manga -> - scope.launch { queueChapterDownload.await(manga, index, onError = { toast(it.message.orEmpty()) }) } + fun setCategories() { + scope.launch { + manga.value ?: return@launch + chooseCategoriesFlow.emit(Unit) + } } - } - fun deleteDownload(id: Long?) { - scope.launch { - if (id == null) { - val manga = _manga.value ?: return@launch - val chapterIds = _selectedIds.value - batchUpdateChapter.await(manga, chapterIds, delete = true, onError = { toast(it.message.orEmpty()) }) - selectedItems.value.forEach { - it.setNotDownloaded() + fun toggleFavorite() { + scope.launch { + manga.value?.let { manga -> + if (manga.inLibrary) { + removeMangaFromLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) + } else { + if (categories.value.isEmpty()) { + addFavorite(emptyList(), emptyList()) + } else { + chooseCategoriesFlow.emit(Unit) + } + } } - _selectedIds.value = persistentListOf() - } else { - chapters.value.find { it.chapter.id == id } - ?.deleteDownload(deleteChapterDownload) } } - } - fun stopDownloadingChapter(index: Int) { - scope.launch { - chapters.value.find { it.chapter.index == index } - ?.stopDownloading(stopChapterDownload) + fun addFavorite( + categories: List, + oldCategories: List, + ) { + scope.launch { + manga.value?.let { manga -> + if (manga.inLibrary) { + if (oldCategories.isEmpty()) { + removeMangaFromCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) + } else { + oldCategories.filterNot { it in categories }.forEach { + removeMangaFromCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) + } + } + } else { + addMangaToLibrary.await(manga, onError = { toast(it.message.orEmpty()) }) + } + if (categories.isEmpty()) { + addMangaToCategory.await(manga.id, 0, onError = { toast(it.message.orEmpty()) }) + } else { + categories.filterNot { it in oldCategories }.forEach { + addMangaToCategory.await(manga, it, onError = { toast(it.message.orEmpty()) }) + } + } + + val mangaCategories = getMangaCategories.await(manga.id, onError = { toast(it.message.orEmpty()) }) + if (mangaCategories != null) { + _mangaCategories.value = mangaCategories.toImmutableList() + } + } + } + } + + private fun setRead( + chapterIds: List, + read: Boolean, + ) { + scope.launch { + manga.value?.let { manga -> + batchUpdateChapter.await(manga, chapterIds, isRead = read, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + } + fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + private fun setBookmarked( + chapterIds: List, + bookmark: Boolean, + ) { + scope.launch { + manga.value?.let { manga -> + batchUpdateChapter.await(manga, chapterIds, isBookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + } + fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + fun markPreviousRead(index: Int) { + scope.launch { + manga.value?.let { manga -> + updateChapterMarkPreviousRead.await(manga, index, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + } + + fun downloadChapter(index: Int) { + manga.value?.let { manga -> + scope.launch { queueChapterDownload.await(manga, index, onError = { toast(it.message.orEmpty()) }) } + } + } + + fun deleteDownload(id: Long?) { + scope.launch { + if (id == null) { + val manga = _manga.value ?: return@launch + val chapterIds = _selectedIds.value + batchUpdateChapter.await(manga, chapterIds, delete = true, onError = { toast(it.message.orEmpty()) }) + selectedItems.value.forEach { + it.setNotDownloaded() + } + _selectedIds.value = persistentListOf() + } else { + chapters.value.find { it.chapter.id == id } + ?.deleteDownload(deleteChapterDownload) + } + } + } + + fun stopDownloadingChapter(index: Int) { + scope.launch { + chapters.value.find { it.chapter.index == index } + ?.stopDownloading(stopChapterDownload) + } + } + + fun selectAll() { + scope.launch { + _selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList() + } + } + + fun invertSelection() { + scope.launch { + _selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList() + } + } + + fun selectChapter(id: Long) { + scope.launch { + _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() + } + } + fun unselectChapter(id: Long) { + scope.launch { + _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() + } + } + + fun clearSelection() { + scope.launch { + _selectedIds.value = persistentListOf() + } + } + + fun downloadChapters() { + scope.launch { + batchChapterDownload.await(_selectedIds.value) + _selectedIds.value = persistentListOf() + } + } + + fun downloadNext(next: Int) { + scope.launch { + batchChapterDownload.await( + _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id } + .takeLast(next), + ) + } + } + + fun downloadUnread() { + scope.launch { + batchChapterDownload.await( + _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id }, + ) + } + } + + fun downloadAll() { + scope.launch { + batchChapterDownload.await( + _chapters.value + .filter { it.downloadState.value == ChapterDownloadState.NotDownloaded } + .map { it.chapter.id }, + ) + } + } + + private fun List.toDownloadChapters() = + map { + ChapterDownloadItem(null, it) + }.toImmutableList() + + data class Params(val mangaId: Long) + + private companion object { + private val log = logging() } } - - fun selectAll() { - scope.launch { - _selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList() - } - } - - fun invertSelection() { - scope.launch { - _selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList() - } - } - - fun selectChapter(id: Long) { - scope.launch { - _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() - } - } - fun unselectChapter(id: Long) { - scope.launch { - _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() - } - } - - fun clearSelection() { - scope.launch { - _selectedIds.value = persistentListOf() - } - } - - fun downloadChapters() { - scope.launch { - batchChapterDownload.await(_selectedIds.value) - _selectedIds.value = persistentListOf() - } - } - - fun downloadNext(next: Int) { - scope.launch { - batchChapterDownload.await( - _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id } - .takeLast(next), - ) - } - } - - fun downloadUnread() { - scope.launch { - batchChapterDownload.await( - _chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id }, - ) - } - } - - fun downloadAll() { - scope.launch { - batchChapterDownload.await( - _chapters.value - .filter { it.downloadState.value == ChapterDownloadState.NotDownloaded } - .map { it.chapter.id }, - ) - } - } - - private fun List.toDownloadChapters() = map { - ChapterDownloadItem(null, it) - }.toImmutableList() - - data class Params(val mangaId: Long) - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt index b8e0bc82..00b18bb6 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/manga/components/MangaMenu.kt @@ -73,7 +73,10 @@ fun MangaItem(manga: Manga) { } @Composable -private fun Cover(manga: Manga, modifier: Modifier = Modifier) { +private fun Cover( + manga: Manga, + modifier: Modifier = Modifier, +) { ImageLoaderImage( data = manga, contentDescription = manga.title, @@ -88,7 +91,10 @@ private fun Cover(manga: Manga, modifier: Modifier = Modifier) { } @Composable -private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) { +private fun MangaInfo( + manga: Manga, + modifier: Modifier = Modifier, +) { SelectionContainer { Column(modifier) { Text( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt index 5b6ac7f1..47adb045 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenu.kt @@ -535,7 +535,10 @@ fun ReaderLayout( } @Composable -fun SideMenuButton(sideMenuOpen: Boolean, onOpenSideMenuClicked: () -> Unit) { +fun SideMenuButton( + sideMenuOpen: Boolean, + onOpenSideMenuClicked: () -> Unit, +) { AnimatedVisibility( !sideMenuOpen, enter = fadeIn() + slideInHorizontally(), @@ -645,19 +648,21 @@ fun ChapterSeparator( } } -fun NavigationMode.toNavigation() = when (this) { - NavigationMode.Disabled -> null - NavigationMode.RightAndLeftNavigation -> RightAndLeftNavigation() - NavigationMode.KindlishNavigation -> KindlishNavigation() - NavigationMode.LNavigation -> LNavigation() - NavigationMode.EdgeNavigation -> EdgeNavigation() -} +fun NavigationMode.toNavigation() = + when (this) { + NavigationMode.Disabled -> null + NavigationMode.RightAndLeftNavigation -> RightAndLeftNavigation() + NavigationMode.KindlishNavigation -> KindlishNavigation() + NavigationMode.LNavigation -> LNavigation() + NavigationMode.EdgeNavigation -> EdgeNavigation() + } -fun ImageScale.toContentScale() = when (this) { - ImageScale.FitScreen -> ContentScale.Inside - ImageScale.FitHeight -> ContentScale.FillHeight - ImageScale.FitWidth -> ContentScale.FillWidth - ImageScale.OriginalSize -> ContentScale.None - ImageScale.SmartFit -> ContentScale.Fit - ImageScale.Stretch -> ContentScale.FillBounds -} +fun ImageScale.toContentScale() = + when (this) { + ImageScale.FitScreen -> ContentScale.Inside + ImageScale.FitHeight -> ContentScale.FillHeight + ImageScale.FitWidth -> ContentScale.FillWidth + ImageScale.OriginalSize -> ContentScale.None + ImageScale.SmartFit -> ContentScale.Fit + ImageScale.Stretch -> ContentScale.FillBounds + } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt index b4367caf..de422433 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderMenuViewModel.kt @@ -69,374 +69,379 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class ReaderMenuViewModel @Inject constructor( - private val readerPreferences: ReaderPreferences, - private val getManga: GetManga, - private val getChapters: GetChapters, - private val getChapter: GetChapter, - private val getChapterPage: GetChapterPage, - private val updateChapterRead: UpdateChapterRead, - private val updateChapterLastPageRead: UpdateChapterLastPageRead, - private val updateMangaMeta: UpdateMangaMeta, - private val updateChapterMeta: UpdateChapterMeta, - private val chapterCache: ChapterCache, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, -) : ViewModel(contextWrapper) { - override val scope = MainScope() - private val _manga = MutableStateFlow(null) - private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null)) - val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null) - val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null) - val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null) +class ReaderMenuViewModel + @Inject + constructor( + private val readerPreferences: ReaderPreferences, + private val getManga: GetManga, + private val getChapters: GetChapters, + private val getChapter: GetChapter, + private val getChapterPage: GetChapterPage, + private val updateChapterRead: UpdateChapterRead, + private val updateChapterLastPageRead: UpdateChapterLastPageRead, + private val updateMangaMeta: UpdateMangaMeta, + private val updateChapterMeta: UpdateChapterMeta, + private val chapterCache: ChapterCache, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, + ) : ViewModel(contextWrapper) { + override val scope = MainScope() + private val _manga = MutableStateFlow(null) + private val viewerChapters = MutableStateFlow(ViewerChapters(null, null, null)) + val previousChapter = viewerChapters.map { it.prevChapter }.stateIn(scope, SharingStarted.Eagerly, null) + val chapter = viewerChapters.map { it.currChapter }.stateIn(scope, SharingStarted.Eagerly, null) + val nextChapter = viewerChapters.map { it.nextChapter }.stateIn(scope, SharingStarted.Eagerly, null) - private val _state = MutableStateFlow(ReaderChapter.State.Wait) - val state = _state.asStateFlow() + private val _state = MutableStateFlow(ReaderChapter.State.Wait) + val state = _state.asStateFlow() - val pages = viewerChapters.flatMapLatest { viewerChapters -> - val previousChapterPages = viewerChapters.prevChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - val chapterPages = viewerChapters.currChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - val nextChapterPages = viewerChapters.nextChapter - ?.pages - ?.map { (it as? PagesState.Success)?.pages } - ?: flowOf(null) - combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next -> - ( - prev.orEmpty() + - ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) + - cur.orEmpty() + - ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) + - next.orEmpty() - ).toImmutableList() + val pages = viewerChapters.flatMapLatest { viewerChapters -> + val previousChapterPages = viewerChapters.prevChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + val chapterPages = viewerChapters.currChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + val nextChapterPages = viewerChapters.nextChapter + ?.pages + ?.map { (it as? PagesState.Success)?.pages } + ?: flowOf(null) + combine(previousChapterPages, chapterPages, nextChapterPages) { prev, cur, next -> + ( + prev.orEmpty() + + ReaderPageSeparator(viewerChapters.prevChapter, viewerChapters.currChapter) + + cur.orEmpty() + + ReaderPageSeparator(viewerChapters.currChapter, viewerChapters.nextChapter) + + next.orEmpty() + ).toImmutableList() + } + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + private val _currentPage = MutableStateFlow(null) + val currentPage = _currentPage.asStateFlow() + + private val _currentPageOffset = MutableStateFlow(1) + val currentPageOffset = _currentPageOffset.asStateFlow() + + private val _readerSettingsMenuOpen = MutableStateFlow(false) + val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow() + + private val _pageEmitter = MutableSharedFlow() + val pageEmitter = StableHolder(_pageEmitter.asSharedFlow()) + + val readerModes = readerPreferences.modes() + .getAsFlow() + .map { it.toImmutableList() } + .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga -> + val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent() + if ( + mangaMode != null && + mangaMode != MangaMeta.DEFAULT_READER_MODE && + mangaMode in readerModes.value + ) { + mangaMode + } else { + mode + } + }.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get()) + + val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode) + + private val loader = ChapterLoader( + readerPreferences = readerPreferences, + getChapterPage = getChapterPage, + chapterCache = chapterCache, + bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper), + ) + + init { + init() } - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val _currentPage = MutableStateFlow(null) - val currentPage = _currentPage.asStateFlow() - - private val _currentPageOffset = MutableStateFlow(1) - val currentPageOffset = _currentPageOffset.asStateFlow() - - private val _readerSettingsMenuOpen = MutableStateFlow(false) - val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow() - - private val _pageEmitter = MutableSharedFlow() - val pageEmitter = StableHolder(_pageEmitter.asSharedFlow()) - - val readerModes = readerPreferences.modes() - .getAsFlow() - .map { it.toImmutableList() } - .stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga -> - val mangaMode = manga?.meta?.juiReaderMode?.decodeURLQueryComponent() - if ( - mangaMode != null && - mangaMode != MangaMeta.DEFAULT_READER_MODE && - mangaMode in readerModes.value - ) { - mangaMode - } else { - mode + fun init() { + scope.launchDefault { + runCatching { + initManga(params.mangaId) + initChapters(params.mangaId, params.chapterIndex) + } + } } - }.stateIn(scope, SharingStarted.Eagerly, readerPreferences.mode().get()) - val readerModeSettings = ReaderModeWatch(readerPreferences, scope, readerMode) + init { + scope.launchDefault { + currentPage + .filterIsInstance() + .collectLatest { page -> + page.chapter.pageLoader?.loadPage(page) + if (page.chapter == chapter.value) { + if ((page.index + 1) >= page.chapter.chapter.pageCount!!) { + markChapterRead(page.chapter) + } + val nextChapter = nextChapter.value + if (nextChapter != null && (page.index + 1) >= (page.chapter.chapter.pageCount!! - 5)) { + requestPreloadChapter(nextChapter) + } + } else { + val previousChapter = previousChapter.value + val nextChapter = nextChapter.value + if (page.chapter == previousChapter) { + viewerChapters.value = viewerChapters.value.movePrev() + initChapters(params.mangaId, page.chapter.chapter.index, fromMenuButton = false) + } else if (page.chapter == nextChapter) { + viewerChapters.value = viewerChapters.value.moveNext() + initChapters(params.mangaId, page.chapter.chapter.index, fromMenuButton = false) + } + } + } + } + } - private val loader = ChapterLoader( - readerPreferences = readerPreferences, - getChapterPage = getChapterPage, - chapterCache = chapterCache, - bitmapDecoderFactory = BitmapDecoderFactory(contextWrapper), - ) + fun navigate(navigationRegion: Navigation): Boolean { + scope.launch { + val moveTo = when (navigationRegion) { + Navigation.MENU -> { + setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value) + null + } + Navigation.NEXT -> MoveTo.Next + Navigation.PREV -> MoveTo.Previous + Navigation.RIGHT -> when (readerModeSettings.direction.value) { + Direction.Left -> MoveTo.Previous + else -> MoveTo.Next + } + Navigation.LEFT -> when (readerModeSettings.direction.value) { + Direction.Left -> MoveTo.Next + else -> MoveTo.Previous + } + Navigation.DOWN -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Previous + else -> MoveTo.Next + } + Navigation.UP -> when (readerModeSettings.direction.value) { + Direction.Up -> MoveTo.Next + else -> MoveTo.Previous + } + } + if (moveTo != null) { + _pageEmitter.emit(PageMove.Direction(moveTo)) + } + } + return true + } - init { - init() - } + fun navigate(page: Int) { + log.info { "Navigate to $page" } + scope.launch { + _pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch)) + } + } - fun init() { - scope.launchDefault { - runCatching { + fun progress(page: ReaderItem) { + log.info { "Progressed to $page" } + _currentPage.value = page + } + + fun retry(page: ReaderPage) { + log.info { "Retrying ${page.index}" } + chapter.value?.pageLoader?.retryPage(page) + } + + fun setMangaReaderMode(mode: String) { + scope.launchDefault { + _manga.value?.let { + updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) }) + } initManga(params.mangaId) - initChapters(params.mangaId, params.chapterIndex) } } - } - init { - scope.launchDefault { - currentPage - .filterIsInstance() - .collectLatest { page -> - page.chapter.pageLoader?.loadPage(page) - if (page.chapter == chapter.value) { - if ((page.index + 1) >= page.chapter.chapter.pageCount!!) { - markChapterRead(page.chapter) - } - val nextChapter = nextChapter.value - if (nextChapter != null && (page.index + 1) >= (page.chapter.chapter.pageCount!! - 5)) { - requestPreloadChapter(nextChapter) - } - } else { - val previousChapter = previousChapter.value - val nextChapter = nextChapter.value - if (page.chapter == previousChapter) { - viewerChapters.value = viewerChapters.value.movePrev() - initChapters(params.mangaId, page.chapter.chapter.index, fromMenuButton = false) - } else if (page.chapter == nextChapter) { - viewerChapters.value = viewerChapters.value.moveNext() - initChapters(params.mangaId, page.chapter.chapter.index, fromMenuButton = false) - } - } - } + fun setReaderSettingsMenuOpen(open: Boolean) { + _readerSettingsMenuOpen.value = open } - } - fun navigate(navigationRegion: Navigation): Boolean { - scope.launch { - val moveTo = when (navigationRegion) { - Navigation.MENU -> { - setReaderSettingsMenuOpen(!readerSettingsMenuOpen.value) - null + fun prevChapter() { + scope.launchDefault { + val prevChapter = previousChapter.value ?: return@launchDefault + try { + _state.value = ReaderChapter.State.Wait + sendProgress() + viewerChapters.value = viewerChapters.value.movePrev() + initChapters(params.mangaId, prevChapter.chapter.index, fromMenuButton = true) + } catch (e: Exception) { + log.warn(e) { "Error loading prev chapter" } } - Navigation.NEXT -> MoveTo.Next - Navigation.PREV -> MoveTo.Previous - Navigation.RIGHT -> when (readerModeSettings.direction.value) { - Direction.Left -> MoveTo.Previous - else -> MoveTo.Next - } - Navigation.LEFT -> when (readerModeSettings.direction.value) { - Direction.Left -> MoveTo.Next - else -> MoveTo.Previous - } - Navigation.DOWN -> when (readerModeSettings.direction.value) { - Direction.Up -> MoveTo.Previous - else -> MoveTo.Next - } - Navigation.UP -> when (readerModeSettings.direction.value) { - Direction.Up -> MoveTo.Next - else -> MoveTo.Previous - } - } - if (moveTo != null) { - _pageEmitter.emit(PageMove.Direction(moveTo)) } } - return true - } - fun navigate(page: Int) { - log.info { "Navigate to $page" } - scope.launch { - _pageEmitter.emit(PageMove.Page(pages.value.getOrNull(page) ?: return@launch)) - } - } - - fun progress(page: ReaderItem) { - log.info { "Progressed to $page" } - _currentPage.value = page - } - - fun retry(page: ReaderPage) { - log.info { "Retrying ${page.index}" } - chapter.value?.pageLoader?.retryPage(page) - } - - fun setMangaReaderMode(mode: String) { - scope.launchDefault { - _manga.value?.let { - updateMangaMeta.await(it, mode, onError = { toast(it.message.orEmpty()) }) - } - initManga(params.mangaId) - } - } - - fun setReaderSettingsMenuOpen(open: Boolean) { - _readerSettingsMenuOpen.value = open - } - - fun prevChapter() { - scope.launchDefault { - val prevChapter = previousChapter.value ?: return@launchDefault - try { - _state.value = ReaderChapter.State.Wait - sendProgress() - viewerChapters.value = viewerChapters.value.movePrev() - initChapters(params.mangaId, prevChapter.chapter.index, fromMenuButton = true) - } catch (e: Exception) { - log.warn(e) { "Error loading prev chapter" } - } - } - } - - fun nextChapter() { - scope.launchDefault { - val nextChapter = nextChapter.value ?: return@launchDefault - try { - _state.value = ReaderChapter.State.Wait - sendProgress() - viewerChapters.value = viewerChapters.value.moveNext() - initChapters(params.mangaId, nextChapter.chapter.index, fromMenuButton = true) - } catch (e: Exception) { - log.warn(e) { "Error loading next chapter" } - } - } - } - - private suspend fun initManga(mangaId: Long) { - getManga.asFlow(mangaId) - .take(1) - .onEach { - _manga.value = it - } - .catch { - _state.value = ReaderChapter.State.Error(it) - log.warn(it) { "Error loading manga" } - } - .collect() - } - - private suspend fun initChapters( - mangaId: Long, - chapterIndex: Int, - fromMenuButton: Boolean = true, - ) { - log.debug { "Loading chapter index $chapterIndex" } - val (chapter, pages) = coroutineScope { - val getCurrentChapter = async { - val chapter = getReaderChapter(chapterIndex) ?: return@async null - val pages = loader.loadChapter(chapter) - viewerChapters.update { it.copy(currChapter = chapter) } - chapter to pages - } - - val getAdjacentChapters = async { - val chapters = getChapters.await( - mangaId, - onError = { /* TODO: 2022-07-01 Error toast */ }, - ).orEmpty() - - val nextChapter = async { - if (viewerChapters.value.nextChapter == null) { - val nextChapter = chapters.find { it.index == chapterIndex + 1 } - if (nextChapter != null) { - val nextReaderChapter = getReaderChapter(nextChapter.index) - viewerChapters.update { it.copy(nextChapter = nextReaderChapter) } - } else { - viewerChapters.update { it.copy(nextChapter = null) } - } - } + fun nextChapter() { + scope.launchDefault { + val nextChapter = nextChapter.value ?: return@launchDefault + try { + _state.value = ReaderChapter.State.Wait + sendProgress() + viewerChapters.value = viewerChapters.value.moveNext() + initChapters(params.mangaId, nextChapter.chapter.index, fromMenuButton = true) + } catch (e: Exception) { + log.warn(e) { "Error loading next chapter" } } - val prevChapter = async { - if (viewerChapters.value.prevChapter == null) { - val prevChapter = chapters.find { it.index == chapterIndex - 1 } - if (prevChapter != null) { - val prevReaderChapter = getReaderChapter(prevChapter.index) - viewerChapters.update { it.copy(prevChapter = prevReaderChapter) } - } else { - viewerChapters.update { it.copy(prevChapter = null) } - } - } - } - nextChapter.await() - prevChapter.await() } - - getAdjacentChapters.await() - getCurrentChapter.await() - } ?: return - - if (fromMenuButton) { - chapter.stateObserver - .onEach { - _state.value = it - } - .launchIn(chapter.scope) - - pages - .filterIsInstance() - .onEach { (pageList) -> - val lastPageReadOffset = chapter.chapter.meta.juiPageOffset - if (lastPageReadOffset != 0) { - _currentPageOffset.value = lastPageReadOffset - } - val lastPageRead = chapter.chapter.lastPageRead - _currentPage.value = if (lastPageRead > 0) { - pageList[lastPageRead.coerceAtMost(pageList.lastIndex)] - } else { - pageList.first() - }.also { chapter.pageLoader?.loadPage(it) } - } - .launchIn(chapter.scope) } - } - private suspend fun getReaderChapter(chapterIndex: Int): ReaderChapter? { - return ReaderChapter( - getChapter.asFlow(params.mangaId, chapterIndex) + private suspend fun initManga(mangaId: Long) { + getManga.asFlow(mangaId) .take(1) + .onEach { + _manga.value = it + } .catch { _state.value = ReaderChapter.State.Error(it) - log.warn(it) { "Error getting chapter $chapterIndex" } + log.warn(it) { "Error loading manga" } } - .singleOrNull() ?: return null, - ) - } - - fun requestPreloadChapter(chapter: ReaderChapter) { - if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { - return + .collect() } - log.debug { "Preloading ${chapter.chapter.index}" } - loader.loadChapter(chapter) - } - private fun markChapterRead(chapter: ReaderChapter) { - scope.launch { - updateChapterRead.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) }) + private suspend fun initChapters( + mangaId: Long, + chapterIndex: Int, + fromMenuButton: Boolean = true, + ) { + log.debug { "Loading chapter index $chapterIndex" } + val (chapter, pages) = coroutineScope { + val getCurrentChapter = async { + val chapter = getReaderChapter(chapterIndex) ?: return@async null + val pages = loader.loadChapter(chapter) + viewerChapters.update { it.copy(currChapter = chapter) } + chapter to pages + } + + val getAdjacentChapters = async { + val chapters = getChapters.await( + mangaId, + onError = { /* TODO: 2022-07-01 Error toast */ }, + ).orEmpty() + + val nextChapter = async { + if (viewerChapters.value.nextChapter == null) { + val nextChapter = chapters.find { it.index == chapterIndex + 1 } + if (nextChapter != null) { + val nextReaderChapter = getReaderChapter(nextChapter.index) + viewerChapters.update { it.copy(nextChapter = nextReaderChapter) } + } else { + viewerChapters.update { it.copy(nextChapter = null) } + } + } + } + val prevChapter = async { + if (viewerChapters.value.prevChapter == null) { + val prevChapter = chapters.find { it.index == chapterIndex - 1 } + if (prevChapter != null) { + val prevReaderChapter = getReaderChapter(prevChapter.index) + viewerChapters.update { it.copy(prevChapter = prevReaderChapter) } + } else { + viewerChapters.update { it.copy(prevChapter = null) } + } + } + } + nextChapter.await() + prevChapter.await() + } + + getAdjacentChapters.await() + getCurrentChapter.await() + } ?: return + + if (fromMenuButton) { + chapter.stateObserver + .onEach { + _state.value = it + } + .launchIn(chapter.scope) + + pages + .filterIsInstance() + .onEach { (pageList) -> + val lastPageReadOffset = chapter.chapter.meta.juiPageOffset + if (lastPageReadOffset != 0) { + _currentPageOffset.value = lastPageReadOffset + } + val lastPageRead = chapter.chapter.lastPageRead + _currentPage.value = if (lastPageRead > 0) { + pageList[lastPageRead.coerceAtMost(pageList.lastIndex)] + } else { + pageList.first() + }.also { chapter.pageLoader?.loadPage(it) } + } + .launchIn(chapter.scope) + } } - } - @OptIn(DelicateCoroutinesApi::class) - fun sendProgress( - chapter: Chapter? = this.chapter.value?.chapter, - lastPageRead: Int = (currentPage.value as? ReaderPage)?.index ?: 0, - ) { - chapter ?: return - if (chapter.read) return - GlobalScope.launch { - updateChapterLastPageRead.await( - chapter, - lastPageRead = lastPageRead, - onError = { toast(it.message.orEmpty()) }, + private suspend fun getReaderChapter(chapterIndex: Int): ReaderChapter? { + return ReaderChapter( + getChapter.asFlow(params.mangaId, chapterIndex) + .take(1) + .catch { + _state.value = ReaderChapter.State.Error(it) + log.warn(it) { "Error getting chapter $chapterIndex" } + } + .singleOrNull() ?: return null, ) } - } - fun updateLastPageReadOffset(offset: Int) { - updateLastPageReadOffset(chapter.value?.chapter ?: return, offset) - } + fun requestPreloadChapter(chapter: ReaderChapter) { + if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) { + return + } + log.debug { "Preloading ${chapter.chapter.index}" } + loader.loadChapter(chapter) + } - @OptIn(DelicateCoroutinesApi::class) - private fun updateLastPageReadOffset(chapter: Chapter, offset: Int) { - GlobalScope.launch { - updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) }) + private fun markChapterRead(chapter: ReaderChapter) { + scope.launch { + updateChapterRead.await(chapter.chapter, read = true, onError = { toast(it.message.orEmpty()) }) + } + } + + @OptIn(DelicateCoroutinesApi::class) + fun sendProgress( + chapter: Chapter? = this.chapter.value?.chapter, + lastPageRead: Int = (currentPage.value as? ReaderPage)?.index ?: 0, + ) { + chapter ?: return + if (chapter.read) return + GlobalScope.launch { + updateChapterLastPageRead.await( + chapter, + lastPageRead = lastPageRead, + onError = { toast(it.message.orEmpty()) }, + ) + } + } + + fun updateLastPageReadOffset(offset: Int) { + updateLastPageReadOffset(chapter.value?.chapter ?: return, offset) + } + + @OptIn(DelicateCoroutinesApi::class) + private fun updateLastPageReadOffset( + chapter: Chapter, + offset: Int, + ) { + GlobalScope.launch { + updateChapterMeta.await(chapter, offset, onError = { toast(it.message.orEmpty()) }) + } + } + + override fun onDispose() { + viewerChapters.value.recycle() + scope.cancel() + } + + data class Params(val chapterIndex: Int, val mangaId: Long) + + private companion object { + private val log = logging() } } - - override fun onDispose() { - viewerChapters.value.recycle() - scope.cancel() - } - - data class Params(val chapterIndex: Int, val mangaId: Long) - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderSideMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderSideMenu.kt index 14b1d110..2e385d32 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderSideMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/ReaderSideMenu.kt @@ -234,7 +234,11 @@ fun ReaderSheet( } @Composable -fun ReaderModeSetting(readerModes: ImmutableList, selectedMode: String, onSetReaderMode: (String) -> Unit) { +fun ReaderModeSetting( + readerModes: ImmutableList, + selectedMode: String, + onSetReaderMode: (String) -> Unit, +) { val modes = remember { persistentListOf(MangaMeta.DEFAULT_READER_MODE) + readerModes } val defaultModeString = stringResource(MR.strings.default_reader_mode) val displayModes = remember { modes.replace(0, defaultModeString).toPersistentList() } @@ -305,7 +309,10 @@ private fun ReaderProgressSlider( } @Composable -private fun NavigateChapters(loadPrevChapter: () -> Unit, loadNextChapter: () -> Unit) { +private fun NavigateChapters( + loadPrevChapter: () -> Unit, + loadNextChapter: () -> Unit, +) { Divider(Modifier.padding(horizontal = 4.dp, vertical = 8.dp)) Row(horizontalArrangement = Arrangement.SpaceBetween) { OutlinedButton(loadPrevChapter, Modifier.weight(0.5F)) { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/PageLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/PageLoader.kt index 851303e5..ba809145 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/PageLoader.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/PageLoader.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.StateFlow * method [recycle] is called. */ abstract class PageLoader { - /** * Whether this loader has been already recycled. */ diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt index 0f8f62da..aa75161d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/loader/TachideskPageLoader.kt @@ -130,7 +130,10 @@ class TachideskPageLoader( .collect() } - private suspend fun putImageInCache(response: HttpResponse, page: ReaderPage) { + private suspend fun putImageInCache( + response: HttpResponse, + page: ReaderPage, + ) { val editor = chapterCache.edit(page.cacheKey) ?: throw Exception("Couldn't open cache") try { @@ -182,7 +185,10 @@ class TachideskPageLoader( * Preloads the given [amount] of pages after the [currentPage] with a lower priority. * @return a list of [PriorityPage] that were added to the [channel] */ - private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List { + private fun preloadNextPages( + currentPage: ReaderPage, + amount: Int, + ): List { val pageIndex = currentPage.index val pages = (currentPage.chapter.pages.value as? PagesState.Success)?.pages ?: return emptyList() if (pageIndex >= pages.lastIndex) return emptyList() @@ -295,6 +301,7 @@ class TachideskPageLoader( private fun DiskCache.Editor.abortQuietly() { try { abort() - } catch (_: Exception) {} + } catch (_: Exception) { + } } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/EdgeNavigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/EdgeNavigation.kt index 8a42d409..ebf735fb 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/EdgeNavigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/EdgeNavigation.kt @@ -21,7 +21,6 @@ import ca.gosyer.jui.ui.reader.model.Navigation */ @Immutable class EdgeNavigation : ViewerNavigation() { - override var regions: List = listOf( Region( rect = Rect(0, 0, 33, 100), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/KindlishNavigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/KindlishNavigation.kt index f0f4f084..84b8bc43 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/KindlishNavigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/KindlishNavigation.kt @@ -21,7 +21,6 @@ import ca.gosyer.jui.ui.reader.model.Navigation */ @Immutable class KindlishNavigation : ViewerNavigation() { - override var regions: List = listOf( Region( rect = Rect(33, 33, 100, 100), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/LNavigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/LNavigation.kt index b9bea9b7..54bf1be9 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/LNavigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/LNavigation.kt @@ -21,7 +21,6 @@ import ca.gosyer.jui.ui.reader.model.Navigation */ @Immutable open class LNavigation : ViewerNavigation() { - override var regions: List = listOf( Region( rect = Rect(0, 33, 33, 66), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/NavigationClickable.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/NavigationClickable.kt index cb1b63fb..62da556b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/NavigationClickable.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/NavigationClickable.kt @@ -48,35 +48,36 @@ fun Modifier.navigationClickable( onClickLabel: String? = null, role: Role? = null, onClick: (Navigation) -> Unit, -): Modifier = composed( - inspectorInfo = debugInspectorInfo { - name = "navigationClickable" - properties["navigation"] = navigation - properties["enabled"] = enabled - properties["onClickLabel"] = onClickLabel - properties["role"] = role - properties["onClick"] = onClick - properties["interactionSource"] = interactionSource - }, -) { - val offsetEvent = remember { MutableStateFlow(null) } - val layoutSize = remember { MutableStateFlow(Size.Zero) } - this - .clickable(interactionSource, null, enabled, onClickLabel, role) { - val offset = offsetEvent.value ?: return@clickable - val size = layoutSize.value - if (offset in size) { - onClick(navigation.getAction(offset, size)) - } - } - .pointerInput(interactionSource) { - forEachGesture { - awaitPointerEventScope { - offsetEvent.value = awaitFirstDown().position +): Modifier = + composed( + inspectorInfo = debugInspectorInfo { + name = "navigationClickable" + properties["navigation"] = navigation + properties["enabled"] = enabled + properties["onClickLabel"] = onClickLabel + properties["role"] = role + properties["onClick"] = onClick + properties["interactionSource"] = interactionSource + }, + ) { + val offsetEvent = remember { MutableStateFlow(null) } + val layoutSize = remember { MutableStateFlow(Size.Zero) } + this + .clickable(interactionSource, null, enabled, onClickLabel, role) { + val offset = offsetEvent.value ?: return@clickable + val size = layoutSize.value + if (offset in size) { + onClick(navigation.getAction(offset, size)) } } - } - .onGloballyPositioned { - layoutSize.value = it.size.toSize() - } -} + .pointerInput(interactionSource) { + forEachGesture { + awaitPointerEventScope { + offsetEvent.value = awaitFirstDown().position + } + } + } + .onGloballyPositioned { + layoutSize.value = it.size.toSize() + } + } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/RightAndLeftNavigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/RightAndLeftNavigation.kt index 91d78748..743c72fd 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/RightAndLeftNavigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/RightAndLeftNavigation.kt @@ -21,7 +21,6 @@ import ca.gosyer.jui.ui.reader.model.Navigation */ @Immutable class RightAndLeftNavigation : ViewerNavigation() { - override var regions: List = listOf( Region( rect = Rect(0, 0, 33, 100), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/ViewerNavigation.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/ViewerNavigation.kt index b93aa795..7df62dc4 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/ViewerNavigation.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/reader/navigation/ViewerNavigation.kt @@ -57,7 +57,10 @@ abstract class ViewerNavigation { var invertMode: TappingInvertMode = TappingInvertMode.NONE - fun getAction(pos: Offset, layoutSize: Size): Navigation { + fun getAction( + pos: Offset, + layoutSize: Size, + ): Navigation { val realX = pos.x / (layoutSize.width * 0.01F) val realY = pos.y / (layoutSize.height * 0.01F) val realPos = IntOffset(realX.toInt(), realY.toInt()) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt index cdf07866..d85e9d91 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAdvancedScreen.kt @@ -73,44 +73,46 @@ class SettingsAdvancedScreen : Screen { } } -class SettingsAdvancedViewModel @Inject constructor( - updatePreferences: UpdatePreferences, - private val imageCache: ImageCache, - private val chapterCache: ChapterCache, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - val updatesEnabled = updatePreferences.enabled().asStateFlow() +class SettingsAdvancedViewModel + @Inject + constructor( + updatePreferences: UpdatePreferences, + private val imageCache: ImageCache, + private val chapterCache: ChapterCache, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val updatesEnabled = updatePreferences.enabled().asStateFlow() - val imageCacheSize = flow { - while (currentCoroutineContext().isActive) { - emit(imageCache.size.bytesIntoHumanReadable()) - delay(1.seconds) + val imageCacheSize = flow { + while (currentCoroutineContext().isActive) { + emit(imageCache.size.bytesIntoHumanReadable()) + delay(1.seconds) + } + }.stateIn(scope, SharingStarted.Eagerly, "") + + val chapterCacheSize = flow { + while (currentCoroutineContext().isActive) { + emit(chapterCache.size.bytesIntoHumanReadable()) + delay(1.seconds) + } + }.stateIn(scope, SharingStarted.Eagerly, "") + + fun clearImageCache() { + scope.launchIO { + imageCache.clear() + } } - }.stateIn(scope, SharingStarted.Eagerly, "") - val chapterCacheSize = flow { - while (currentCoroutineContext().isActive) { - emit(chapterCache.size.bytesIntoHumanReadable()) - delay(1.seconds) + fun clearChapterCache() { + scope.launchIO { + chapterCache.clear() + } } - }.stateIn(scope, SharingStarted.Eagerly, "") - - fun clearImageCache() { - scope.launchIO { - imageCache.clear() + companion object { + private val log = logging() } } - fun clearChapterCache() { - scope.launchIO { - chapterCache.clear() - } - } - companion object { - private val log = logging() - } -} - @Composable fun SettingsAdvancedScreenContent( updatesEnabled: PreferenceMutableStateFlow, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt index cdb23bea..b784c179 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsAppearanceScreen.kt @@ -108,24 +108,25 @@ class SettingsAppearanceScreen : Screen { } } -class ThemesViewModel @Inject constructor( - private val uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { +class ThemesViewModel + @Inject + constructor( + private val uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val themeMode = uiPreferences.themeMode().asStateFlow() + val lightTheme = uiPreferences.lightTheme().asStateFlow() + val darkTheme = uiPreferences.darkTheme().asStateFlow() + val lightColors = uiPreferences.getLightColors().asStateFlow(scope) + val darkColors = uiPreferences.getDarkColors().asStateFlow(scope) - val themeMode = uiPreferences.themeMode().asStateFlow() - val lightTheme = uiPreferences.lightTheme().asStateFlow() - val darkTheme = uiPreferences.darkTheme().asStateFlow() - val lightColors = uiPreferences.getLightColors().asStateFlow(scope) - val darkColors = uiPreferences.getDarkColors().asStateFlow(scope) + val windowDecorations = uiPreferences.windowDecorations().asStateFlow() - val windowDecorations = uiPreferences.windowDecorations().asStateFlow() - - @Composable - fun getActiveColors(): AppColorsPreferenceState { - return if (MaterialTheme.colors.isLight) lightColors else darkColors + @Composable + fun getActiveColors(): AppColorsPreferenceState { + return if (MaterialTheme.colors.isLight) lightColors else darkColors + } } -} expect val showWindowDecorationsOption: Boolean diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt index d9a48f65..9bdad983 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsBackupScreen.kt @@ -119,150 +119,152 @@ class SettingsBackupScreen : Screen { } } -class SettingsBackupViewModel @Inject constructor( - private val validateBackupFile: ValidateBackupFile, - private val importBackupFile: ImportBackupFile, - private val exportBackupFile: ExportBackupFile, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - private val _restoreStatus = MutableStateFlow(Status.Nothing) - val restoreStatus = _restoreStatus.asStateFlow() +class SettingsBackupViewModel + @Inject + constructor( + private val validateBackupFile: ValidateBackupFile, + private val importBackupFile: ImportBackupFile, + private val exportBackupFile: ExportBackupFile, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + private val _restoreStatus = MutableStateFlow(Status.Nothing) + val restoreStatus = _restoreStatus.asStateFlow() - private val _missingSourceFlow = MutableSharedFlow>>() - val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow()) + private val _missingSourceFlow = MutableSharedFlow>>() + val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow()) - private val _creatingStatus = MutableStateFlow(Status.Nothing) - val creatingStatus = _creatingStatus.asStateFlow() - private val _createFlow = MutableSharedFlow() - val createFlowHolder = StableHolder(_createFlow.asSharedFlow()) - fun restoreFile(source: Source) { - scope.launch { - val file = try { - FileSystem.SYSTEM_TEMPORARY_DIRECTORY - .resolve("tachidesk.${Random.nextLong()}.proto.gz") - .also { file -> - source.saveTo(file) + private val _creatingStatus = MutableStateFlow(Status.Nothing) + val creatingStatus = _creatingStatus.asStateFlow() + private val _createFlow = MutableSharedFlow() + val createFlowHolder = StableHolder(_createFlow.asSharedFlow()) + fun restoreFile(source: Source) { + scope.launch { + val file = try { + FileSystem.SYSTEM_TEMPORARY_DIRECTORY + .resolve("tachidesk.${Random.nextLong()}.proto.gz") + .also { file -> + source.saveTo(file) + } + } catch (e: Exception) { + log.warn(e) { "Error creating backup file" } + _restoreStatus.value = Status.Error + e.throwIfCancellation() + return@launch + } + + validateBackupFile.asFlow(file) + .onEach { (missingSources) -> + if (missingSources.isEmpty()) { + restoreBackup(file) + } else { + _missingSourceFlow.emit(file to missingSources.toImmutableList()) + } } - } catch (e: Exception) { - log.warn(e) { "Error creating backup file" } - _restoreStatus.value = Status.Error - e.throwIfCancellation() - return@launch + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error importing backup" } + _restoreStatus.value = Status.Error + } + .collect() } + } - validateBackupFile.asFlow(file) - .onEach { (missingSources) -> - if (missingSources.isEmpty()) { - restoreBackup(file) - } else { - _missingSourceFlow.emit(file to missingSources.toImmutableList()) + fun restoreBackup(file: Path) { + importBackupFile + .asFlow(file) { + onUpload { bytesSentTotal, contentLength -> + _restoreStatus.value = Status.InProgress( + (bytesSentTotal.toFloat() / contentLength) + .coerceAtMost(1.0F), + ) } } + .onStart { + _restoreStatus.value = Status.InProgress(null) + } + .onEach { + _restoreStatus.value = Status.Success + } .catch { toast(it.message.orEmpty()) log.warn(it) { "Error importing backup" } _restoreStatus.value = Status.Error } - .collect() + .launchIn(scope) } - } - fun restoreBackup(file: Path) { - importBackupFile - .asFlow(file) { - onUpload { bytesSentTotal, contentLength -> - _restoreStatus.value = Status.InProgress( - (bytesSentTotal.toFloat() / contentLength) - .coerceAtMost(1.0F), - ) + fun stopRestore() { + _restoreStatus.value = Status.Error + } + + private val tempFile = MutableStateFlow(null) + private val mutex = Mutex() + + fun exportBackup() { + exportBackupFile + .asFlow { + onDownload { bytesSentTotal, contentLength -> + _creatingStatus.value = Status.InProgress( + (bytesSentTotal.toFloat() / contentLength) + .coerceAtMost(0.99F), + ) + } } - } - .onStart { - _restoreStatus.value = Status.InProgress(null) - } - .onEach { - _restoreStatus.value = Status.Success - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error importing backup" } - _restoreStatus.value = Status.Error - } - .launchIn(scope) - } - - fun stopRestore() { - _restoreStatus.value = Status.Error - } - - private val tempFile = MutableStateFlow(null) - private val mutex = Mutex() - - fun exportBackup() { - exportBackupFile - .asFlow { - onDownload { bytesSentTotal, contentLength -> - _creatingStatus.value = Status.InProgress( - (bytesSentTotal.toFloat() / contentLength) - .coerceAtMost(0.99F), - ) + .onStart { + _creatingStatus.value = Status.InProgress(null) } - } - .onStart { - _creatingStatus.value = Status.InProgress(null) - } - .onEach { backup -> - val filename = - backup.headers["content-disposition"]?.substringAfter("filename=") - ?.trim('"') ?: "backup" - tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { - mutex.tryLock() - scope.launch { - try { - backup.bodyAsChannel().toSource().saveTo(it) - } catch (e: Exception) { - e.throwIfCancellation() - log.warn(e) { "Error creating backup" } - _creatingStatus.value = Status.Error - } finally { - mutex.unlock() + .onEach { backup -> + val filename = + backup.headers["content-disposition"]?.substringAfter("filename=") + ?.trim('"') ?: "backup" + tempFile.value = FileSystem.SYSTEM_TEMPORARY_DIRECTORY.resolve(filename).also { + mutex.tryLock() + scope.launch { + try { + backup.bodyAsChannel().toSource().saveTo(it) + } catch (e: Exception) { + e.throwIfCancellation() + log.warn(e) { "Error creating backup" } + _creatingStatus.value = Status.Error + } finally { + mutex.unlock() + } } } + _createFlow.emit(filename) } - _createFlow.emit(filename) - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error exporting backup" } - _creatingStatus.value = Status.Error - } - .launchIn(scope) - } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error exporting backup" } + _creatingStatus.value = Status.Error + } + .launchIn(scope) + } - fun exportBackupFileFound(backupSink: Sink) { - scope.launch { - mutex.withLock { - val tempFile = tempFile.value - if (creatingStatus.value is Status.InProgress && tempFile != null) { - try { - FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer()) - _creatingStatus.value = Status.Success - } catch (e: Exception) { - e.throwIfCancellation() - log.error(e) { "Error moving created backup" } + fun exportBackupFileFound(backupSink: Sink) { + scope.launch { + mutex.withLock { + val tempFile = tempFile.value + if (creatingStatus.value is Status.InProgress && tempFile != null) { + try { + FileSystem.SYSTEM.source(tempFile).copyTo(backupSink.buffer()) + _creatingStatus.value = Status.Success + } catch (e: Exception) { + e.throwIfCancellation() + log.error(e) { "Error moving created backup" } + _creatingStatus.value = Status.Error + } + } else { _creatingStatus.value = Status.Error } - } else { - _creatingStatus.value = Status.Error } } } - } - private companion object { - private val log = logging() + private companion object { + private val log = logging() + } } -} sealed class Status { object Nothing : Status() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt index f7c252f5..95013838 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsGeneralScreen.kt @@ -79,63 +79,65 @@ class SettingsGeneralScreen : Screen { } } -class SettingsGeneralViewModel @Inject constructor( - private val dateHandler: DateHandler, - uiPreferences: UiPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { +class SettingsGeneralViewModel + @Inject + constructor( + private val dateHandler: DateHandler, + uiPreferences: UiPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val startScreen = uiPreferences.startScreen().asStateFlow() + val confirmExit = uiPreferences.confirmExit().asStateFlow() + val language = uiPreferences.language().asStateFlow() + val dateFormat = uiPreferences.dateFormat().asStateFlow() - val startScreen = uiPreferences.startScreen().asStateFlow() - val confirmExit = uiPreferences.confirmExit().asStateFlow() - val language = uiPreferences.language().asStateFlow() - val dateFormat = uiPreferences.dateFormat().asStateFlow() + private val now = Clock.System.now() + private val currentLocale = Locale.current - private val now = Clock.System.now() - private val currentLocale = Locale.current + @Composable + fun getStartScreenChoices(): ImmutableMap = + persistentMapOf( + StartScreen.Library to stringResource(MR.strings.location_library), + StartScreen.Updates to stringResource(MR.strings.location_updates), + StartScreen.Sources to stringResource(MR.strings.location_sources), + StartScreen.Extensions to stringResource(MR.strings.location_extensions), + ) - @Composable - fun getStartScreenChoices(): ImmutableMap = persistentMapOf( - StartScreen.Library to stringResource(MR.strings.location_library), - StartScreen.Updates to stringResource(MR.strings.location_updates), - StartScreen.Sources to stringResource(MR.strings.location_sources), - StartScreen.Extensions to stringResource(MR.strings.location_extensions), - ) - - @Composable - fun getLanguageChoices(): ImmutableMap { - val langJsonState = MR.files.languages.readTextAsync() - val langs by produceState(emptyMap(), langJsonState.value) { - val langJson = langJsonState.value - if (langJson != null) { - withIOContext { - value = Json.decodeFromString(langJson)["langs"] - ?.jsonArray - .orEmpty() - .map { it.jsonPrimitive.content } - .associateWith { Locale(it).getDisplayName(currentLocale) } + @Composable + fun getLanguageChoices(): ImmutableMap { + val langJsonState = MR.files.languages.readTextAsync() + val langs by produceState(emptyMap(), langJsonState.value) { + val langJson = langJsonState.value + if (langJson != null) { + withIOContext { + value = Json.decodeFromString(langJson)["langs"] + ?.jsonArray + .orEmpty() + .map { it.jsonPrimitive.content } + .associateWith { Locale(it).getDisplayName(currentLocale) } + } } } + return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale))) + .plus(langs) + .toImmutableMap() } - return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale))) - .plus(langs) - .toImmutableMap() - } - @Composable - fun getDateChoices(): ImmutableMap { - return dateHandler.formatOptions - .associateWith { - it.ifEmpty { stringResource(MR.strings.date_system_default) } + - " (${getFormattedDate(it)})" - } - .toImmutableMap() - } + @Composable + fun getDateChoices(): ImmutableMap { + return dateHandler.formatOptions + .associateWith { + it.ifEmpty { stringResource(MR.strings.date_system_default) } + + " (${getFormattedDate(it)})" + } + .toImmutableMap() + } - @Composable - private fun getFormattedDate(prefValue: String): String { - return dateHandler.getDateFormat(prefValue).invoke(now) + @Composable + private fun getFormattedDate(prefValue: String): String { + return dateHandler.getDateFormat(prefValue).invoke(now) + } } -} @Composable fun SettingsGeneralScreenContent( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt index 42c3e71e..d2aa662b 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsLibraryScreen.kt @@ -94,35 +94,37 @@ class SettingsLibraryScreen : Screen { } } -class SettingsLibraryViewModel @Inject constructor( - libraryPreferences: LibraryPreferences, - private val getCategories: GetCategories, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { +class SettingsLibraryViewModel + @Inject + constructor( + libraryPreferences: LibraryPreferences, + private val getCategories: GetCategories, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val displayMode = libraryPreferences.displayMode().asStateFlow() + val gridColumns = libraryPreferences.gridColumns().asStateFlow() + val gridSize = libraryPreferences.gridSize().asStateFlow() - val displayMode = libraryPreferences.displayMode().asStateFlow() - val gridColumns = libraryPreferences.gridColumns().asStateFlow() - val gridSize = libraryPreferences.gridSize().asStateFlow() + val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() + private val _categories = MutableStateFlow(0) + val categories = _categories.asStateFlow() - val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() - private val _categories = MutableStateFlow(0) - val categories = _categories.asStateFlow() - - init { - refreshCategoryCount() - } - - fun refreshCategoryCount() { - scope.launch { - _categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0 + init { + refreshCategoryCount() } - } - @Composable - fun getDisplayModeChoices() = DisplayMode.values() - .associateWith { stringResource(it.res) } - .toImmutableMap() -} + fun refreshCategoryCount() { + scope.launch { + _categories.value = getCategories.await(true, onError = { toast(it.message.orEmpty()) })?.size ?: 0 + } + } + + @Composable + fun getDisplayModeChoices() = + DisplayMode.values() + .associateWith { stringResource(it.res) } + .toImmutableMap() + } @Composable fun SettingsLibraryScreenContent( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt index 2d1a2f5d..0b99e3a8 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsReaderScreen.kt @@ -88,68 +88,75 @@ class SettingsReaderScreen : Screen { } } -class SettingsReaderViewModel @Inject constructor( - readerPreferences: ReaderPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - val modes = readerPreferences.modes().asStateFlow() - .map { - it.associateWith { it } - .toImmutableMap() - } - .stateIn(scope, SharingStarted.Eagerly, persistentMapOf()) - val selectedMode = readerPreferences.mode().asStateIn(scope) - - private val _modeSettings = MutableStateFlow>>( - persistentListOf(), - ) - val modeSettings = _modeSettings.asStateFlow() - - init { - modes.onEach { modes -> - val modeSettings = _modeSettings.value - val modesInSettings = modeSettings.map { it.item.mode } - _modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) -> - it !in modesInSettings - }.map { (it) -> - StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it))) +class SettingsReaderViewModel + @Inject + constructor( + readerPreferences: ReaderPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val modes = readerPreferences.modes().asStateFlow() + .map { + it.associateWith { it } + .toImmutableMap() } - }.launchIn(scope) + .stateIn(scope, SharingStarted.Eagerly, persistentMapOf()) + val selectedMode = readerPreferences.mode().asStateIn(scope) + + private val _modeSettings = MutableStateFlow>>( + persistentListOf(), + ) + val modeSettings = _modeSettings.asStateFlow() + + init { + modes.onEach { modes -> + val modeSettings = _modeSettings.value + val modesInSettings = modeSettings.map { it.item.mode } + _modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) -> + it !in modesInSettings + }.map { (it) -> + StableHolder(ReaderModePreference(scope, it, readerPreferences.getMode(it))) + } + }.launchIn(scope) + } + + fun getDirectionChoices() = + Direction.values().associateWith { it.res.toPlatformString() } + .toImmutableMap() + + fun getPaddingChoices() = + mapOf( + 0 to MR.strings.page_padding_none.toPlatformString(), + 8 to "8 Dp", + 16 to "16 Dp", + 32 to "32 Dp", + ).toImmutableMap() + + fun getMaxSizeChoices(direction: Direction) = + if (direction == Direction.Right || direction == Direction.Left) { + mapOf( + 0 to MR.strings.max_size_unrestricted.toPlatformString(), + 700 to "700 Dp", + 900 to "900 Dp", + 1100 to "1100 Dp", + ) + } else { + mapOf( + 0 to MR.strings.max_size_unrestricted.toPlatformString(), + 500 to "500 Dp", + 700 to "700 Dp", + 900 to "900 Dp", + ) + }.toImmutableMap() + + fun getImageScaleChoices() = + ImageScale.values().associateWith { it.res.toPlatformString() } + .toImmutableMap() + + fun getNavigationModeChoices() = + NavigationMode.values().associateWith { it.res.toPlatformString() } + .toImmutableMap() } - fun getDirectionChoices() = Direction.values().associateWith { it.res.toPlatformString() } - .toImmutableMap() - - fun getPaddingChoices() = mapOf( - 0 to MR.strings.page_padding_none.toPlatformString(), - 8 to "8 Dp", - 16 to "16 Dp", - 32 to "32 Dp", - ).toImmutableMap() - - fun getMaxSizeChoices(direction: Direction) = if (direction == Direction.Right || direction == Direction.Left) { - mapOf( - 0 to MR.strings.max_size_unrestricted.toPlatformString(), - 700 to "700 Dp", - 900 to "900 Dp", - 1100 to "1100 Dp", - ) - } else { - mapOf( - 0 to MR.strings.max_size_unrestricted.toPlatformString(), - 500 to "500 Dp", - 700 to "700 Dp", - 900 to "900 Dp", - ) - }.toImmutableMap() - - fun getImageScaleChoices() = ImageScale.values().associateWith { it.res.toPlatformString() } - .toImmutableMap() - - fun getNavigationModeChoices() = NavigationMode.values().associateWith { it.res.toPlatformString() } - .toImmutableMap() -} - data class ReaderModePreference( val scope: CoroutineScope, val mode: String, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsScreen.kt index 74953acb..17b6f693 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsScreen.kt @@ -48,7 +48,6 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow class SettingsScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt index 708dc34d..b32cd4a3 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/settings/SettingsServerScreen.kt @@ -88,45 +88,49 @@ expect class SettingsServerHostViewModel : ViewModel expect fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostViewModel): LazyListScope.() -> Unit -class SettingsServerViewModel @Inject constructor( - serverPreferences: ServerPreferences, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - val serverUrl = serverPreferences.server().asStateIn(scope) - val serverPort = serverPreferences.port().asStringStateIn(scope) - val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope) +class SettingsServerViewModel + @Inject + constructor( + serverPreferences: ServerPreferences, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val serverUrl = serverPreferences.server().asStateIn(scope) + val serverPort = serverPreferences.port().asStringStateIn(scope) + val serverPathPrefix = serverPreferences.pathPrefix().asStateIn(scope) - val proxy = serverPreferences.proxy().asStateIn(scope) + val proxy = serverPreferences.proxy().asStateIn(scope) - @Composable - fun getProxyChoices(): ImmutableMap = persistentMapOf( - Proxy.NO_PROXY to stringResource(MR.strings.no_proxy), - Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy), - Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy), - ) + @Composable + fun getProxyChoices(): ImmutableMap = + persistentMapOf( + Proxy.NO_PROXY to stringResource(MR.strings.no_proxy), + Proxy.HTTP_PROXY to stringResource(MR.strings.http_proxy), + Proxy.SOCKS_PROXY to stringResource(MR.strings.socks_proxy), + ) - val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope) - val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope) - val socksHost = serverPreferences.proxySocksHost().asStateIn(scope) - val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope) + val httpHost = serverPreferences.proxyHttpHost().asStateIn(scope) + val httpPort = serverPreferences.proxyHttpPort().asStringStateIn(scope) + val socksHost = serverPreferences.proxySocksHost().asStateIn(scope) + val socksPort = serverPreferences.proxySocksPort().asStringStateIn(scope) - val auth = serverPreferences.auth().asStateIn(scope) + val auth = serverPreferences.auth().asStateIn(scope) - @Composable - fun getAuthChoices(): ImmutableMap = persistentMapOf( - Auth.NONE to stringResource(MR.strings.no_auth), - Auth.BASIC to stringResource(MR.strings.basic_auth), - Auth.DIGEST to stringResource(MR.strings.digest_auth), - ) - val authUsername = serverPreferences.authUsername().asStateIn(scope) - val authPassword = serverPreferences.authPassword().asStateIn(scope) + @Composable + fun getAuthChoices(): ImmutableMap = + persistentMapOf( + Auth.NONE to stringResource(MR.strings.no_auth), + Auth.BASIC to stringResource(MR.strings.basic_auth), + Auth.DIGEST to stringResource(MR.strings.digest_auth), + ) + val authUsername = serverPreferences.authUsername().asStateIn(scope) + val authPassword = serverPreferences.authPassword().asStateIn(scope) - private val _serverSettingChanged = MutableStateFlow(false) - val serverSettingChanged = _serverSettingChanged.asStateFlow() - fun serverSettingChanged() { - _serverSettingChanged.value = true + private val _serverSettingChanged = MutableStateFlow(false) + val serverSettingChanged = _serverSettingChanged.asStateFlow() + fun serverSettingChanged() { + _serverSettingChanged.value = true + } } -} @Composable fun SettingsServerScreenContent( diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/SourcesScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/SourcesScreen.kt index e5f5c2c3..1bb2e9f0 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/SourcesScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/SourcesScreen.kt @@ -13,7 +13,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class SourcesScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreen.kt index 0f5cb52c..0c639ce6 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreen.kt @@ -21,7 +21,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow class SourceScreen(val source: Source, private val initialQuery: String? = null) : BaseScreen() { - override val key: ScreenKey = source.id.toString() @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt index 9b607f69..4135f538 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/SourceScreenViewModel.kt @@ -44,8 +44,8 @@ class SourceScreenViewModel( private val savedStateHandle: SavedStateHandle, initialQuery: String?, ) : ViewModel(contextWrapper) { - - @Inject constructor( + @Inject + constructor( getLatestManga: GetLatestManga, getPopularManga: GetPopularManga, getSearchManga: GetSearchManga, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt index 56e9c6cf..56d0b40d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersMenu.kt @@ -120,7 +120,10 @@ fun SourceFiltersMenu( } @Composable -fun SourceFiltersView<*, *>.toView(startExpanded: Boolean = false, onExpandChanged: ((Boolean, Int) -> Unit)? = null) { +fun SourceFiltersView<*, *>.toView( + startExpanded: Boolean = false, + onExpandChanged: ((Boolean, Int) -> Unit)? = null, +) { when (this) { is SourceFiltersView.CheckBox -> CheckboxView(this) is SourceFiltersView.Group -> GroupView(this, startExpanded, onExpandChanged) @@ -159,7 +162,11 @@ fun SourceFilterAction( } @Composable -fun GroupView(group: SourceFiltersView.Group, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)? = null) { +fun GroupView( + group: SourceFiltersView.Group, + startExpanded: Boolean, + onExpandChanged: ((Boolean, Int) -> Unit)? = null, +) { val state by key(group.hashCode()) { group.state.collectAsState() } ExpandablePreference( title = group.name, @@ -230,7 +237,12 @@ fun SeparatorView() { } @Composable -fun SortRow(name: String, selected: Boolean, asc: Boolean, onClick: () -> Unit) { +fun SortRow( + name: String, + selected: Boolean, + asc: Boolean, + onClick: () -> Unit, +) { SourceFilterAction(name, onClick) { if (selected) { val rotation = if (asc) { @@ -239,7 +251,10 @@ fun SortRow(name: String, selected: Boolean, asc: Boolean, onClick: () -> Unit) 180F } val angle: Float by animateFloatAsState( - targetValue = if (rotation > 360 - rotation) { -(360 - rotation) } else rotation, + targetValue = if (rotation > 360 - rotation) { + -(360 - rotation) } else { + rotation + }, animationSpec = tween( durationMillis = 500, // rotation is retrieved with this frequency easing = LinearEasing, @@ -259,7 +274,11 @@ fun SortRow(name: String, selected: Boolean, asc: Boolean, onClick: () -> Unit) } @Composable -fun SortView(sort: SourceFiltersView.Sort, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)?) { +fun SortView( + sort: SourceFiltersView.Sort, + startExpanded: Boolean, + onExpandChanged: ((Boolean, Int) -> Unit)?, +) { val state by key(sort.hashCode()) { sort.state.collectAsState() } ExpandablePreference( sort.name, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersViewModel.kt index cb29c4e4..2fbddef9 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/SourceFiltersViewModel.kt @@ -37,7 +37,8 @@ class SourceFiltersViewModel( contextWrapper: ContextWrapper, private val savedStateHandle: SavedStateHandle, ) : ViewModel(contextWrapper) { - @Inject constructor( + @Inject + constructor( getFilterList: GetFilterList, setSourceFilter: SetSourceFilter, contextWrapper: ContextWrapper, @@ -133,9 +134,10 @@ class SourceFiltersViewModel( data class Params(val sourceId: Long) - private fun List.toView() = mapIndexed { index, sourcePreference -> - SourceFiltersView(index, sourcePreference) - }.toImmutableList() + private fun List.toView() = + mapIndexed { index, sourcePreference -> + SourceFiltersView(index, sourcePreference) + }.toImmutableList() private companion object { private val log = logging() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/model/SourceFiltersView.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/model/SourceFiltersView.kt index 460abccc..a1a03693 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/model/SourceFiltersView.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/browse/filter/model/SourceFiltersView.kt @@ -164,7 +164,10 @@ sealed class SourceFiltersView { } @Suppress("FunctionName") -fun SourceFiltersView(index: Int, sourceFilter: SourceFilter): SourceFiltersView<*, *> { +fun SourceFiltersView( + index: Int, + sourceFilter: SourceFilter, +): SourceFiltersView<*, *> { return when (sourceFilter) { is CheckBoxFilter -> SourceFiltersView.CheckBox(index, sourceFilter) is HeaderFilter -> SourceFiltersView.Header(index, sourceFilter) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesNavigator.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesNavigator.kt index 819ef35f..47e6b55e 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesNavigator.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/components/SourcesNavigator.kt @@ -93,9 +93,7 @@ private fun navigatorSaver( ) @Composable -private fun SourceNavigatorDisposableEffect( - navigator: SourcesNavigator, -) { +private fun SourceNavigatorDisposableEffect(navigator: SourcesNavigator) { val currentScreen = navigator.current DisposableEffectIgnoringConfiguration(currentScreen.key) { @@ -125,7 +123,6 @@ class SourcesNavigator internal constructor( val screens: SnapshotStateMap = SnapshotStateMap() .also { it[-1] = homeScreen }, ) { - fun remove(source: Source) { navigator replaceAll screens[-1]!! screens.remove(source.id)?.let(this::dispose) @@ -135,7 +132,10 @@ class SourcesNavigator internal constructor( navigator replaceAll screens.getOrPut(source.id) { SourceScreen(source) } } - fun open(source: Source, query: String? = null) { + fun open( + source: Source, + query: String? = null, + ) { screens.remove(source.id)?.let(this::dispose) navigator replaceAll SourceScreen(source, query).also { screens[source.id] = it } } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchScreen.kt index 7a9d22fe..6b883c15 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchScreen.kt @@ -18,7 +18,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow class GlobalSearchScreen(private val initialQuery: String) : BaseScreen() { - @Composable override fun Content() { val vm = stateViewModel { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt index 2c8c74bc..abd56e73 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/globalsearch/GlobalSearchViewModel.kt @@ -43,119 +43,121 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class GlobalSearchViewModel @Inject constructor( - private val getSourceList: GetSourceList, - private val getSearchManga: GetSearchManga, - catalogPreferences: CatalogPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, - @Assisted params: Params, -) : ViewModel(contextWrapper) { - private val _query by savedStateHandle.getStateFlow { params.initialQuery } - val query = _query.asStateFlow() +class GlobalSearchViewModel + @Inject + constructor( + private val getSourceList: GetSourceList, + private val getSearchManga: GetSearchManga, + catalogPreferences: CatalogPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, + @Assisted params: Params, + ) : ViewModel(contextWrapper) { + private val _query by savedStateHandle.getStateFlow { params.initialQuery } + val query = _query.asStateFlow() - private val installedSources = MutableStateFlow(emptyList()) + private val installedSources = MutableStateFlow(emptyList()) - private val languages = catalogPreferences.languages().stateIn(scope) - val displayMode = catalogPreferences.displayMode().stateIn(scope) + private val languages = catalogPreferences.languages().stateIn(scope) + val displayMode = catalogPreferences.displayMode().stateIn(scope) - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - val sources = combine(installedSources, languages) { installedSources, languages -> - installedSources.filter { - it.lang in languages || it.id == Source.LOCAL_SOURCE_ID - }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val sources = combine(installedSources, languages) { installedSources, languages -> + installedSources.filter { + it.lang in languages || it.id == Source.LOCAL_SOURCE_ID + }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val search by savedStateHandle.getStateFlow { params.initialQuery } + private val search by savedStateHandle.getStateFlow { params.initialQuery } - val results = SnapshotStateMap() + val results = SnapshotStateMap() - init { - getSources() - readySearch() - } + init { + getSources() + readySearch() + } - private fun getSources() { - getSourceList.asFlow() - .onEach { sources -> - installedSources.value = sources.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { it.lang } - .thenBy(String.CASE_INSENSITIVE_ORDER) { - it.name - }, - ) - _isLoading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error getting sources" } - _isLoading.value = false - } - .launchIn(scope) - } - - private val semaphore = Semaphore(5) - - private fun readySearch() { - search - .combine(sources) { query, sources -> - query to sources - } - .mapLatest { (query, sources) -> - results.clear() - supervisorScope { - sources.map { source -> - async { - semaphore.withPermit { - getSearchManga.asFlow(source, query, 1) - .map { - if (it.mangaList.isEmpty()) { - Search.Failure(MR.strings.no_results_found.toPlatformString()) - } else { - Search.Success(it.mangaList.toImmutableList()) - } - } - .catch { - log.warn(it) { "Error getting search from ${source.displayName}" } - emit(Search.Failure(it)) - } - .onEach { - results[source.id] = it - } - .collect() - } - } - }.awaitAll() + private fun getSources() { + getSourceList.asFlow() + .onEach { sources -> + installedSources.value = sources.sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { it.lang } + .thenBy(String.CASE_INSENSITIVE_ORDER) { + it.name + }, + ) + _isLoading.value = false } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error getting sources" } + _isLoading.value = false + } + .launchIn(scope) + } + + private val semaphore = Semaphore(5) + + private fun readySearch() { + search + .combine(sources) { query, sources -> + query to sources + } + .mapLatest { (query, sources) -> + results.clear() + supervisorScope { + sources.map { source -> + async { + semaphore.withPermit { + getSearchManga.asFlow(source, query, 1) + .map { + if (it.mangaList.isEmpty()) { + Search.Failure(MR.strings.no_results_found.toPlatformString()) + } else { + Search.Success(it.mangaList.toImmutableList()) + } + } + .catch { + log.warn(it) { "Error getting search from ${source.displayName}" } + emit(Search.Failure(it)) + } + .onEach { + results[source.id] = it + } + .collect() + } + } + }.awaitAll() + } + } + .catch { + log.warn(it) { "Error getting sources" } + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } + + fun setQuery(query: String) { + _query.value = query + } + + fun startSearch(query: String) { + search.value = query + } + + data class Params(val initialQuery: String) + + sealed class Search { + object Searching : Search() + data class Success(val mangaList: ImmutableList) : Search() + data class Failure(val e: String?) : Search() { + constructor(e: Throwable) : this(e.message) } - .catch { - log.warn(it) { "Error getting sources" } - } - .flowOn(Dispatchers.IO) - .launchIn(scope) - } + } - fun setQuery(query: String) { - _query.value = query - } - - fun startSearch(query: String) { - search.value = query - } - - data class Params(val initialQuery: String) - - sealed class Search { - object Searching : Search() - data class Success(val mangaList: ImmutableList) : Search() - data class Failure(val e: String?) : Search() { - constructor(e: Throwable) : this(e.message) + private companion object { + private val log = logging() } } - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreen.kt index 9020a12e..ce464548 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreen.kt @@ -18,7 +18,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow class SourceHomeScreen : BaseScreen() { - @Composable override fun Content() { val vm = stateViewModel { sourceHomeViewModel(it) } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt index d4ae4885..1e6a5638 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/SourceHomeScreenViewModel.kt @@ -34,98 +34,100 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SourceHomeScreenViewModel @Inject constructor( - private val getSourceList: GetSourceList, - catalogPreferences: CatalogPreferences, - contextWrapper: ContextWrapper, - @Assisted private val savedStateHandle: SavedStateHandle, -) : ViewModel(contextWrapper) { - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() +class SourceHomeScreenViewModel + @Inject + constructor( + private val getSourceList: GetSourceList, + catalogPreferences: CatalogPreferences, + contextWrapper: ContextWrapper, + @Assisted private val savedStateHandle: SavedStateHandle, + ) : ViewModel(contextWrapper) { + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - private val installedSources = MutableStateFlow(emptyList()) + private val installedSources = MutableStateFlow(emptyList()) - private val _languages = catalogPreferences.languages().asStateFlow() - val languages = _languages.asStateFlow() - .map { it.toImmutableSet() } - .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) + private val _languages = catalogPreferences.languages().asStateFlow() + val languages = _languages.asStateFlow() + .map { it.toImmutableSet() } + .stateIn(scope, SharingStarted.Eagerly, persistentSetOf()) - val sources = combine(installedSources, languages) { installedSources, languages -> - val all = MR.strings.all.toPlatformString() - val other = MR.strings.other.toPlatformString() - installedSources - .distinctBy { it.id } - .filter { - it.lang in languages || it.id == Source.LOCAL_SOURCE_ID - } - .groupBy(Source::displayLang) - .mapValues { - it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name)) - .map(SourceUI::SourceItem) - } - .mapKeys { (key) -> - when (key) { - "all" -> all - "other" -> other - else -> Locale(key).displayName + val sources = combine(installedSources, languages) { installedSources, languages -> + val all = MR.strings.all.toPlatformString() + val other = MR.strings.other.toPlatformString() + installedSources + .distinctBy { it.id } + .filter { + it.lang in languages || it.id == Source.LOCAL_SOURCE_ID } - } - .toList() - .sortedWith( - compareBy> { (key) -> + .groupBy(Source::displayLang) + .mapValues { + it.value.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, Source::name)) + .map(SourceUI::SourceItem) + } + .mapKeys { (key) -> when (key) { - all -> 1 - other -> 3 - else -> 2 + "all" -> all + "other" -> other + else -> Locale(key).displayName } - }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), - ) - .flatMap { (key, value) -> - listOf(SourceUI.Header(key)) + value - } - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + } + .toList() + .sortedWith( + compareBy> { (key) -> + when (key) { + all -> 1 + other -> 3 + else -> 2 + } + }.thenBy(String.CASE_INSENSITIVE_ORDER, Pair::first), + ) + .flatMap { (key, value) -> + listOf(SourceUI.Header(key)) + value + } + .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - val sourceLanguages = installedSources.map { sources -> - sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG) - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + val sourceLanguages = installedSources.map { sources -> + sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG) + .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - private val _query by savedStateHandle.getStateFlow { "" } - val query = _query.asStateFlow() + private val _query by savedStateHandle.getStateFlow { "" } + val query = _query.asStateFlow() - init { - getSources() + init { + getSources() + } + + private fun getSources() { + getSourceList.asFlow() + .onEach { + installedSources.value = it + _isLoading.value = false + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error getting sources" } + _isLoading.value = false + } + .launchIn(scope) + } + + fun setEnabledLanguages(langs: Set) { + log.info { langs } + _languages.value = langs + } + + fun setQuery(query: String) { + _query.value = query + } + + private companion object { + private val log = logging() + } } - private fun getSources() { - getSourceList.asFlow() - .onEach { - installedSources.value = it - _isLoading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error getting sources" } - _isLoading.value = false - } - .launchIn(scope) - } - - fun setEnabledLanguages(langs: Set) { - log.info { langs } - _languages.value = langs - } - - fun setQuery(query: String) { - _query.value = query - } - - private companion object { - private val log = logging() - } -} - @Stable sealed class SourceUI { @Stable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt index 089b9ef8..235fbc50 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/home/components/SourceHomeScreenContent.kt @@ -350,9 +350,7 @@ fun ThinSourceItem( @Composable @Stable -private fun getActionItems( - openEnabledLanguagesClick: () -> Unit, -): ImmutableList { +private fun getActionItems(openEnabledLanguagesClick: () -> Unit): ImmutableList { return persistentListOf( ActionItem( stringResource(MR.strings.enabled_languages), diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreen.kt index 59731b94..f0c0f17f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreen.kt @@ -15,7 +15,6 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey class SourceSettingsScreen(private val sourceId: Long) : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt index 20a166e4..19dab39c 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/SourceSettingsScreenViewModel.kt @@ -28,61 +28,64 @@ import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class SourceSettingsScreenViewModel @Inject constructor( - private val getSourceSettings: GetSourceSettings, - private val setSourceSetting: SetSourceSetting, - contextWrapper: ContextWrapper, - @Assisted private val params: Params, -) : ViewModel(contextWrapper) { - private val _loading = MutableStateFlow(true) - val loading = _loading.asStateFlow() +class SourceSettingsScreenViewModel + @Inject + constructor( + private val getSourceSettings: GetSourceSettings, + private val setSourceSetting: SetSourceSetting, + contextWrapper: ContextWrapper, + @Assisted private val params: Params, + ) : ViewModel(contextWrapper) { + private val _loading = MutableStateFlow(true) + val loading = _loading.asStateFlow() - private val _sourceSettings = MutableStateFlow>>(persistentListOf()) - val sourceSettings = _sourceSettings.asStateFlow() + private val _sourceSettings = MutableStateFlow>>(persistentListOf()) + val sourceSettings = _sourceSettings.asStateFlow() - init { - getSourceSettings() - sourceSettings.mapLatest { settings -> - supervisorScope { - settings.forEach { setting -> - setting.state.drop(1) - .filterNotNull() - .onEach { - setSourceSetting.await( - sourceId = params.sourceId, - settingIndex = setting.index, - setting = it, - onError = { toast(it.message.orEmpty()) }, - ) - getSourceSettings() - } - .launchIn(this) + init { + getSourceSettings() + sourceSettings.mapLatest { settings -> + supervisorScope { + settings.forEach { setting -> + setting.state.drop(1) + .filterNotNull() + .onEach { + setSourceSetting.await( + sourceId = params.sourceId, + settingIndex = setting.index, + setting = it, + onError = { toast(it.message.orEmpty()) }, + ) + getSourceSettings() + } + .launchIn(this) + } } - } - }.launchIn(scope) + }.launchIn(scope) + } + + private fun getSourceSettings() { + getSourceSettings.asFlow(params.sourceId) + .onEach { + _sourceSettings.value = it.toView() + _loading.value = false + } + .catch { + toast(it.message.orEmpty()) + log.warn(it) { "Error setting source setting" } + _loading.value = false + } + .launchIn(scope) + } + + data class Params(val sourceId: Long) + + private fun List.toView() = + mapIndexed { index, sourcePreference -> + SourceSettingsView(index, sourcePreference) + }.toImmutableList() + + private companion object { + private val log = logging() + } } - - private fun getSourceSettings() { - getSourceSettings.asFlow(params.sourceId) - .onEach { - _sourceSettings.value = it.toView() - _loading.value = false - } - .catch { - toast(it.message.orEmpty()) - log.warn(it) { "Error setting source setting" } - _loading.value = false - } - .launchIn(scope) - } - - data class Params(val sourceId: Long) - - private fun List.toView() = mapIndexed { index, sourcePreference -> - SourceSettingsView(index, sourcePreference) - }.toImmutableList() - - private companion object { - private val log = logging() - } -} diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt index 7305af0c..29ef4c16 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/components/SourceSettingsScreenContent.kt @@ -59,9 +59,7 @@ import com.vanpra.composematerialdialogs.title import kotlinx.collections.immutable.ImmutableList @Composable -fun SourceSettingsScreenContent( - settings: ImmutableList>, -) { +fun SourceSettingsScreenContent(settings: ImmutableList>) { Scaffold( modifier = Modifier.windowInsetsPadding( WindowInsets.statusBars.add( @@ -119,7 +117,10 @@ fun SourceSettingsScreenContent( } @Composable -private fun TwoStatePreference(twoState: TwoState, checkbox: Boolean) { +private fun TwoStatePreference( + twoState: TwoState, + checkbox: Boolean, +) { val state by twoState.state.collectAsState() val title = remember(state) { twoState.title ?: twoState.summary ?: "No title" } val subtitle = remember(state) { diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/model/SourceSettingsView.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/model/SourceSettingsView.kt index f798d8cc..5b625057 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/model/SourceSettingsView.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/sources/settings/model/SourceSettingsView.kt @@ -100,9 +100,10 @@ sealed class SourceSettingsView { override val summary: String? get() = subtitle?.let { withFormat(it, props.entries[props.entryValues.indexOf(state.value)]) } - fun getOptions() = props.entryValues.mapIndexed { index, s -> - s to props.entries[index] - }.toImmutableList() + fun getOptions() = + props.entryValues.mapIndexed { index, s -> + s to props.entries[index] + }.toImmutableList() } data class MultiSelect internal constructor( @@ -125,9 +126,10 @@ sealed class SourceSettingsView { preference.props, ) - fun getOptions() = props.entryValues.mapIndexed { index, s -> - s to props.entries[index] - }.toImmutableList() + fun getOptions() = + props.entryValues.mapIndexed { index, s -> + s to props.entries[index] + }.toImmutableList() fun toggleOption(key: String) { if (key in state.value.orEmpty()) { @@ -162,11 +164,17 @@ sealed class SourceSettingsView { } } -private fun withFormat(text: String, value: Any?): String { +private fun withFormat( + text: String, + value: Any?, +): String { return stringFormat(text, value) } -fun SourceSettingsView(index: Int, preference: SourcePreference): SourceSettingsView<*, *> { +fun SourceSettingsView( + index: Int, + preference: SourcePreference, +): SourceSettingsView<*, *> { return when (preference) { is CheckBoxPreference -> SourceSettingsView.CheckBox(index, preference) is SwitchPreference -> SourceSettingsView.Switch(index, preference) diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreen.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreen.kt index e9604de0..08fafc7f 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreen.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreen.kt @@ -19,7 +19,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow class UpdatesScreen : Screen { - override val key: ScreenKey = uniqueScreenKey @Composable diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt index 283f955c..e570bb5d 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/UpdatesScreenViewModel.kt @@ -40,192 +40,199 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class UpdatesScreenViewModel @Inject constructor( - private val queueChapterDownload: QueueChapterDownload, - private val stopChapterDownload: StopChapterDownload, - private val deleteChapterDownload: DeleteChapterDownload, - private val getRecentUpdates: GetRecentUpdates, - private val batchUpdateChapter: BatchUpdateChapter, - private val batchChapterDownload: BatchChapterDownload, - private val updateLibrary: UpdateLibrary, - private val updatesPager: UpdatesPager, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { +class UpdatesScreenViewModel + @Inject + constructor( + private val queueChapterDownload: QueueChapterDownload, + private val stopChapterDownload: StopChapterDownload, + private val deleteChapterDownload: DeleteChapterDownload, + private val getRecentUpdates: GetRecentUpdates, + private val batchUpdateChapter: BatchUpdateChapter, + private val batchChapterDownload: BatchChapterDownload, + private val updateLibrary: UpdateLibrary, + private val updatesPager: UpdatesPager, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + private val _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() - private val _isLoading = MutableStateFlow(true) - val isLoading = _isLoading.asStateFlow() - - val updates = updatesPager.updates.map { - it.map { - when (it) { - is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date) - is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter)) - } - }.toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - private val _selectedIds = MutableStateFlow>(persistentListOf()) - val selectedItems = combine(updates, _selectedIds) { updates, selectedItems -> - updates.asSequence() - .filterIsInstance() - .filter { it.chapterDownloadItem.isSelected(selectedItems) } - .map { it.chapterDownloadItem } - .toImmutableList() - }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - - val inActionMode = _selectedIds.map { it.isNotEmpty() } - .stateIn(scope, SharingStarted.Eagerly, false) - - init { - updatesPager.loadNextPage( - onComplete = { - _isLoading.value = false - }, - onError = { - toast(it.message.orEmpty()) - }, - ) - updates - .map { updates -> - updates.filterIsInstance().mapNotNull { - it.chapterDownloadItem.manga?.id - }.toSet() - } - .combine(DownloadService.downloadQueue) { mangaIds, queue -> - mangaIds to queue - } - .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - .onEach { (mangaIds, queue) -> - val chapters = queue.filter { it.mangaId in mangaIds } - updates.value.filterIsInstance().forEach { - it.chapterDownloadItem.updateFrom(chapters) + val updates = updatesPager.updates.map { + it.map { + when (it) { + is UpdatesPager.Updates.Date -> UpdatesUI.Header(it.date) + is UpdatesPager.Updates.Update -> UpdatesUI.Item(ChapterDownloadItem(it.manga, it.chapter)) } - } - .flowOn(Dispatchers.Default) - .launchIn(scope) - } + }.toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) - fun loadNextPage() { - updatesPager.loadNextPage( - onError = { - toast(it.message.orEmpty()) - }, - ) - } - - private fun setRead(chapterIds: List, read: Boolean) { - scope.launch { - batchUpdateChapter.await(chapterIds, isRead = read, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - } - } - fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - private fun setBookmarked(chapterIds: List, bookmark: Boolean) { - scope.launch { - batchUpdateChapter.await(chapterIds, isBookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - } - } - fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) - fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) - - fun downloadChapter(chapter: Chapter?) { - scope.launch { - if (chapter == null) { - val selectedIds = _selectedIds.value - batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) - _selectedIds.value = persistentListOf() - return@launch - } - queueChapterDownload.await(chapter, onError = { toast(it.message.orEmpty()) }) - } - } - - fun deleteDownloadedChapter(chapter: Chapter?) { - scope.launchDefault { - if (chapter == null) { - val selectedIds = _selectedIds.value - batchUpdateChapter.await(selectedIds, delete = true, onError = { toast(it.message.orEmpty()) }) - selectedItems.value.forEach { - it.setNotDownloaded() - } - _selectedIds.value = persistentListOf() - return@launchDefault - } - updates.value + private val _selectedIds = MutableStateFlow>(persistentListOf()) + val selectedItems = combine(updates, _selectedIds) { updates, selectedItems -> + updates.asSequence() .filterIsInstance() - .find { (chapterDownloadItem) -> - chapterDownloadItem.chapter.mangaId == chapter.mangaId && - chapterDownloadItem.chapter.index == chapter.index - } - ?.chapterDownloadItem - ?.deleteDownload(deleteChapterDownload) - } - } - - fun stopDownloadingChapter(chapter: Chapter) { - scope.launchDefault { - updates.value - .filterIsInstance() - .find { (chapterDownloadItem) -> - chapterDownloadItem.chapter.mangaId == chapter.mangaId && - chapterDownloadItem.chapter.index == chapter.index - } - ?.chapterDownloadItem - ?.stopDownloading(stopChapterDownload) - } - } - - fun selectAll() { - scope.launchDefault { - _selectedIds.value = updates.value.filterIsInstance() - .map { it.chapterDownloadItem.chapter.id } + .filter { it.chapterDownloadItem.isSelected(selectedItems) } + .map { it.chapterDownloadItem } .toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + val inActionMode = _selectedIds.map { it.isNotEmpty() } + .stateIn(scope, SharingStarted.Eagerly, false) + + init { + updatesPager.loadNextPage( + onComplete = { + _isLoading.value = false + }, + onError = { + toast(it.message.orEmpty()) + }, + ) + updates + .map { updates -> + updates.filterIsInstance().mapNotNull { + it.chapterDownloadItem.manga?.id + }.toSet() + } + .combine(DownloadService.downloadQueue) { mangaIds, queue -> + mangaIds to queue + } + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + .onEach { (mangaIds, queue) -> + val chapters = queue.filter { it.mangaId in mangaIds } + updates.value.filterIsInstance().forEach { + it.chapterDownloadItem.updateFrom(chapters) + } + } + .flowOn(Dispatchers.Default) + .launchIn(scope) + } + + fun loadNextPage() { + updatesPager.loadNextPage( + onError = { + toast(it.message.orEmpty()) + }, + ) + } + + private fun setRead( + chapterIds: List, + read: Boolean, + ) { + scope.launch { + batchUpdateChapter.await(chapterIds, isRead = read, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + fun markRead(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + fun markUnread(id: Long?) = setRead(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + private fun setBookmarked( + chapterIds: List, + bookmark: Boolean, + ) { + scope.launch { + batchUpdateChapter.await(chapterIds, isBookmarked = bookmark, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + } + } + fun bookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, true) + fun unBookmarkChapter(id: Long?) = setBookmarked(listOfNotNull(id).ifEmpty { _selectedIds.value }, false) + + fun downloadChapter(chapter: Chapter?) { + scope.launch { + if (chapter == null) { + val selectedIds = _selectedIds.value + batchChapterDownload.await(selectedIds, onError = { toast(it.message.orEmpty()) }) + _selectedIds.value = persistentListOf() + return@launch + } + queueChapterDownload.await(chapter, onError = { toast(it.message.orEmpty()) }) + } + } + + fun deleteDownloadedChapter(chapter: Chapter?) { + scope.launchDefault { + if (chapter == null) { + val selectedIds = _selectedIds.value + batchUpdateChapter.await(selectedIds, delete = true, onError = { toast(it.message.orEmpty()) }) + selectedItems.value.forEach { + it.setNotDownloaded() + } + _selectedIds.value = persistentListOf() + return@launchDefault + } + updates.value + .filterIsInstance() + .find { (chapterDownloadItem) -> + chapterDownloadItem.chapter.mangaId == chapter.mangaId && + chapterDownloadItem.chapter.index == chapter.index + } + ?.chapterDownloadItem + ?.deleteDownload(deleteChapterDownload) + } + } + + fun stopDownloadingChapter(chapter: Chapter) { + scope.launchDefault { + updates.value + .filterIsInstance() + .find { (chapterDownloadItem) -> + chapterDownloadItem.chapter.mangaId == chapter.mangaId && + chapterDownloadItem.chapter.index == chapter.index + } + ?.chapterDownloadItem + ?.stopDownloading(stopChapterDownload) + } + } + + fun selectAll() { + scope.launchDefault { + _selectedIds.value = updates.value.filterIsInstance() + .map { it.chapterDownloadItem.chapter.id } + .toImmutableList() + } + } + + fun invertSelection() { + scope.launchDefault { + _selectedIds.value = updates.value.filterIsInstance() + .map { it.chapterDownloadItem.chapter.id } + .minus(_selectedIds.value) + .toImmutableList() + } + } + + fun selectChapter(id: Long) { + scope.launchDefault { + _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() + } + } + fun unselectChapter(id: Long) { + scope.launchDefault { + _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() + } + } + + fun clearSelection() { + scope.launchDefault { + _selectedIds.value = persistentListOf() + } + } + + fun updateLibrary() { + scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } + } + + override fun onDispose() { + super.onDispose() + updatesPager.cancel() + } + + private companion object { + private val log = logging() } } - fun invertSelection() { - scope.launchDefault { - _selectedIds.value = updates.value.filterIsInstance() - .map { it.chapterDownloadItem.chapter.id } - .minus(_selectedIds.value) - .toImmutableList() - } - } - - fun selectChapter(id: Long) { - scope.launchDefault { - _selectedIds.value = _selectedIds.value.plus(id).toImmutableList() - } - } - fun unselectChapter(id: Long) { - scope.launchDefault { - _selectedIds.value = _selectedIds.value.minus(id).toImmutableList() - } - } - - fun clearSelection() { - scope.launchDefault { - _selectedIds.value = persistentListOf() - } - } - - fun updateLibrary() { - scope.launchDefault { updateLibrary.await(onError = { toast(it.message.orEmpty()) }) } - } - - override fun onDispose() { - super.onDispose() - updatesPager.cancel() - } - - private companion object { - private val log = logging() - } -} - sealed class UpdatesUI { data class Item(val chapterDownloadItem: ChapterDownloadItem) : UpdatesUI() data class Header(val date: String) : UpdatesUI() diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt index d81274d6..e72ee461 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/updates/components/UpdatesScreenContent.kt @@ -189,7 +189,9 @@ fun UpdatesScreenContent( UpdatesItem( chapterDownloadItem = item.chapterDownloadItem, onClickItem = if (inActionMode) { - { if (item.chapterDownloadItem.isSelected.value) onUnselectChapter(item.chapterDownloadItem.chapter.id) else onSelectChapter(item.chapterDownloadItem.chapter.id) } + { + if (item.chapterDownloadItem.isSelected.value) onUnselectChapter(item.chapterDownloadItem.chapter.id) else onSelectChapter(item.chapterDownloadItem.chapter.id) + } } else { { openChapter(chapter.index, manga.id) } }, diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt index 6c3d9b16..fb32b9fe 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt @@ -9,5 +9,8 @@ package ca.gosyer.jui.ui.util.lang import androidx.compose.ui.text.intl.Locale expect class CollatorComparator(locale: Locale) : Comparator { - override fun compare(source: String, target: String): Int + override fun compare( + source: String, + target: String, + ): Int } diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt index 67d8d99b..d7f1b0f9 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt @@ -6,4 +6,7 @@ package ca.gosyer.jui.ui.util.lang -expect fun stringFormat(string: String, vararg args: Any?): String +expect fun stringFormat( + string: String, + vararg args: Any?, +): String diff --git a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/system/Flow.kt b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/system/Flow.kt index dcd5cb16..0a38f6d9 100644 --- a/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/system/Flow.kt +++ b/presentation/src/commonMain/kotlin/ca/gosyer/jui/ui/util/system/Flow.kt @@ -14,7 +14,11 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch -fun Flow.asStateFlow(defaultValue: T, scope: CoroutineScope, dropFirst: Boolean = false): StateFlow { +fun Flow.asStateFlow( + defaultValue: T, + scope: CoroutineScope, + dropFirst: Boolean = false, +): StateFlow { val flow = MutableStateFlow(defaultValue) scope.launch { if (dropFirst) { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt index ec307e59..14f2aeff 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/image/DesktopImageLoaderBuilder.kt @@ -18,11 +18,17 @@ import com.seiko.imageloader.option.OptionsBuilder actual fun OptionsBuilder.configure(contextWrapper: ContextWrapper) { } -actual fun ComponentRegistryBuilder.register(contextWrapper: ContextWrapper, http: Http) { +actual fun ComponentRegistryBuilder.register( + contextWrapper: ContextWrapper, + http: Http, +) { setupDefaultComponents(httpClient = { http }) } -actual fun DiskCacheBuilder.configure(contextWrapper: ContextWrapper, cacheDir: String) { +actual fun DiskCacheBuilder.configure( + contextWrapper: ContextWrapper, + cacheDir: String, +) { directory(userDataDir / cacheDir) maxSizeBytes(1024 * 1024 * 150) // 150 MB } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt index 955a2994..ba4f8004 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt @@ -17,7 +17,11 @@ import androidx.compose.ui.unit.dp import ca.gosyer.jui.uicore.components.BoxWithTooltipSurface @Composable -actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { +actual fun ActionIcon( + onClick: () -> Unit, + contentDescription: String, + icon: ImageVector, +) { BoxWithTooltipSurface( { Text(contentDescription, modifier = Modifier.padding(10.dp)) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/DesktopBackHandler.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/DesktopBackHandler.kt index fa711eae..bcaaf3d9 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/DesktopBackHandler.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/navigation/DesktopBackHandler.kt @@ -9,4 +9,7 @@ package ca.gosyer.jui.ui.base.navigation import androidx.compose.runtime.Composable @Composable -internal actual fun RealBackHandler(enabled: Boolean, onBack: () -> Unit) {} +internal actual fun RealBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) {} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt index 31471b90..ef916cc3 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt @@ -11,6 +11,5 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey actual abstract class BaseScreen : Screen { - override val key: ScreenKey = uniqueScreenKey } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/state/DesktopSavedStateHandle.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/state/DesktopSavedStateHandle.kt index 21e5eb00..b7577948 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/state/DesktopSavedStateHandle.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/base/state/DesktopSavedStateHandle.kt @@ -19,7 +19,10 @@ actual class SavedStateHandle { return regular[key] as T? } - actual operator fun set(key: String, value: T?) { + actual operator fun set( + key: String, + value: T?, + ) { regular[key] = value flows[key]?.value = value } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesWindow.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesWindow.kt index a8ddfb26..f75987f8 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesWindow.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/categories/CategoriesWindow.kt @@ -17,7 +17,6 @@ import ca.gosyer.jui.presentation.build.BuildKonfig import cafe.adriel.voyager.navigator.Navigator actual class CategoriesLauncher(private val notifyFinished: () -> Unit) { - private var isOpen by mutableStateOf(false) actual fun open() { diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/library/components/DesktopLibraryGrid.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/library/components/DesktopLibraryGrid.kt index 977a30b0..2516757a 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/library/components/DesktopLibraryGrid.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/library/components/DesktopLibraryGrid.kt @@ -18,19 +18,18 @@ import ca.gosyer.jui.uicore.resources.stringResource actual fun Modifier.libraryMangaModifier( onClickManga: () -> Unit, onClickRemoveManga: () -> Unit, -): Modifier = this - .onClick(onClick = onClickManga) - .onRightClickContextMenu( - items = { - getContextItems(onClickRemoveManga) - }, - ) +): Modifier = + this + .onClick(onClick = onClickManga) + .onRightClickContextMenu( + items = { + getContextItems(onClickRemoveManga) + }, + ) @Composable @Stable -private fun getContextItems( - onClickRemoveManga: () -> Unit, -): List { +private fun getContextItems(onClickRemoveManga: () -> Unit): List { return listOf( ContextMenuItem(stringResource(MR.strings.action_remove_favorite), onClickRemoveManga), ) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt index a1a32235..0657e906 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt @@ -21,5 +21,5 @@ actual fun getDebugInfo(): String { Runtime name: ${runtime.vmName} Runtime vendor: ${runtime.vmVendor} Runtime version: ${runtime.vmVersion} - """.trimIndent() + """.trimIndent() } diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt index 6a574f6a..ce7ebdfc 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/main/components/TrayViewModel.kt @@ -18,24 +18,26 @@ import kotlinx.coroutines.flow.shareIn import me.tatarka.inject.annotations.Inject import org.lighthousegames.logging.logging -class TrayViewModel @Inject constructor( - updateChecker: UpdateChecker, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - override val scope = MainScope() +class TrayViewModel + @Inject + constructor( + updateChecker: UpdateChecker, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + override val scope = MainScope() - val updateFound = updateChecker - .asFlow(false) - .catch { log.warn(it) { "Failed to check for updates" } } - .filterIsInstance() - .shareIn(scope, SharingStarted.Eagerly, 1) + val updateFound = updateChecker + .asFlow(false) + .catch { log.warn(it) { "Failed to check for updates" } } + .filterIsInstance() + .shareIn(scope, SharingStarted.Eagerly, 1) - override fun onDispose() { - super.onDispose() - scope.cancel() + override fun onDispose() { + super.onDispose() + scope.cancel() + } + + companion object { + private val log = logging() + } } - - companion object { - private val log = logging() - } -} diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/manga/components/DesktopChapterItem.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/manga/components/DesktopChapterItem.kt index 425d2990..2e776dd1 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/manga/components/DesktopChapterItem.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/manga/components/DesktopChapterItem.kt @@ -25,26 +25,27 @@ actual fun Modifier.chapterItemModifier( markPreviousAsRead: () -> Unit, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = this - .onClick( - onClick = onClick, - onLongClick = onSelectChapter, - ) - .onClick( - onClick = onSelectChapter ?: onUnselectChapter ?: {}, - keyboardModifiers = { isCtrlPressed }, - ) - .onRightClickContextMenu( - items = { - getContextItems( - markRead = markRead, - markUnread = markUnread, - bookmarkChapter = bookmarkChapter, - unBookmarkChapter = unBookmarkChapter, - markPreviousAsRead = markPreviousAsRead, - ) - }, - ) +): Modifier = + this + .onClick( + onClick = onClick, + onLongClick = onSelectChapter, + ) + .onClick( + onClick = onSelectChapter ?: onUnselectChapter ?: {}, + keyboardModifiers = { isCtrlPressed }, + ) + .onRightClickContextMenu( + items = { + getContextItems( + markRead = markRead, + markUnread = markUnread, + bookmarkChapter = bookmarkChapter, + unBookmarkChapter = unBookmarkChapter, + markPreviousAsRead = markPreviousAsRead, + ) + }, + ) @Composable @Stable diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt index d0852750..e3f3c002 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/reader/DesktopReaderMenu.kt @@ -24,7 +24,6 @@ import ca.gosyer.jui.ui.util.lang.launchApplication import kotlinx.coroutines.DelicateCoroutinesApi actual class ReaderLauncher { - private var isOpen by mutableStateOf?>(null) actual fun launch( diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt index 0d18b068..8715cf88 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/settings/DesktopSettingsServerScreen.kt @@ -72,70 +72,72 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie } } -actual class SettingsServerHostViewModel @Inject constructor( - serverPreferences: ServerPreferences, - serverHostPreferences: ServerHostPreferences, - private val serverService: ServerService, - contextWrapper: ContextWrapper, -) : ViewModel(contextWrapper) { - val host = serverHostPreferences.host().asStateIn(scope) - val ip = serverHostPreferences.ip().asStateIn(scope) - val port = serverHostPreferences.port().asStringStateIn(scope) +actual class SettingsServerHostViewModel + @Inject + constructor( + serverPreferences: ServerPreferences, + serverHostPreferences: ServerHostPreferences, + private val serverService: ServerService, + contextWrapper: ContextWrapper, + ) : ViewModel(contextWrapper) { + val host = serverHostPreferences.host().asStateIn(scope) + val ip = serverHostPreferences.ip().asStateIn(scope) + val port = serverHostPreferences.port().asStringStateIn(scope) - // Proxy - val socksProxyEnabled = serverHostPreferences.socksProxyEnabled().asStateIn(scope) - val socksProxyHost = serverHostPreferences.socksProxyHost().asStateIn(scope) - val socksProxyPort = serverHostPreferences.socksProxyPort().asStringStateIn(scope) + // Proxy + val socksProxyEnabled = serverHostPreferences.socksProxyEnabled().asStateIn(scope) + val socksProxyHost = serverHostPreferences.socksProxyHost().asStateIn(scope) + val socksProxyPort = serverHostPreferences.socksProxyPort().asStringStateIn(scope) - // Misc - val debugLogsEnabled = serverHostPreferences.debugLogsEnabled().asStateIn(scope) - val systemTrayEnabled = serverHostPreferences.systemTrayEnabled().asStateIn(scope) + // Misc + val debugLogsEnabled = serverHostPreferences.debugLogsEnabled().asStateIn(scope) + val systemTrayEnabled = serverHostPreferences.systemTrayEnabled().asStateIn(scope) - // Downloader - val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) - val downloadAsCbz = serverHostPreferences.downloadAsCbz().asStateIn(scope) + // Downloader + val downloadPath = serverHostPreferences.downloadPath().asStateIn(scope) + val downloadAsCbz = serverHostPreferences.downloadAsCbz().asStateIn(scope) - // WebUI - val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) - val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope) + // WebUI + val webUIEnabled = serverHostPreferences.webUIEnabled().asStateIn(scope) + val openInBrowserEnabled = serverHostPreferences.openInBrowserEnabled().asStateIn(scope) - // Authentication - val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) - val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) - val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) + // Authentication + val basicAuthEnabled = serverHostPreferences.basicAuthEnabled().asStateIn(scope) + val basicAuthUsername = serverHostPreferences.basicAuthUsername().asStateIn(scope) + val basicAuthPassword = serverHostPreferences.basicAuthPassword().asStateIn(scope) - private val _serverSettingChanged = MutableStateFlow(false) - val serverSettingChanged = _serverSettingChanged.asStateFlow() - fun serverSettingChanged() { - _serverSettingChanged.value = true - } + private val _serverSettingChanged = MutableStateFlow(false) + val serverSettingChanged = _serverSettingChanged.asStateFlow() + fun serverSettingChanged() { + _serverSettingChanged.value = true + } - fun restartServer() { - if (serverSettingChanged.value) { - serverService.startServer() + fun restartServer() { + if (serverSettingChanged.value) { + serverService.startServer() + } + } + + // Handle password connection to hosted server + val auth = serverPreferences.auth().asStateIn(scope) + val authUsername = serverPreferences.authUsername().asStateIn(scope) + val authPassword = serverPreferences.authPassword().asStateIn(scope) + + init { + combine(basicAuthEnabled, basicAuthUsername, basicAuthPassword) { enabled, username, password -> + if (enabled) { + auth.value = Auth.BASIC + authUsername.value = username + authPassword.value = password + } else { + auth.value = Auth.NONE + authUsername.value = "" + authPassword.value = "" + } + }.launchIn(scope) } } - // Handle password connection to hosted server - val auth = serverPreferences.auth().asStateIn(scope) - val authUsername = serverPreferences.authUsername().asStateIn(scope) - val authPassword = serverPreferences.authPassword().asStateIn(scope) - - init { - combine(basicAuthEnabled, basicAuthUsername, basicAuthPassword) { enabled, username, password -> - if (enabled) { - auth.value = Auth.BASIC - authUsername.value = username - authPassword.value = password - } else { - auth.value = Auth.NONE - authUsername.value = "" - authPassword.value = "" - } - }.launchIn(scope) - } -} - fun LazyListScope.ServerHostItems( hostValue: Boolean, basicAuthEnabledValue: Boolean, diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/sources/components/DesktopSourcesMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/sources/components/DesktopSourcesMenu.kt index fcca9438..2574ccc2 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/sources/components/DesktopSourcesMenu.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/sources/components/DesktopSourcesMenu.kt @@ -14,12 +14,13 @@ import androidx.compose.ui.input.pointer.PointerButton actual fun Modifier.sourceSideMenuItem( onSourceTabClick: () -> Unit, onSourceCloseTabClick: () -> Unit, -): Modifier = this - .onClick( - matcher = PointerMatcher.mouse(PointerButton.Primary), - onClick = onSourceTabClick, - ) - .onClick( - matcher = PointerMatcher.mouse(PointerButton.Tertiary), - onClick = onSourceCloseTabClick, - ) +): Modifier = + this + .onClick( + matcher = PointerMatcher.mouse(PointerButton.Primary), + onClick = onSourceTabClick, + ) + .onClick( + matcher = PointerMatcher.mouse(PointerButton.Tertiary), + onClick = onSourceCloseTabClick, + ) diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/updates/components/DesktopUpdatesItem.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/updates/components/DesktopUpdatesItem.kt index a568db90..2d989859 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/updates/components/DesktopUpdatesItem.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/updates/components/DesktopUpdatesItem.kt @@ -24,25 +24,26 @@ actual fun Modifier.updatesItemModifier( unBookmarkChapter: (() -> Unit)?, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = this - .onClick( - onClick = onClick, - onLongClick = onSelectChapter, - ) - .onClick( - onClick = onSelectChapter ?: onUnselectChapter ?: {}, - keyboardModifiers = { isCtrlPressed }, - ) - .onRightClickContextMenu( - items = { - getContextItems( - markRead = markRead, - markUnread = markUnread, - bookmarkChapter = bookmarkChapter, - unBookmarkChapter = unBookmarkChapter, - ) - }, - ) +): Modifier = + this + .onClick( + onClick = onClick, + onLongClick = onSelectChapter, + ) + .onClick( + onClick = onSelectChapter ?: onUnselectChapter ?: {}, + keyboardModifiers = { isCtrlPressed }, + ) + .onRightClickContextMenu( + items = { + getContextItems( + markRead = markRead, + markUnread = markUnread, + bookmarkChapter = bookmarkChapter, + unBookmarkChapter = unBookmarkChapter, + ) + }, + ) @Composable @Stable diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt index 4fc6fd7c..da237dea 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/compose/Image.kt @@ -24,7 +24,11 @@ fun imageFromFile(file: Path): ImageBitmap { .toComposeImageBitmap() } -suspend fun imageFromUrl(client: Http, url: String, block: HttpRequestBuilder.() -> Unit): ImageBitmap { +suspend fun imageFromUrl( + client: Http, + url: String, + block: HttpRequestBuilder.() -> Unit, +): ImageBitmap { return client.get(url) { expectSuccess = true block() diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/lang/CoroutineExtensions.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/lang/CoroutineExtensions.kt index 6b3126b4..d337277b 100644 --- a/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/lang/CoroutineExtensions.kt +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/jui/ui/util/lang/CoroutineExtensions.kt @@ -13,6 +13,4 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @DelicateCoroutinesApi -fun launchApplication( - content: @Composable (ApplicationScope.() -> Unit), -) = GlobalScope.launchApplication(content) +fun launchApplication(content: @Composable (ApplicationScope.() -> Unit)) = GlobalScope.launchApplication(content) diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt index 118d9d2f..36cdb825 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/image/IosImageLoaderBuilder.kt @@ -22,11 +22,17 @@ import platform.Foundation.NSUserDomainMask actual fun OptionsBuilder.configure(contextWrapper: ContextWrapper) { } -actual fun ComponentRegistryBuilder.register(contextWrapper: ContextWrapper, http: Http) { +actual fun ComponentRegistryBuilder.register( + contextWrapper: ContextWrapper, + http: Http, +) { setupDefaultComponents(httpClient = { http }) } -actual fun DiskCacheBuilder.configure(contextWrapper: ContextWrapper, cacheDir: String) { +actual fun DiskCacheBuilder.configure( + contextWrapper: ContextWrapper, + cacheDir: String, +) { directory(getCacheDir().toPath() / cacheDir) maxSizeBytes(1024 * 1024 * 150) // 150 MB } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt index 1d87a0dc..b4797335 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/ActionIcon.kt @@ -13,7 +13,11 @@ import androidx.compose.ui.graphics.vector.ImageVector // todo @Composable -actual fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) { +actual fun ActionIcon( + onClick: () -> Unit, + contentDescription: String, + icon: ImageVector, +) { IconButton(onClick = onClick) { Icon(icon, contentDescription) } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/IosBackHandler.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/IosBackHandler.kt index fa711eae..bcaaf3d9 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/IosBackHandler.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/navigation/IosBackHandler.kt @@ -9,4 +9,7 @@ package ca.gosyer.jui.ui.base.navigation import androidx.compose.runtime.Composable @Composable -internal actual fun RealBackHandler(enabled: Boolean, onBack: () -> Unit) {} +internal actual fun RealBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) {} diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/prefs/IosColorExtensions.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/prefs/IosColorExtensions.kt index 1ef702b6..197ea6c4 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/prefs/IosColorExtensions.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/prefs/IosColorExtensions.kt @@ -16,16 +16,17 @@ import platform.UIKit.UIColor fun Color.toUIColor() = UIColor(red = red.toDouble(), green = green.toDouble(), blue = blue.toDouble(), alpha = 1.0) -internal actual fun Color.toHsv(): FloatArray = memScoped { - val uiColor = toUIColor() - val hue = alloc() - val saturation = alloc() - val brightness = alloc() - val alpha = alloc() - uiColor.getHue(hue.ptr, saturation.ptr, brightness.ptr, alpha.ptr) +internal actual fun Color.toHsv(): FloatArray = + memScoped { + val uiColor = toUIColor() + val hue = alloc() + val saturation = alloc() + val brightness = alloc() + val alpha = alloc() + uiColor.getHue(hue.ptr, saturation.ptr, brightness.ptr, alpha.ptr) - floatArrayOf(hue.value.toFloat(), saturation.value.toFloat(), brightness.value.toFloat()) -} + floatArrayOf(hue.value.toFloat(), saturation.value.toFloat(), brightness.value.toFloat()) + } internal actual fun hexStringToColor(hex: String): Color? { return try { diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt index 31471b90..ef916cc3 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/screen/BaseScreen.kt @@ -11,6 +11,5 @@ import cafe.adriel.voyager.core.screen.ScreenKey import cafe.adriel.voyager.core.screen.uniqueScreenKey actual abstract class BaseScreen : Screen { - override val key: ScreenKey = uniqueScreenKey } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/state/IosSavedStateHandle.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/state/IosSavedStateHandle.kt index 21e5eb00..b7577948 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/state/IosSavedStateHandle.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/base/state/IosSavedStateHandle.kt @@ -19,7 +19,10 @@ actual class SavedStateHandle { return regular[key] as T? } - actual operator fun set(key: String, value: T?) { + actual operator fun set( + key: String, + value: T?, + ) { regular[key] = value flows[key]?.value = value } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt index c0a3cebc..d9564fcc 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/categories/OpenCategories.kt @@ -12,7 +12,6 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator actual class CategoriesLauncher(private val navigator: Navigator?) { - actual fun open() { navigator?.push(CategoriesScreen()) } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/library/components/IosLibraryGrid.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/library/components/IosLibraryGrid.kt index d516f822..4dfbf2de 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/library/components/IosLibraryGrid.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/library/components/IosLibraryGrid.kt @@ -22,30 +22,31 @@ import ca.gosyer.jui.uicore.resources.stringResource actual fun Modifier.libraryMangaModifier( onClickManga: () -> Unit, onClickRemoveManga: () -> Unit, -): Modifier = composed { - var expanded by remember { mutableStateOf(false) } - DropdownMenu( - expanded, - onDismissRequest = { expanded = false }, - ) { - listOf( - stringResource(MR.strings.action_remove_favorite) to onClickRemoveManga, - ).forEach { (label, onClick) -> - DropdownMenuItem( - onClick = { - expanded = false - onClick() - }, - ) { - Text(text = label) +): Modifier = + composed { + var expanded by remember { mutableStateOf(false) } + DropdownMenu( + expanded, + onDismissRequest = { expanded = false }, + ) { + listOf( + stringResource(MR.strings.action_remove_favorite) to onClickRemoveManga, + ).forEach { (label, onClick) -> + DropdownMenuItem( + onClick = { + expanded = false + onClick() + }, + ) { + Text(text = label) + } } } - } - Modifier.combinedClickable( - onClick = { onClickManga() }, - onLongClick = { - expanded = true - }, - ) -} + Modifier.combinedClickable( + onClick = { onClickManga() }, + onLongClick = { + expanded = true + }, + ) + } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt index 7f809296..172d09e3 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/about/components/getDebugInfo.kt @@ -18,5 +18,5 @@ actual fun getDebugInfo(): String { Device model: ${device.model} System name: ${device.systemName} System version: ${device.systemVersion} - """.trimIndent() + """.trimIndent() } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt index fe68bf4f..dceef75c 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt @@ -11,8 +11,10 @@ import ca.gosyer.jui.uicore.vm.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import me.tatarka.inject.annotations.Inject -actual class DebugOverlayViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) { - actual val maxMemory: String - get() = "" - actual val usedMemoryFlow: MutableStateFlow = MutableStateFlow("") -} +actual class DebugOverlayViewModel + @Inject + constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) { + actual val maxMemory: String + get() = "" + actual val usedMemoryFlow: MutableStateFlow = MutableStateFlow("") + } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/manga/components/IosChapterItem.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/manga/components/IosChapterItem.kt index d45c8c95..98a4b13a 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/manga/components/IosChapterItem.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/manga/components/IosChapterItem.kt @@ -18,7 +18,8 @@ actual fun Modifier.chapterItemModifier( markPreviousAsRead: () -> Unit, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = combinedClickable( - onClick = onUnselectChapter ?: onClick, - onLongClick = onSelectChapter, -) +): Modifier = + combinedClickable( + onClick = onUnselectChapter ?: onClick, + onLongClick = onSelectChapter, + ) diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt index 025608fa..5072cd2d 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/reader/IosReaderMenu.kt @@ -14,7 +14,6 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen { - @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -27,7 +26,6 @@ class ReaderScreen(val chapterIndex: Int, val mangaId: Long) : Screen { } actual class ReaderLauncher(private val navigator: Navigator?) { - actual fun launch( chapterIndex: Int, mangaId: Long, diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt index 47bfbdb4..9f6e7c9b 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/settings/IosSettingsServerScreen.kt @@ -17,4 +17,6 @@ actual fun getServerHostItems(viewModel: @Composable () -> SettingsServerHostVie return {} } -actual class SettingsServerHostViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) +actual class SettingsServerHostViewModel + @Inject + constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/sources/components/IosSourcesMenu.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/sources/components/IosSourcesMenu.kt index ec473745..3a99e1e1 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/sources/components/IosSourcesMenu.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/sources/components/IosSourcesMenu.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier actual fun Modifier.sourceSideMenuItem( onSourceTabClick: () -> Unit, onSourceCloseTabClick: () -> Unit, -): Modifier = clickable( - onClick = onSourceTabClick, -) +): Modifier = + clickable( + onClick = onSourceTabClick, + ) diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/updates/components/IosUpdatesItem.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/updates/components/IosUpdatesItem.kt index f5f23e0d..557540ea 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/updates/components/IosUpdatesItem.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/updates/components/IosUpdatesItem.kt @@ -17,7 +17,8 @@ actual fun Modifier.updatesItemModifier( unBookmarkChapter: (() -> Unit)?, onSelectChapter: (() -> Unit)?, onUnselectChapter: (() -> Unit)?, -): Modifier = combinedClickable( - onClick = onUnselectChapter ?: onClick, - onLongClick = onSelectChapter, -) +): Modifier = + combinedClickable( + onClick = onUnselectChapter ?: onClick, + onLongClick = onSelectChapter, + ) diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt index 254e8954..1104b563 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt @@ -11,10 +11,12 @@ import platform.Foundation.NSString import platform.Foundation.localizedCaseInsensitiveCompare actual class CollatorComparator() : Comparator { - actual constructor(locale: Locale) : this() - actual override fun compare(source: String, target: String): Int { + actual override fun compare( + source: String, + target: String, + ): Int { @Suppress("CAST_NEVER_SUCCEEDS") return (source as NSString).localizedCaseInsensitiveCompare(target).toInt() } diff --git a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt index 56706c9d..4e1c8e85 100644 --- a/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt +++ b/presentation/src/iosMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt @@ -10,7 +10,10 @@ import platform.Foundation.NSString import platform.Foundation.stringWithFormat // Taken from https://github.com/icerockdev/moko-resources/blob/master/resources/src/appleMain/kotlin/dev/icerock/moko/resources/desc/Utils.kt -actual fun stringFormat(string: String, vararg args: Any?): String { +actual fun stringFormat( + string: String, + vararg args: Any?, +): String { // NSString format works with NSObjects via %@, we should change standard format to %@ val objcFormat = string.replace(Regex("%((?:\\.|\\d|\\$)*)[abcdefs]"), "%$1@") // bad but objc interop limited :( diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt index 4162ebdd..9169e5d2 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/main/components/DebugOverlayViewModel.kt @@ -16,32 +16,34 @@ import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import kotlin.time.Duration.Companion.milliseconds -actual class DebugOverlayViewModel @Inject constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) { - override val scope = MainScope() +actual class DebugOverlayViewModel + @Inject + constructor(contextWrapper: ContextWrapper) : ViewModel(contextWrapper) { + override val scope = MainScope() - val runtime: Runtime = Runtime.getRuntime() - actual val maxMemory = runtime.maxMemory().formatSize() - actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) + val runtime: Runtime = Runtime.getRuntime() + actual val maxMemory = runtime.maxMemory().formatSize() + actual val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) - init { - scope.launch { - while (true) { - usedMemoryFlow.value = runtime.usedMemory().formatSize() - delay(100.milliseconds) + init { + scope.launch { + while (true) { + usedMemoryFlow.value = runtime.usedMemory().formatSize() + delay(100.milliseconds) + } } } - } - private fun Long.formatSize(): String { - if (this < 1024) return "$this B" - val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10 - return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z]) - } + private fun Long.formatSize(): String { + if (this < 1024) return "$this B" + val z = (63 - java.lang.Long.numberOfLeadingZeros(this)) / 10 + return String.format("%.1f %sB", toDouble() / (1L shl z * 10), " KMGTPE"[z]) + } - private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory() + private fun Runtime.usedMemory(): Long = totalMemory() - freeMemory() - override fun onDispose() { - super.onDispose() - scope.cancel() + override fun onDispose() { + super.onDispose() + scope.cancel() + } } -} diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt index 3df9031c..317e1143 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/Collator.kt @@ -11,14 +11,16 @@ import ca.gosyer.jui.core.lang.toPlatform import java.text.Collator actual class CollatorComparator(private val collator: Collator) : Comparator { - actual constructor(locale: Locale) : this(Collator.getInstance(locale.toPlatform())) init { collator.strength = Collator.PRIMARY } - actual override fun compare(source: String, target: String): Int { + actual override fun compare( + source: String, + target: String, + ): Int { return collator.compare(source, target) } } diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt index d82a8149..d1461555 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/jui/ui/util/lang/StringFormat.kt @@ -8,7 +8,10 @@ package ca.gosyer.jui.ui.util.lang import java.util.Formatter -actual fun stringFormat(string: String, vararg args: Any?): String { +actual fun stringFormat( + string: String, + vararg args: Any?, +): String { return Formatter().use { it.format(string, *args).toString() } diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt index 8032ee29..f72bbcfa 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidBottomActionMenu.kt @@ -16,11 +16,12 @@ import androidx.compose.ui.composed actual fun Modifier.buttonModifier( onClick: () -> Unit, onHintClick: () -> Unit, -): Modifier = composed { - combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onLongClick = onHintClick, - onClick = onClick, - ) -} +): Modifier = + composed { + combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onLongClick = onHintClick, + onClick = onClick, + ) + } diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidDropDownMenu.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidDropDownMenu.kt index 09cf30d6..d1cd4ca3 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidDropDownMenu.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidDropDownMenu.kt @@ -52,19 +52,21 @@ internal actual fun RealDropdownMenuItem( ) @Stable -fun PopupProperties.toAndroidProperties() = AndroidPopupProperties( - focusable = focusable, - dismissOnBackPress = dismissOnBackPress, - dismissOnClickOutside = dismissOnClickOutside, - securePolicy = securePolicy.toAndroidSecureFlagPolicy(), - excludeFromSystemGesture = excludeFromSystemGesture, - clippingEnabled = clippingEnabled, - usePlatformDefaultWidth = usePlatformDefaultWidth, -) +fun PopupProperties.toAndroidProperties() = + AndroidPopupProperties( + focusable = focusable, + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + securePolicy = securePolicy.toAndroidSecureFlagPolicy(), + excludeFromSystemGesture = excludeFromSystemGesture, + clippingEnabled = clippingEnabled, + usePlatformDefaultWidth = usePlatformDefaultWidth, + ) @Stable -fun SecureFlagPolicy.toAndroidSecureFlagPolicy() = when (this) { - SecureFlagPolicy.Inherit -> AndroidSecureFlagPolicy.Inherit - SecureFlagPolicy.SecureOn -> AndroidSecureFlagPolicy.SecureOn - SecureFlagPolicy.SecureOff -> AndroidSecureFlagPolicy.SecureOff -} +fun SecureFlagPolicy.toAndroidSecureFlagPolicy() = + when (this) { + SecureFlagPolicy.Inherit -> AndroidSecureFlagPolicy.Inherit + SecureFlagPolicy.SecureOn -> AndroidSecureFlagPolicy.SecureOn + SecureFlagPolicy.SecureOff -> AndroidSecureFlagPolicy.SecureOff + } diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidScrollbar.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidScrollbar.kt index e5f9f881..f685c5b8 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidScrollbar.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/components/AndroidScrollbar.kt @@ -106,18 +106,14 @@ internal actual fun RealHorizontalScrollbar( } @Composable -actual fun rememberScrollbarAdapter( - scrollState: ScrollState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter { return remember(scrollState) { ScrollStateScrollbarAdapter(scrollState) } } @Composable -actual fun rememberScrollbarAdapter( - scrollState: LazyListState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter { return remember(scrollState) { LazyListStateScrollbarAdapter(scrollState) } @@ -153,76 +149,78 @@ private fun Modifier.drawScrollbar( state: ScrollState, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation = orientation, - reverseScrolling = reverseScrolling, - scrollFlow = snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val showScrollbar = state.maxValue > 0 - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val totalSize = canvasSize + state.maxValue - val thumbSize = canvasSize / totalSize * canvasSize - val startOffset = state.value / totalSize * canvasSize - val drawScrollbar = onDrawScrollbar( +): Modifier = + drawScrollbar( orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() + reverseScrolling = reverseScrolling, + scrollFlow = snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val showScrollbar = state.maxValue > 0 + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val totalSize = canvasSize + state.maxValue + val thumbSize = canvasSize / totalSize * canvasSize + val startOffset = state.value / totalSize * canvasSize + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } -} private fun Modifier.drawScrollbar( state: LazyListState, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation, - reverseScrolling, - snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val layoutInfo = state.layoutInfo - val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset - val items = layoutInfo.visibleItemsInfo - val itemsSize = items.fastSumBy { it.size } - val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize - val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size - val totalSize = estimatedItemSize * layoutInfo.totalItemsCount - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val thumbSize = viewportSize / totalSize * canvasSize - val startOffset = if (items.isEmpty()) { - 0f - } else { - items - .first() - .run { - (estimatedItemSize * index - offset) / totalSize * canvasSize - } +): Modifier = + drawScrollbar( + orientation, + reverseScrolling, + snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val items = layoutInfo.visibleItemsInfo + val itemsSize = items.fastSumBy { it.size } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val thumbSize = viewportSize / totalSize * canvasSize + val startOffset = if (items.isEmpty()) { + 0f + } else { + items + .first() + .run { + (estimatedItemSize * index - offset) / totalSize * canvasSize + } + } + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } - val drawScrollbar = onDrawScrollbar( - orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() - } -} private fun Modifier.drawScrollbar( state: LazyGridState, @@ -230,50 +228,51 @@ private fun Modifier.drawScrollbar( spacing: Dp, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation, - reverseScrolling, - snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val layoutInfo = state.layoutInfo - val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset - val items = layoutInfo.visibleItemsInfo - // TODO Fix spacing - val itemsSize = items.chunked( - with(gridCells) { - calculateCrossAxisCellSizes(viewportSize, spacing.roundToPx()).size - }, - ).sumOf { it.fastMaxBy { it.size.height }?.size?.height ?: 0 } - val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize - val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size - val totalSize = estimatedItemSize * layoutInfo.totalItemsCount - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val thumbSize = viewportSize / totalSize * canvasSize - val startOffset = if (items.isEmpty()) { - 0f - } else { - items - .first() - .run { - (estimatedItemSize * index - if (orientation == Orientation.Vertical) offset.y else offset.x) / totalSize * canvasSize - } +): Modifier = + drawScrollbar( + orientation, + reverseScrolling, + snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val items = layoutInfo.visibleItemsInfo + // TODO Fix spacing + val itemsSize = items.chunked( + with(gridCells) { + calculateCrossAxisCellSizes(viewportSize, spacing.roundToPx()).size + }, + ).sumOf { it.fastMaxBy { it.size.height }?.size?.height ?: 0 } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val thumbSize = viewportSize / totalSize * canvasSize + val startOffset = if (items.isEmpty()) { + 0f + } else { + items + .first() + .run { + (estimatedItemSize * index - if (orientation == Orientation.Vertical) offset.y else offset.x) / totalSize * canvasSize + } + } + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } - val drawScrollbar = onDrawScrollbar( - orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() - } -} private fun CacheDrawScope.onDrawScrollbar( orientation: Orientation, @@ -326,33 +325,34 @@ private fun Modifier.drawScrollbar( color: Color, alpha: Float, ) -> DrawResult, -): Modifier = composed { - val isScrollInProgress by scrollFlow.collectAsState(initial = false) - val alpha = remember { Animatable(0f) } - LaunchedEffect(isScrollInProgress, alpha) { - if (isScrollInProgress) { - alpha.snapTo(1f) +): Modifier = + composed { + val isScrollInProgress by scrollFlow.collectAsState(initial = false) + val alpha = remember { Animatable(0f) } + LaunchedEffect(isScrollInProgress, alpha) { + if (isScrollInProgress) { + alpha.snapTo(1f) + } else { + delay(ViewConfiguration.getScrollDefaultDelay().toLong()) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } + } + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val reverseDirection = if (orientation == Orientation.Horizontal) { + if (isLtr) reverseScrolling else !reverseScrolling } else { - delay(ViewConfiguration.getScrollDefaultDelay().toLong()) - alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + reverseScrolling } - } - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val reverseDirection = if (orientation == Orientation.Horizontal) { - if (isLtr) reverseScrolling else !reverseScrolling - } else { - reverseScrolling - } - val atEnd = if (orientation == Orientation.Vertical) isLtr else true + val atEnd = if (orientation == Orientation.Vertical) isLtr else true - // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 - val thickness = with(LocalDensity.current) { Thickness.toPx() } - val color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) - Modifier - .drawWithCache { - onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha.value) - } -} + // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 + val thickness = with(LocalDensity.current) { Thickness.toPx() } + val color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) + Modifier + .drawWithCache { + onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha.value) + } + } private val Thickness = 4.dp private val FadeOutAnimationSpec = diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/resources/AndroidStringResource.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/resources/AndroidStringResource.kt index 7954d91f..cd58c739 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/resources/AndroidStringResource.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/resources/AndroidStringResource.kt @@ -23,19 +23,29 @@ actual fun stringResource(resource: StringResource): String { } @Composable -actual fun stringResource(resource: StringResource, vararg args: Any): String { +actual fun stringResource( + resource: StringResource, + vararg args: Any, +): String { val context = LocalContext.current return StringDesc.ResourceFormatted(resource, *args).toString(context) } @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int): String { +actual fun stringResource( + resource: PluralsResource, + quantity: Int, +): String { val context = LocalContext.current return StringDesc.Plural(resource, quantity).toString(context) } @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String { +actual fun stringResource( + resource: PluralsResource, + quantity: Int, + vararg args: Any, +): String { val context = LocalContext.current return StringDesc.PluralFormatted(resource, quantity, *args).toString(context) } diff --git a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt index 5e759e37..f59eb647 100644 --- a/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt +++ b/ui-core/src/androidMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt @@ -14,23 +14,31 @@ import dev.icerock.moko.resources.desc.desc import dev.icerock.moko.resources.format import me.tatarka.inject.annotations.Inject -actual class ContextWrapper @Inject constructor(context: Context) : ContextWrapper(context) { - actual fun toPlatformString(stringResource: StringResource): String { - return stringResource.desc().toString(this) - } +actual class ContextWrapper + @Inject + constructor(context: Context) : ContextWrapper(context) { + actual fun toPlatformString(stringResource: StringResource): String { + return stringResource.desc().toString(this) + } - actual fun toPlatformString(stringResource: StringResource, vararg args: Any): String { - return stringResource.format(*args).toString(this) - } + actual fun toPlatformString( + stringResource: StringResource, + vararg args: Any, + ): String { + return stringResource.format(*args).toString(this) + } - actual fun toast(string: String, length: Length) { - Toast.makeText( - this, - string, - when (length) { - Length.SHORT -> Toast.LENGTH_SHORT - Length.LONG -> Toast.LENGTH_LONG - }, - ).show() + actual fun toast( + string: String, + length: Length, + ) { + Toast.makeText( + this, + string, + when (length) { + Length.SHORT -> Toast.LENGTH_SHORT + Length.LONG -> Toast.LENGTH_LONG + }, + ).show() + } } -} diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropDownMenu.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropDownMenu.kt index 7c7f845f..6a77617c 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropDownMenu.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/DropDownMenu.kt @@ -45,16 +45,18 @@ enum class SecureFlagPolicy { } @Immutable -data class PopupProperties @ExperimentalComposeUiApi constructor( - val focusable: Boolean = false, - val dismissOnBackPress: Boolean = true, - val dismissOnClickOutside: Boolean = true, - val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, - val excludeFromSystemGesture: Boolean = true, - val clippingEnabled: Boolean = true, - @property:ExperimentalComposeUiApi - val usePlatformDefaultWidth: Boolean = false, -) +data class PopupProperties + @ExperimentalComposeUiApi + constructor( + val focusable: Boolean = false, + val dismissOnBackPress: Boolean = true, + val dismissOnClickOutside: Boolean = true, + val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, + val excludeFromSystemGesture: Boolean = true, + val clippingEnabled: Boolean = true, + @property:ExperimentalComposeUiApi + val usePlatformDefaultWidth: Boolean = false, + ) @Composable internal expect fun RealDropdownMenu( diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/LoadingScreen.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/LoadingScreen.kt index a50999ba..0cd19a27 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/LoadingScreen.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/LoadingScreen.kt @@ -23,7 +23,7 @@ import ca.gosyer.jui.uicore.resources.stringResource fun LoadingScreen( isLoading: Boolean = true, modifier: Modifier = Modifier.fillMaxSize(), - /*@FloatRange(from = 0.0, to = 1.0)*/ + // @FloatRange(from = 0.0, to = 1.0) progress: Float = 0.0F, errorMessage: String? = null, retryMessage: String = stringResource(MR.strings.action_retry), diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Modifier.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Modifier.kt index 27897401..b4783875 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Modifier.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Modifier.kt @@ -13,13 +13,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.alpha -fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed { - if (isSelected) { - val alpha = if (isSystemInDarkTheme()) 0.08f else 0.22f - background(MaterialTheme.colors.secondary.copy(alpha = alpha)) - } else { - this +fun Modifier.selectedBackground(isSelected: Boolean): Modifier = + composed { + if (isSelected) { + val alpha = if (isSystemInDarkTheme()) 0.08f else 0.22f + background(MaterialTheme.colors.secondary.copy(alpha = alpha)) + } else { + this + } } -} fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha) diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Scrollbar.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Scrollbar.kt index 589aed70..acd81073 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Scrollbar.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Scrollbar.kt @@ -60,36 +60,34 @@ fun HorizontalScrollbar( ) = RealHorizontalScrollbar(adapter, modifier, reverseLayout, style, interactionSource) @Composable -expect fun rememberScrollbarAdapter( - scrollState: ScrollState, -): ScrollbarAdapter +expect fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter @Composable -expect fun rememberScrollbarAdapter( - scrollState: LazyListState, -): ScrollbarAdapter +expect fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter @Composable fun rememberVerticalScrollbarAdapter( scrollState: LazyGridState, gridCells: GridCells, arrangement: Arrangement.Vertical? = null, -): ScrollbarAdapter = realRememberVerticalScrollbarAdapter( - scrollState, - gridCells, - arrangement, -) +): ScrollbarAdapter = + realRememberVerticalScrollbarAdapter( + scrollState, + gridCells, + arrangement, + ) @Composable fun rememberHorizontalScrollbarAdapter( scrollState: LazyGridState, gridCells: GridCells, arrangement: Arrangement.Horizontal? = null, -): ScrollbarAdapter = realRememberHorizontalScrollbarAdapter( - scrollState, - gridCells, - arrangement, -) +): ScrollbarAdapter = + realRememberHorizontalScrollbarAdapter( + scrollState, + gridCells, + arrangement, + ) @Composable internal expect fun realRememberVerticalScrollbarAdapter( diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Spinner.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Spinner.kt index 0a961253..77aeb513 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Spinner.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/components/Spinner.kt @@ -31,7 +31,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun Spinner(modifier: Modifier, items: List, selectedItemIndex: Int, onSelectItem: (Int) -> Unit) { +fun Spinner( + modifier: Modifier, + items: List, + selectedItemIndex: Int, + onSelectItem: (Int) -> Unit, +) { var expanded by remember { mutableStateOf(false) } val shape = RoundedCornerShape(4.dp) Box( diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt index 009ecdd2..f22f526e 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/pager/Pager.kt @@ -167,11 +167,10 @@ private fun Pager( } @Composable -fun rememberPagerState( - initialPage: Int = 0, -) = rememberSaveable(saver = PagerState.Saver) { - PagerState(currentPage = initialPage) -} +fun rememberPagerState(initialPage: Int = 0) = + rememberSaveable(saver = PagerState.Saver) { + PagerState(currentPage = initialPage) + } @Stable class PagerState( @@ -234,7 +233,6 @@ private fun lazyListSnapLayoutInfoProvider( layoutSize / 2f - itemSize / 2f }, ) = object : SnapLayoutInfoProvider { - private val layoutInfo: LazyListLayoutInfo get() = lazyListState.layoutInfo @@ -263,13 +261,14 @@ private fun lazyListSnapLayoutInfoProvider( return lowerBoundOffset.rangeTo(upperBoundOffset) } - override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) { - if (visibleItemsInfo.isNotEmpty()) { - visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat() - } else { - 0f + override fun Density.calculateSnapStepSize(): Float = + with(layoutInfo) { + if (visibleItemsInfo.isNotEmpty()) { + visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat() + } else { + 0f + } } - } } @Composable diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/ColorPreference.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/ColorPreference.kt index c47bfce5..3c298830 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/ColorPreference.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/ColorPreference.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.stateIn class ColorPreference( private val preference: Preference, ) : Preference { - override fun key(): String { return preference.key() } diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/PreferenceMutableState.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/PreferenceMutableState.kt index c67174c9..90b1a79e 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/PreferenceMutableState.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/prefs/PreferenceMutableState.kt @@ -17,7 +17,6 @@ class PreferenceMutableStateFlow( scope: CoroutineScope, private val state: MutableStateFlow = MutableStateFlow(preference.get()), ) : MutableStateFlow by state { - init { preference.changes() .onEach { state.value = it } diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/resources/StringResource.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/resources/StringResource.kt index ed019e1d..3b955daf 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/resources/StringResource.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/resources/StringResource.kt @@ -14,10 +14,20 @@ import dev.icerock.moko.resources.StringResource expect fun stringResource(resource: StringResource): String @Composable -expect fun stringResource(resource: StringResource, vararg args: Any): String +expect fun stringResource( + resource: StringResource, + vararg args: Any, +): String @Composable -expect fun stringResource(resource: PluralsResource, quantity: Int): String +expect fun stringResource( + resource: PluralsResource, + quantity: Int, +): String @Composable -expect fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String +expect fun stringResource( + resource: PluralsResource, + quantity: Int, + vararg args: Any, +): String diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/theme/ExtraColors.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/theme/ExtraColors.kt index 6a46ce21..6701d0d1 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/theme/ExtraColors.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/theme/ExtraColors.kt @@ -30,10 +30,11 @@ class ExtraColors( fun copy( tertiary: Color = this.tertiary, onTertiary: Color = this.onTertiary, - ): ExtraColors = ExtraColors( - tertiary, - onTertiary, - ) + ): ExtraColors = + ExtraColors( + tertiary, + onTertiary, + ) override fun toString(): String { return "ExtraColors(" + @@ -44,7 +45,10 @@ class ExtraColors( companion object { @Composable - fun WithExtraColors(extraColors: ExtraColors, content: @Composable () -> Unit) { + fun WithExtraColors( + extraColors: ExtraColors, + content: @Composable () -> Unit, + ) { CompositionLocalProvider( LocalExtraColors provides extraColors, content = content, diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt index f788fe19..928aad77 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt @@ -10,8 +10,14 @@ import dev.icerock.moko.resources.StringResource expect class ContextWrapper { fun toPlatformString(stringResource: StringResource): String - fun toPlatformString(stringResource: StringResource, vararg args: Any): String - fun toast(string: String, length: Length) + fun toPlatformString( + stringResource: StringResource, + vararg args: Any, + ): String + fun toast( + string: String, + length: Length, + ) } enum class Length { SHORT, diff --git a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt index c42e2d91..ff7b56ce 100644 --- a/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt +++ b/ui-core/src/commonMain/kotlin/ca/gosyer/jui/uicore/vm/ViewModel.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch abstract class ViewModel(private val contextWrapper: ContextWrapper) : ScreenModel { - protected open val scope: CoroutineScope get() = coroutineScope @@ -39,7 +38,10 @@ abstract class ViewModel(private val contextWrapper: ContextWrapper) : ScreenMod fun StringResource.toPlatformString(vararg args: Any): String { return contextWrapper.toPlatformString(this, *args) } - fun toast(string: String, length: Length = Length.SHORT) { + fun toast( + string: String, + length: Length = Length.SHORT, + ) { scope.launchUI { contextWrapper.toast(string, length) } diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt index fbfb9515..d464cfd7 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopBottomActionMenu.kt @@ -23,20 +23,21 @@ import kotlin.time.Duration.Companion.seconds actual fun Modifier.buttonModifier( onClick: () -> Unit, onHintClick: () -> Unit, -): Modifier = composed { - val interactionSource = remember { MutableInteractionSource() } - LaunchedEffect(interactionSource) { - launch { - interactionSource.interactions - .mapLatest { - if (it !is HoverInteraction.Enter) return@mapLatest - delay(2.seconds) - onHintClick() - } - .collect() +): Modifier = + composed { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + launch { + interactionSource.interactions + .mapLatest { + if (it !is HoverInteraction.Enter) return@mapLatest + delay(2.seconds) + onHintClick() + } + .collect() + } } + interactionSource.interactions + onClick(onClick = onClick) + .hoverable(interactionSource) } - interactionSource.interactions - onClick(onClick = onClick) - .hoverable(interactionSource) -} diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopScrollbar.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopScrollbar.kt index c3d9a68d..cbc1fcac 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopScrollbar.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/components/DesktopScrollbar.kt @@ -56,16 +56,12 @@ internal actual fun RealHorizontalScrollbar( ) @Composable -actual fun rememberScrollbarAdapter( - scrollState: ScrollState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter { return androidx.compose.foundation.rememberScrollbarAdapter(scrollState) } @Composable -actual fun rememberScrollbarAdapter( - scrollState: LazyListState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter { return androidx.compose.foundation.rememberScrollbarAdapter(scrollState) } diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/insets/DesktopWindowInsets.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/insets/DesktopWindowInsets.kt index dcf1147d..32eb3732 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/insets/DesktopWindowInsets.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/insets/DesktopWindowInsets.kt @@ -19,8 +19,14 @@ import androidx.compose.ui.unit.LayoutDirection @Immutable object EmptyWindowInsets : WindowInsets { override fun getBottom(density: Density): Int = 0 - override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 - override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getLeft( + density: Density, + layoutDirection: LayoutDirection, + ): Int = 0 + override fun getRight( + density: Density, + layoutDirection: LayoutDirection, + ): Int = 0 override fun getTop(density: Density): Int = 0 } diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopFileResource.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopFileResource.kt index b89f30a6..c895b8c3 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopFileResource.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopFileResource.kt @@ -14,16 +14,18 @@ import ca.gosyer.jui.core.lang.withIOContext import dev.icerock.moko.resources.FileResource @Composable -actual fun FileResource.rememberReadText(): String = remember { - readText() -} +actual fun FileResource.rememberReadText(): String = + remember { + readText() + } @Composable -actual fun FileResource.readTextAsync(): State = produceState( - null, - this.filePath, -) { - withIOContext { - value = readText() +actual fun FileResource.readTextAsync(): State = + produceState( + null, + this.filePath, + ) { + withIOContext { + value = readText() + } } -} diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopStringResource.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopStringResource.kt index 40dbc3be..d5b0215e 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopStringResource.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/resources/DesktopStringResource.kt @@ -16,17 +16,23 @@ import dev.icerock.moko.resources.desc.ResourceFormatted import dev.icerock.moko.resources.desc.StringDesc @Composable -actual fun stringResource(resource: StringResource): String = - StringDesc.Resource(resource).localized() +actual fun stringResource(resource: StringResource): String = StringDesc.Resource(resource).localized() @Composable -actual fun stringResource(resource: StringResource, vararg args: Any): String = - StringDesc.ResourceFormatted(resource, *args).localized() +actual fun stringResource( + resource: StringResource, + vararg args: Any, +): String = StringDesc.ResourceFormatted(resource, *args).localized() @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int): String = - StringDesc.Plural(resource, quantity).localized() +actual fun stringResource( + resource: PluralsResource, + quantity: Int, +): String = StringDesc.Plural(resource, quantity).localized() @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String = - StringDesc.PluralFormatted(resource, quantity, *args).localized() +actual fun stringResource( + resource: PluralsResource, + quantity: Int, + vararg args: Any, +): String = StringDesc.PluralFormatted(resource, quantity, *args).localized() diff --git a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt index 094705f7..88a66db6 100644 --- a/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt +++ b/ui-core/src/desktopMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt @@ -22,10 +22,16 @@ actual class ContextWrapper { actual fun toPlatformString(stringResource: StringResource): String { return stringResource.localized() } - actual fun toPlatformString(stringResource: StringResource, vararg args: Any): String { + actual fun toPlatformString( + stringResource: StringResource, + vararg args: Any, + ): String { return stringResource.format(*args).localized() } - actual fun toast(string: String, length: Length) { + actual fun toast( + string: String, + length: Length, + ) { GlobalScope.launchDefault { _toasts.emit(string to length) } diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosBottomActionMenu.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosBottomActionMenu.kt index 8032ee29..f72bbcfa 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosBottomActionMenu.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosBottomActionMenu.kt @@ -16,11 +16,12 @@ import androidx.compose.ui.composed actual fun Modifier.buttonModifier( onClick: () -> Unit, onHintClick: () -> Unit, -): Modifier = composed { - combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onLongClick = onHintClick, - onClick = onClick, - ) -} +): Modifier = + composed { + combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onLongClick = onHintClick, + onClick = onClick, + ) + } diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosScrollbar.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosScrollbar.kt index 229d0776..0f33bba7 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosScrollbar.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/components/IosScrollbar.kt @@ -105,18 +105,14 @@ internal actual fun RealHorizontalScrollbar( } @Composable -actual fun rememberScrollbarAdapter( - scrollState: ScrollState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: ScrollState): ScrollbarAdapter { return remember(scrollState) { ScrollStateScrollbarAdapter(scrollState) } } @Composable -actual fun rememberScrollbarAdapter( - scrollState: LazyListState, -): ScrollbarAdapter { +actual fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter { return remember(scrollState) { LazyListStateScrollbarAdapter(scrollState) } @@ -152,76 +148,78 @@ private fun Modifier.drawScrollbar( state: ScrollState, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation = orientation, - reverseScrolling = reverseScrolling, - scrollFlow = snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val showScrollbar = state.maxValue > 0 - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val totalSize = canvasSize + state.maxValue - val thumbSize = canvasSize / totalSize * canvasSize - val startOffset = state.value / totalSize * canvasSize - val drawScrollbar = onDrawScrollbar( +): Modifier = + drawScrollbar( orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() + reverseScrolling = reverseScrolling, + scrollFlow = snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val showScrollbar = state.maxValue > 0 + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val totalSize = canvasSize + state.maxValue + val thumbSize = canvasSize / totalSize * canvasSize + val startOffset = state.value / totalSize * canvasSize + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } -} private fun Modifier.drawScrollbar( state: LazyListState, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation, - reverseScrolling, - snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val layoutInfo = state.layoutInfo - val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset - val items = layoutInfo.visibleItemsInfo - val itemsSize = items.fastSumBy { it.size } - val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize - val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size - val totalSize = estimatedItemSize * layoutInfo.totalItemsCount - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val thumbSize = viewportSize / totalSize * canvasSize - val startOffset = if (items.isEmpty()) { - 0f - } else { - items - .first() - .run { - (estimatedItemSize * index - offset) / totalSize * canvasSize - } +): Modifier = + drawScrollbar( + orientation, + reverseScrolling, + snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val items = layoutInfo.visibleItemsInfo + val itemsSize = items.fastSumBy { it.size } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val thumbSize = viewportSize / totalSize * canvasSize + val startOffset = if (items.isEmpty()) { + 0f + } else { + items + .first() + .run { + (estimatedItemSize * index - offset) / totalSize * canvasSize + } + } + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } - val drawScrollbar = onDrawScrollbar( - orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() - } -} private fun Modifier.drawScrollbar( state: LazyGridState, @@ -229,50 +227,51 @@ private fun Modifier.drawScrollbar( spacing: Dp, orientation: Orientation, reverseScrolling: Boolean, -): Modifier = drawScrollbar( - orientation, - reverseScrolling, - snapshotFlow { state.isScrollInProgress }, -) { reverseDirection, atEnd, thickness, color, alpha -> - val layoutInfo = state.layoutInfo - val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset - val items = layoutInfo.visibleItemsInfo - // TODO Fix spacing - val itemsSize = items.chunked( - with(gridCells) { - calculateCrossAxisCellSizes(viewportSize, spacing.roundToPx()).size - }, - ).sumOf { it.fastMaxBy { it.size.height }?.size?.height ?: 0 } - val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize - val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size - val totalSize = estimatedItemSize * layoutInfo.totalItemsCount - val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height - val thumbSize = viewportSize / totalSize * canvasSize - val startOffset = if (items.isEmpty()) { - 0f - } else { - items - .first() - .run { - (estimatedItemSize * index - if (orientation == Orientation.Vertical) offset.y else offset.x) / totalSize * canvasSize - } +): Modifier = + drawScrollbar( + orientation, + reverseScrolling, + snapshotFlow { state.isScrollInProgress }, + ) { reverseDirection, atEnd, thickness, color, alpha -> + val layoutInfo = state.layoutInfo + val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset + val items = layoutInfo.visibleItemsInfo + // TODO Fix spacing + val itemsSize = items.chunked( + with(gridCells) { + calculateCrossAxisCellSizes(viewportSize, spacing.roundToPx()).size + }, + ).sumOf { it.fastMaxBy { it.size.height }?.size?.height ?: 0 } + val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize + val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size + val totalSize = estimatedItemSize * layoutInfo.totalItemsCount + val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height + val thumbSize = viewportSize / totalSize * canvasSize + val startOffset = if (items.isEmpty()) { + 0f + } else { + items + .first() + .run { + (estimatedItemSize * index - if (orientation == Orientation.Vertical) offset.y else offset.x) / totalSize * canvasSize + } + } + val drawScrollbar = onDrawScrollbar( + orientation = orientation, + reverseDirection = reverseDirection, + atEnd = atEnd, + showScrollbar = showScrollbar, + thickness = thickness, + color = color, + alpha = alpha, + thumbSize = thumbSize, + startOffset = startOffset, + ) + onDrawWithContent { + drawContent() + drawScrollbar() + } } - val drawScrollbar = onDrawScrollbar( - orientation = orientation, - reverseDirection = reverseDirection, - atEnd = atEnd, - showScrollbar = showScrollbar, - thickness = thickness, - color = color, - alpha = alpha, - thumbSize = thumbSize, - startOffset = startOffset, - ) - onDrawWithContent { - drawContent() - drawScrollbar() - } -} private fun CacheDrawScope.onDrawScrollbar( orientation: Orientation, @@ -325,33 +324,34 @@ private fun Modifier.drawScrollbar( color: Color, alpha: Float, ) -> DrawResult, -): Modifier = composed { - val isScrollInProgress by scrollFlow.collectAsState(initial = false) - val alpha = remember { Animatable(0f) } - LaunchedEffect(isScrollInProgress, alpha) { - if (isScrollInProgress) { - alpha.snapTo(1f) +): Modifier = + composed { + val isScrollInProgress by scrollFlow.collectAsState(initial = false) + val alpha = remember { Animatable(0f) } + LaunchedEffect(isScrollInProgress, alpha) { + if (isScrollInProgress) { + alpha.snapTo(1f) + } else { + delay(300) + alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + } + } + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val reverseDirection = if (orientation == Orientation.Horizontal) { + if (isLtr) reverseScrolling else !reverseScrolling } else { - delay(300) - alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) + reverseScrolling } - } - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val reverseDirection = if (orientation == Orientation.Horizontal) { - if (isLtr) reverseScrolling else !reverseScrolling - } else { - reverseScrolling - } - val atEnd = if (orientation == Orientation.Vertical) isLtr else true + val atEnd = if (orientation == Orientation.Vertical) isLtr else true - // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 - val thickness = with(LocalDensity.current) { Thickness.toPx() } - val color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) - Modifier - .drawWithCache { - onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha.value) - } -} + // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 + val thickness = with(LocalDensity.current) { Thickness.toPx() } + val color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) + Modifier + .drawWithCache { + onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha.value) + } + } private val Thickness = 4.dp private val FadeOutAnimationSpec = diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/insets/IosWindowInsets.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/insets/IosWindowInsets.kt index dcf1147d..32eb3732 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/insets/IosWindowInsets.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/insets/IosWindowInsets.kt @@ -19,8 +19,14 @@ import androidx.compose.ui.unit.LayoutDirection @Immutable object EmptyWindowInsets : WindowInsets { override fun getBottom(density: Density): Int = 0 - override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 - override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getLeft( + density: Density, + layoutDirection: LayoutDirection, + ): Int = 0 + override fun getRight( + density: Density, + layoutDirection: LayoutDirection, + ): Int = 0 override fun getTop(density: Density): Int = 0 } diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosFileResource.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosFileResource.kt index 572e70b6..1fac0e5d 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosFileResource.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosFileResource.kt @@ -14,16 +14,18 @@ import ca.gosyer.jui.core.lang.withIOContext import dev.icerock.moko.resources.FileResource @Composable -actual fun FileResource.rememberReadText(): String = remember { - readText() -} +actual fun FileResource.rememberReadText(): String = + remember { + readText() + } @Composable -actual fun FileResource.readTextAsync(): State = produceState( - null, - this.path, -) { - withIOContext { - value = readText() +actual fun FileResource.readTextAsync(): State = + produceState( + null, + this.path, + ) { + withIOContext { + value = readText() + } } -} diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosStringResource.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosStringResource.kt index 06c84d9a..2fc7faa0 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosStringResource.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/resources/IosStringResource.kt @@ -21,17 +21,23 @@ expect fun PluralStringDesc.localized(): String expect fun PluralFormattedStringDesc.localized(): String @Composable -actual fun stringResource(resource: StringResource): String = - StringDesc.Resource(resource).localized() +actual fun stringResource(resource: StringResource): String = StringDesc.Resource(resource).localized() @Composable -actual fun stringResource(resource: StringResource, vararg args: Any): String = - StringDesc.ResourceFormatted(resource, *args).localized() +actual fun stringResource( + resource: StringResource, + vararg args: Any, +): String = StringDesc.ResourceFormatted(resource, *args).localized() @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int): String = - StringDesc.Plural(resource, quantity).localized() +actual fun stringResource( + resource: PluralsResource, + quantity: Int, +): String = StringDesc.Plural(resource, quantity).localized() @Composable -actual fun stringResource(resource: PluralsResource, quantity: Int, vararg args: Any): String = - StringDesc.PluralFormatted(resource, quantity, *args).localized() +actual fun stringResource( + resource: PluralsResource, + quantity: Int, + vararg args: Any, +): String = StringDesc.PluralFormatted(resource, quantity, *args).localized() diff --git a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt index 18fc5d44..7b9121a2 100644 --- a/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt +++ b/ui-core/src/iosMain/kotlin/ca/gosyer/jui/uicore/vm/ContextWrapper.kt @@ -15,19 +15,27 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import me.tatarka.inject.annotations.Inject -actual class ContextWrapper @Inject constructor() { - private val _toasts = MutableSharedFlow>() - val toasts = _toasts.asSharedFlow() +actual class ContextWrapper + @Inject + constructor() { + private val _toasts = MutableSharedFlow>() + val toasts = _toasts.asSharedFlow() - actual fun toPlatformString(stringResource: StringResource): String { - return stringResource.desc().localized() - } - actual fun toPlatformString(stringResource: StringResource, vararg args: Any): String { - return stringResource.format(*args).localized() - } - actual fun toast(string: String, length: Length) { - GlobalScope.launchDefault { - _toasts.emit(string to length) + actual fun toPlatformString(stringResource: StringResource): String { + return stringResource.desc().localized() + } + actual fun toPlatformString( + stringResource: StringResource, + vararg args: Any, + ): String { + return stringResource.format(*args).localized() + } + actual fun toast( + string: String, + length: Length, + ) { + GlobalScope.launchDefault { + _toasts.emit(string to length) + } } } -}