mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Performance improvements
This commit is contained in:
@@ -75,6 +75,7 @@ dependencies {
|
||||
// Utility
|
||||
implementation(libs.krokiCoroutines)
|
||||
implementation(libs.dateTime)
|
||||
implementation(libs.immutableCollections)
|
||||
|
||||
// Localization
|
||||
implementation(libs.moko.core)
|
||||
|
||||
@@ -15,10 +15,12 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.theme.AppTheme
|
||||
import ca.gosyer.jui.ui.reader.ReaderMenu
|
||||
import ca.gosyer.jui.ui.reader.supportedKeyList
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
@@ -34,6 +36,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private val hotkeyFlow = MutableSharedFlow<KeyEvent>()
|
||||
private val hotkeyFlowHolder = StableHolder(hotkeyFlow.asSharedFlow())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -52,7 +55,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
ReaderMenu(
|
||||
chapterIndex = chapterIndex,
|
||||
mangaId = mangaId,
|
||||
hotkeyFlow = hotkeyFlow,
|
||||
hotkeyFlowHolder = hotkeyFlowHolder,
|
||||
onCloseRequest = onBackPressedDispatcher::onBackPressed
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ dependencies {
|
||||
// Utility
|
||||
implementation(libs.krokiCoroutines)
|
||||
implementation(libs.dateTime)
|
||||
implementation(libs.immutableCollections)
|
||||
|
||||
// Localization
|
||||
implementation(libs.moko.core)
|
||||
|
||||
@@ -13,7 +13,9 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.configureSwingGlobalsForCompose
|
||||
import androidx.compose.ui.graphics.toPainter
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
@@ -32,6 +34,7 @@ import ca.gosyer.jui.domain.server.service.ServerService.ServerResult
|
||||
import ca.gosyer.jui.domain.ui.model.ThemeMode
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.theme.AppTheme
|
||||
import ca.gosyer.jui.ui.main.MainMenu
|
||||
import ca.gosyer.jui.ui.main.components.DebugOverlay
|
||||
@@ -40,7 +43,6 @@ import ca.gosyer.jui.ui.util.compose.WindowGet
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.prefs.asStateIn
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import ca.gosyer.jui.uicore.resources.toPainter
|
||||
import com.github.weisj.darklaf.LafManager
|
||||
import com.github.weisj.darklaf.theme.DarculaTheme
|
||||
import com.github.weisj.darklaf.theme.IntelliJTheme
|
||||
@@ -141,7 +143,7 @@ suspend fun main() {
|
||||
placement = placement
|
||||
)
|
||||
|
||||
val icon = MR.images.icon.toPainter()
|
||||
val icon = remember { StableHolder(MR.images.icon.image.toPainter()) }
|
||||
|
||||
Tray(icon)
|
||||
|
||||
@@ -156,7 +158,7 @@ suspend fun main() {
|
||||
}
|
||||
},
|
||||
title = BuildConfig.NAME,
|
||||
icon = icon,
|
||||
icon = icon.item,
|
||||
state = windowState,
|
||||
onKeyEvent = {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
|
||||
@@ -55,6 +55,7 @@ kroki = "1.22"
|
||||
desugarJdkLibs = "1.2.0"
|
||||
aboutLibraries = "10.4.0"
|
||||
dateTime = "0.4.0"
|
||||
immutableCollections = "0.3.5"
|
||||
|
||||
# Localization
|
||||
moko = "0.20.1"
|
||||
@@ -155,6 +156,7 @@ desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref =
|
||||
aboutLibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
|
||||
aboutLibraries-ui = { module = "com.mikepenz:aboutlibraries-compose", version.ref = "aboutLibraries" }
|
||||
dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTime" }
|
||||
immutableCollections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutableCollections" }
|
||||
|
||||
# Localization
|
||||
moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" }
|
||||
|
||||
@@ -52,6 +52,7 @@ kotlin {
|
||||
api(libs.accompanist.flowLayout)
|
||||
api(libs.krokiCoroutines)
|
||||
api(libs.dateTime)
|
||||
api(libs.immutableCollections)
|
||||
api(libs.aboutLibraries.core)
|
||||
api(libs.aboutLibraries.ui)
|
||||
api(projects.core)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.ui.compose.Libraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
|
||||
import com.mikepenz.aboutlibraries.util.withContext
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
actual fun getLicenses(): Libs? {
|
||||
@@ -36,7 +37,7 @@ actual fun getLicenses(): Libs? {
|
||||
|
||||
@Composable
|
||||
actual fun InternalAboutLibraries(
|
||||
libraries: List<Library>,
|
||||
libraries: ImmutableList<Library>,
|
||||
modifier: Modifier,
|
||||
lazyListState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.material.icons.rounded.ArrowDownward
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Error
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -38,12 +39,14 @@ import ca.gosyer.jui.domain.download.model.DownloadChapter
|
||||
import ca.gosyer.jui.domain.download.model.DownloadState
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.DropdownIconButton
|
||||
import ca.gosyer.jui.uicore.components.DropdownMenuItem
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
@Stable
|
||||
data class ChapterDownloadItem(
|
||||
val manga: Manga?,
|
||||
val chapter: Chapter,
|
||||
@@ -54,7 +57,7 @@ data class ChapterDownloadItem(
|
||||
ChapterDownloadState.NotDownloaded
|
||||
}
|
||||
),
|
||||
private val _downloadChapterFlow: MutableStateFlow<DownloadChapter?> = MutableStateFlow(null)
|
||||
private val _downloadChapterFlow: MutableStateFlow<StableHolder<DownloadChapter?>> = MutableStateFlow(StableHolder(null))
|
||||
) {
|
||||
val downloadState = _downloadState.asStateFlow()
|
||||
val downloadChapterFlow = _downloadChapterFlow.asStateFlow()
|
||||
@@ -69,7 +72,7 @@ data class ChapterDownloadItem(
|
||||
if (downloadState.value == ChapterDownloadState.Downloading && downloadingChapter == null) {
|
||||
_downloadState.value = ChapterDownloadState.Downloaded
|
||||
}
|
||||
_downloadChapterFlow.value = downloadingChapter
|
||||
_downloadChapterFlow.value = StableHolder(downloadingChapter)
|
||||
}
|
||||
|
||||
suspend fun deleteDownload(deleteChapterDownload: DeleteChapterDownload) {
|
||||
@@ -141,7 +144,8 @@ private fun DownloadIconButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: () -> Unit) {
|
||||
private fun DownloadingIconButton(downloadChapterHolder: StableHolder<DownloadChapter?>, onClick: () -> Unit) {
|
||||
val downloadChapter = downloadChapterHolder.item
|
||||
DropdownIconButton(
|
||||
downloadChapter?.mangaId to downloadChapter?.chapterIndex,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.jui.ui.base.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
class StableHolder<T>(val item: T) {
|
||||
operator fun component1(): T = item
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class ImmutableHolder<T>(val item: T) {
|
||||
operator fun component1(): T = item
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.components.DropdownMenu
|
||||
import ca.gosyer.jui.uicore.components.DropdownMenuItem
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
// Originally from https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf
|
||||
|
||||
@@ -53,7 +54,7 @@ enum class OverflowMode {
|
||||
// Note: should be used in a RowScope
|
||||
@Composable
|
||||
fun ActionMenu(
|
||||
items: List<ActionItem>,
|
||||
items: ImmutableList<ActionItem>,
|
||||
numIcons: Int = 3, // includes overflow menu icon; may be overridden by NEVER_OVERFLOW
|
||||
menuVisible: MutableState<Boolean> = remember { mutableStateOf(false) },
|
||||
iconItem: @Composable (onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean) -> Unit
|
||||
@@ -111,7 +112,7 @@ fun ActionMenu(
|
||||
}
|
||||
|
||||
private fun separateIntoIconAndOverflow(
|
||||
items: List<ActionItem>,
|
||||
items: ImmutableList<ActionItem>,
|
||||
numIcons: Int
|
||||
): Pair<List<ActionItem>, List<ActionItem>> {
|
||||
var (iconCount, overflowCount, preferIconCount) = Triple(0, 0, 0)
|
||||
|
||||
@@ -74,6 +74,8 @@ import ca.gosyer.jui.uicore.components.keyboardHandler
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
@@ -82,7 +84,7 @@ fun Toolbar(
|
||||
closable: Boolean = (navigator?.size ?: 0) > 1,
|
||||
onClose: () -> Unit = { navigator?.pop() },
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable () -> List<ActionItem> = { emptyList() },
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
|
||||
contentColor: Color = contentColorFor(backgroundColor), // CustomColors.current.onBars,
|
||||
elevation: Dp = Dp.Hairline,
|
||||
@@ -129,7 +131,7 @@ private fun WideToolbar(
|
||||
closable: Boolean,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier,
|
||||
actions: @Composable () -> List<ActionItem> = { emptyList() },
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
elevation: Dp,
|
||||
@@ -198,7 +200,7 @@ private fun ThinToolbar(
|
||||
closable: Boolean,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier,
|
||||
actions: @Composable () -> List<ActionItem> = { emptyList() },
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
elevation: Dp,
|
||||
|
||||
@@ -70,6 +70,8 @@ import ca.gosyer.jui.uicore.components.keyboardHandler
|
||||
import com.vanpra.composematerialdialogs.MaterialDialog
|
||||
import com.vanpra.composematerialdialogs.MaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlin.math.round
|
||||
|
||||
@Composable
|
||||
@@ -125,7 +127,7 @@ private fun ColorPresets(
|
||||
) {
|
||||
val presets = remember {
|
||||
if (initialColor.isSpecified) {
|
||||
(listOf(initialColor) + presetColors).distinct()
|
||||
(listOf(initialColor) + presetColors).distinct().toImmutableList()
|
||||
} else {
|
||||
presetColors
|
||||
}
|
||||
@@ -203,14 +205,14 @@ private fun ColorPresetItem(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColorShades(color: Color): List<Color> {
|
||||
private fun getColorShades(color: Color): ImmutableList<Color> {
|
||||
val f = color.toLong()
|
||||
return listOf(
|
||||
shadeColor(f, 0.9), shadeColor(f, 0.7), shadeColor(f, 0.5),
|
||||
shadeColor(f, 0.333), shadeColor(f, 0.166), shadeColor(f, -0.125),
|
||||
shadeColor(f, -0.25), shadeColor(f, -0.375), shadeColor(f, -0.5),
|
||||
shadeColor(f, -0.675), shadeColor(f, -0.7), shadeColor(f, -0.775)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
private fun shadeColor(f: Long, percent: Double): Color {
|
||||
@@ -445,4 +447,4 @@ private val presetColors = listOf(
|
||||
Color(0xFF795548), // BROWN 500
|
||||
Color(0xFF607D8B), // BLUE GREY 500
|
||||
Color(0xFF9E9E9E) // GREY 500
|
||||
)
|
||||
).toImmutableList()
|
||||
|
||||
@@ -82,6 +82,9 @@ import com.vanpra.composematerialdialogs.listItemsMultiChoice
|
||||
import com.vanpra.composematerialdialogs.listItemsSingleChoice
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun PreferenceRow(
|
||||
@@ -219,7 +222,7 @@ fun EditTextPreference(
|
||||
@Composable
|
||||
fun <Key> ChoicePreference(
|
||||
preference: PreferenceMutableStateFlow<Key>,
|
||||
choices: Map<Key, String>,
|
||||
choices: ImmutableMap<Key, String>,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
changeListener: () -> Unit = {},
|
||||
@@ -237,7 +240,7 @@ fun <Key> ChoicePreference(
|
||||
)
|
||||
ChoiceDialog(
|
||||
state = dialogState,
|
||||
items = choices.toList(),
|
||||
items = choices.toList().toImmutableList(),
|
||||
selected = prefValue,
|
||||
title = title,
|
||||
onSelected = { selected ->
|
||||
@@ -250,7 +253,7 @@ fun <Key> ChoicePreference(
|
||||
@Composable
|
||||
fun <T> ChoiceDialog(
|
||||
state: MaterialDialogState,
|
||||
items: List<Pair<T, String>>,
|
||||
items: ImmutableList<Pair<T, String>>,
|
||||
selected: T?,
|
||||
onCloseRequest: () -> Unit = {},
|
||||
onSelected: (T) -> Unit,
|
||||
@@ -292,10 +295,10 @@ fun <T> ChoiceDialog(
|
||||
@Composable
|
||||
fun <T> MultiSelectDialog(
|
||||
state: MaterialDialogState,
|
||||
items: List<Pair<T, String>>,
|
||||
selected: List<T>?,
|
||||
items: ImmutableList<Pair<T, String>>,
|
||||
selected: ImmutableList<T>?,
|
||||
onCloseRequest: () -> Unit = {},
|
||||
onFinished: (List<T>) -> Unit,
|
||||
onFinished: (ImmutableList<T>) -> Unit,
|
||||
title: String
|
||||
) {
|
||||
MaterialDialog(
|
||||
@@ -322,7 +325,7 @@ fun <T> MultiSelectDialog(
|
||||
?.toSet()
|
||||
.orEmpty(),
|
||||
onCheckedChange = { indexes ->
|
||||
onFinished(indexes.map { items[it].first })
|
||||
onFinished(indexes.map { items[it].first }.toImmutableList())
|
||||
}
|
||||
)
|
||||
VerticalScrollbar(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.categories
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import ca.gosyer.jui.domain.category.interactor.CreateCategory
|
||||
import ca.gosyer.jui.domain.category.interactor.DeleteCategory
|
||||
import ca.gosyer.jui.domain.category.interactor.GetCategories
|
||||
@@ -14,6 +15,12 @@ import ca.gosyer.jui.domain.category.interactor.ReorderCategory
|
||||
import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.minus
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,7 +36,7 @@ class CategoriesScreenViewModel @Inject constructor(
|
||||
contextWrapper: ContextWrapper
|
||||
) : ViewModel(contextWrapper) {
|
||||
private var originalCategories = emptyList<Category>()
|
||||
private val _categories = MutableStateFlow(emptyList<MenuCategory>())
|
||||
private val _categories = MutableStateFlow<ImmutableList<MenuCategory>>(persistentListOf())
|
||||
val categories = _categories.asStateFlow()
|
||||
|
||||
init {
|
||||
@@ -39,13 +46,14 @@ class CategoriesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun getCategories() {
|
||||
_categories.value = emptyList()
|
||||
_categories.value = persistentListOf()
|
||||
val categories = getCategories.await(true)
|
||||
if (categories != null) {
|
||||
_categories.value = categories
|
||||
.sortedBy { it.order }
|
||||
.also { originalCategories = it }
|
||||
.map { it.toMenuCategory() }
|
||||
.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +87,16 @@ class CategoriesScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun renameCategory(category: MenuCategory, newName: String) {
|
||||
_categories.value = (_categories.value - category + category.copy(name = newName)).sortedBy { it.order }
|
||||
_categories.value = (_categories.value.toPersistentList() - category + category.copy(name = newName)).sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun deleteCategory(category: MenuCategory) {
|
||||
_categories.value = _categories.value - category
|
||||
_categories.value = _categories.value.toPersistentList() - category
|
||||
}
|
||||
|
||||
fun createCategory(name: String) {
|
||||
_categories.value += MenuCategory(order = categories.value.size + 1, name = name, default = false)
|
||||
_categories.value = _categories.value.toPersistentList() + MenuCategory(order = categories.value.size + 1, name = name, default = false)
|
||||
}
|
||||
|
||||
fun moveUp(category: MenuCategory) {
|
||||
@@ -95,10 +104,12 @@ class CategoriesScreenViewModel @Inject constructor(
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index - 1, categories.removeAt(index))
|
||||
categories.forEachIndexed { i, _ ->
|
||||
categories[i].order = i + 1
|
||||
}
|
||||
_categories.value = categories.sortedBy { it.order }.toList()
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
fun moveDown(category: MenuCategory) {
|
||||
@@ -106,15 +117,18 @@ class CategoriesScreenViewModel @Inject constructor(
|
||||
val index = categories.indexOf(category)
|
||||
if (index == -1) throw Exception("Invalid index")
|
||||
categories.add(index + 1, categories.removeAt(index))
|
||||
categories.forEachIndexed { i, _ ->
|
||||
categories[i].order = i + 1
|
||||
}
|
||||
_categories.value = categories.sortedBy { it.order }.toList()
|
||||
_categories.value = categories
|
||||
.mapIndexed { i, menuCategory ->
|
||||
menuCategory.copy(order = i + 1)
|
||||
}
|
||||
.sortedBy { it.order }
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun Category.toMenuCategory() = MenuCategory(id, order, name, default)
|
||||
|
||||
data class MenuCategory(val id: Long? = null, var order: Int, val name: String, val default: Boolean = false)
|
||||
@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()
|
||||
|
||||
@@ -49,6 +49,7 @@ import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -60,7 +61,7 @@ private val log = logging()
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Composable
|
||||
fun CategoriesScreenContent(
|
||||
categories: List<MenuCategory>,
|
||||
categories: ImmutableList<MenuCategory>,
|
||||
updateRemoteCategories: suspend () -> Unit,
|
||||
moveCategoryUp: (MenuCategory) -> Unit,
|
||||
moveCategoryDown: (MenuCategory) -> Unit,
|
||||
|
||||
@@ -14,12 +14,18 @@ import ca.gosyer.jui.domain.download.interactor.ClearDownloadQueue
|
||||
import ca.gosyer.jui.domain.download.interactor.StartDownloading
|
||||
import ca.gosyer.jui.domain.download.interactor.StopDownloading
|
||||
import ca.gosyer.jui.domain.download.service.DownloadService
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
@@ -43,7 +49,8 @@ class DownloadsScreenViewModel @Inject constructor(
|
||||
|
||||
val serviceStatus get() = DownloadService.status.asStateFlow()
|
||||
val downloaderStatus get() = DownloadService.downloaderStatus.asStateFlow()
|
||||
val downloadQueue get() = DownloadService.downloadQueue.asStateFlow()
|
||||
val downloadQueue get() = DownloadService.downloadQueue.map { it.map(::StableHolder).toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
fun start() {
|
||||
scope.launch { startDownloading.await() }
|
||||
|
||||
@@ -43,6 +43,7 @@ import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||
import ca.gosyer.jui.domain.download.model.DownloadChapter
|
||||
import ca.gosyer.jui.domain.download.model.DownloaderStatus
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.uicore.components.DropdownIconButton
|
||||
@@ -57,10 +58,12 @@ import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun DownloadsScreenContent(
|
||||
downloadQueue: List<DownloadChapter>,
|
||||
downloadQueue: ImmutableList<StableHolder<DownloadChapter>>,
|
||||
downloadStatus: DownloaderStatus,
|
||||
startDownloading: () -> Unit,
|
||||
pauseDownloading: () -> Unit,
|
||||
@@ -90,7 +93,7 @@ fun DownloadsScreenContent(
|
||||
items(downloadQueue) {
|
||||
DownloadsItem(
|
||||
it,
|
||||
{ onMangaClick(it.mangaId) },
|
||||
{ onMangaClick(it.item.mangaId) },
|
||||
stopDownload,
|
||||
moveDownloadToBottom
|
||||
)
|
||||
@@ -108,11 +111,12 @@ fun DownloadsScreenContent(
|
||||
|
||||
@Composable
|
||||
fun DownloadsItem(
|
||||
item: DownloadChapter,
|
||||
itemHolder: StableHolder<DownloadChapter>,
|
||||
onClickCover: () -> Unit,
|
||||
onClickCancel: (Chapter) -> Unit,
|
||||
onClickMoveToBottom: (Chapter) -> Unit
|
||||
) {
|
||||
val item = itemHolder.item
|
||||
MangaListItem(
|
||||
modifier = Modifier
|
||||
.height(96.dp)
|
||||
@@ -182,7 +186,7 @@ private fun getActionItems(
|
||||
startDownloading: () -> Unit,
|
||||
pauseDownloading: () -> Unit,
|
||||
clearQueue: () -> Unit
|
||||
): List<ActionItem> {
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOf(
|
||||
if (downloadStatus == DownloaderStatus.Started) {
|
||||
ActionItem(
|
||||
@@ -198,5 +202,5 @@ private fun getActionItems(
|
||||
)
|
||||
},
|
||||
ActionItem(stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll, doAction = clearQueue)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ import ca.gosyer.jui.domain.extension.service.ExtensionPreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -40,7 +45,8 @@ class ExtensionsScreenViewModel @Inject constructor(
|
||||
private val extensionList = MutableStateFlow<List<Extension>?>(null)
|
||||
|
||||
private val _enabledLangs = extensionPreferences.languages().asStateFlow()
|
||||
val enabledLangs = _enabledLangs.asStateFlow()
|
||||
val enabledLangs = _enabledLangs.map { it.toImmutableSet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentSetOf())
|
||||
|
||||
private val _searchQuery = MutableStateFlow<String?>(null)
|
||||
val searchQuery = _searchQuery.asStateFlow()
|
||||
@@ -51,11 +57,11 @@ class ExtensionsScreenViewModel @Inject constructor(
|
||||
enabledLangs
|
||||
) { searchQuery, extensions, enabledLangs ->
|
||||
search(searchQuery, extensions, enabledLangs)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val availableLangs = extensionList.filterNotNull().map { langs ->
|
||||
langs.map { it.lang }.distinct()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
langs.map { it.lang }.distinct().toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
@@ -103,7 +109,7 @@ class ExtensionsScreenViewModel @Inject constructor(
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
private fun search(searchQuery: String?, extensionList: List<Extension>?, enabledLangs: Set<String>): List<ExtensionUI> {
|
||||
private fun search(searchQuery: String?, extensionList: List<Extension>?, enabledLangs: Set<String>): ImmutableList<ExtensionUI> {
|
||||
val extensions = extensionList?.filter { it.lang in enabledLangs }
|
||||
.orEmpty()
|
||||
return if (searchQuery.isNullOrBlank()) {
|
||||
@@ -118,7 +124,7 @@ class ExtensionsScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Extension>.splitSort(): List<ExtensionUI> {
|
||||
private fun List<Extension>.splitSort(): ImmutableList<ExtensionUI> {
|
||||
val all = MR.strings.all.toPlatformString()
|
||||
return this
|
||||
.filter(Extension::installed)
|
||||
@@ -165,6 +171,7 @@ class ExtensionsScreenViewModel @Inject constructor(
|
||||
listOf(ExtensionUI.Header(key)) + value
|
||||
}
|
||||
)
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -62,15 +62,18 @@ import com.vanpra.composematerialdialogs.MaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.listItemsMultiChoice
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun ExtensionsScreenContent(
|
||||
extensions: List<ExtensionUI>,
|
||||
extensions: ImmutableList<ExtensionUI>,
|
||||
isLoading: Boolean,
|
||||
query: String?,
|
||||
setQuery: (String) -> Unit,
|
||||
enabledLangs: Set<String>,
|
||||
availableLangs: List<String>,
|
||||
enabledLangs: ImmutableSet<String>,
|
||||
availableLangs: ImmutableList<String>,
|
||||
setEnabledLanguages: (Set<String>) -> Unit,
|
||||
installExtension: (Extension) -> Unit,
|
||||
updateExtension: (Extension) -> Unit,
|
||||
@@ -118,7 +121,7 @@ fun ExtensionsScreenContent(
|
||||
is ExtensionUI.ExtensionItem -> Column {
|
||||
ExtensionItem(
|
||||
Modifier.animateItemPlacement(),
|
||||
it.extension,
|
||||
it,
|
||||
onInstallClicked = installExtension,
|
||||
onUpdateClicked = updateExtension,
|
||||
onUninstallClicked = uninstallExtension
|
||||
@@ -159,11 +162,12 @@ fun ExtensionsToolbar(
|
||||
@Composable
|
||||
fun ExtensionItem(
|
||||
modifier: Modifier,
|
||||
extension: Extension,
|
||||
extensionItem: ExtensionUI.ExtensionItem,
|
||||
onInstallClicked: (Extension) -> Unit,
|
||||
onUpdateClicked: (Extension) -> Unit,
|
||||
onUninstallClicked: (Extension) -> Unit
|
||||
) {
|
||||
val extension = extensionItem.extension
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(end = 12.dp)
|
||||
@@ -225,8 +229,8 @@ fun ExtensionItem(
|
||||
@Composable
|
||||
fun LanguageDialog(
|
||||
state: MaterialDialogState,
|
||||
enabledLangs: Set<String>,
|
||||
availableLangs: List<String>,
|
||||
enabledLangs: ImmutableSet<String>,
|
||||
availableLangs: ImmutableList<String>,
|
||||
setLangs: (Set<String>) -> Unit
|
||||
) {
|
||||
MaterialDialog(
|
||||
@@ -268,12 +272,12 @@ fun LanguageDialog(
|
||||
@Composable
|
||||
private fun getActionItems(
|
||||
openLanguageDialog: () -> Unit
|
||||
): List<ActionItem> {
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(
|
||||
stringResource(MR.strings.enabled_languages),
|
||||
Icons.Rounded.Translate,
|
||||
doAction = openLanguageDialog
|
||||
)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.library
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toLowerCase
|
||||
import ca.gosyer.jui.core.lang.withDefaultContext
|
||||
@@ -22,11 +23,16 @@ import ca.gosyer.jui.domain.manga.model.MangaStatus
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateCategory
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateLibrary
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.util.lang.Collator
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -39,6 +45,7 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.toList
|
||||
@@ -46,17 +53,21 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
@Stable
|
||||
sealed class CategoryState {
|
||||
@Stable
|
||||
object Loading : CategoryState()
|
||||
@Stable
|
||||
data class Failed(val e: Throwable) : CategoryState()
|
||||
@Stable
|
||||
data class Loaded(
|
||||
val items: StateFlow<List<Manga>>,
|
||||
val unfilteredItems: MutableStateFlow<List<Manga>>
|
||||
val items: StateFlow<ImmutableList<StableHolder<Manga>>>,
|
||||
val unfilteredItems: MutableStateFlow<ImmutableList<StableHolder<Manga>>>
|
||||
) : CategoryState()
|
||||
}
|
||||
|
||||
private typealias LibraryMap = MutableMap<Long, MutableStateFlow<CategoryState>>
|
||||
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap)
|
||||
private data class Library(val categories: MutableStateFlow<ImmutableList<StableHolder<Category>>>, val mangaMap: LibraryMap)
|
||||
|
||||
private fun LibraryMap.getManga(id: Long) =
|
||||
getOrPut(id) {
|
||||
@@ -65,7 +76,7 @@ private fun LibraryMap.getManga(id: Long) =
|
||||
private fun LibraryMap.setError(id: Long, e: Throwable) {
|
||||
getManga(id).value = CategoryState.Failed(e)
|
||||
}
|
||||
private fun LibraryMap.setManga(id: Long, manga: List<Manga>, getItemsFlow: (StateFlow<List<Manga>>) -> StateFlow<List<Manga>>) {
|
||||
private fun LibraryMap.setManga(id: Long, manga: ImmutableList<StableHolder<Manga>>, getItemsFlow: (StateFlow<List<StableHolder<Manga>>>) -> StateFlow<ImmutableList<StableHolder<Manga>>>) {
|
||||
val flow = getManga(id)
|
||||
when (val state = flow.value) {
|
||||
is CategoryState.Loaded -> state.unfilteredItems.value = manga
|
||||
@@ -85,7 +96,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
contextWrapper: ContextWrapper
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
|
||||
private val library = Library(MutableStateFlow(persistentListOf()), mutableMapOf())
|
||||
val categories = library.categories.asStateFlow()
|
||||
|
||||
private val _selectedCategoryIndex = MutableStateFlow(0)
|
||||
@@ -105,12 +116,12 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
private val sortMode = libraryPreferences.sortMode().stateIn(scope)
|
||||
private val sortAscending = libraryPreferences.sortAscending().stateIn(scope)
|
||||
|
||||
private val filter = combine(
|
||||
private val filter: Flow<(StableHolder<Manga>) -> Boolean> = combine(
|
||||
libraryPreferences.filterDownloaded().getAsFlow(),
|
||||
libraryPreferences.filterUnread().getAsFlow(),
|
||||
libraryPreferences.filterCompleted().getAsFlow()
|
||||
) { downloaded, unread, completed ->
|
||||
{ manga: Manga ->
|
||||
{ (manga) ->
|
||||
when (downloaded) {
|
||||
FilterState.EXCLUDED -> manga.downloadCount == null || manga.downloadCount == 0
|
||||
FilterState.INCLUDED -> manga.downloadCount != null && (manga.downloadCount ?: 0) > 0
|
||||
@@ -138,7 +149,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
|
||||
private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending ->
|
||||
getComparator(sortMode, sortAscending)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title })
|
||||
}.stateIn(scope, SharingStarted.Eagerly, compareBy { it.item.title })
|
||||
|
||||
init {
|
||||
getLibrary()
|
||||
@@ -152,6 +163,8 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
throw Exception(MR.strings.library_empty.toPlatformString())
|
||||
}
|
||||
library.categories.value = categories.sortedBy { it.order }
|
||||
.map(::StableHolder)
|
||||
.toImmutableList()
|
||||
updateCategories(categories)
|
||||
_isLoading.value = false
|
||||
}
|
||||
@@ -171,18 +184,18 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
_showingMenu.value = showingMenu
|
||||
}
|
||||
|
||||
private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator<Manga> {
|
||||
val sortFn: (Manga, Manga) -> Int = when (sortMode) {
|
||||
private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator<StableHolder<Manga>> {
|
||||
val sortFn: (StableHolder<Manga>, StableHolder<Manga>) -> Int = when (sortMode) {
|
||||
Sort.ALPHABETICAL -> {
|
||||
val locale = Locale.current
|
||||
val collator = Collator(locale);
|
||||
|
||||
{ a, b ->
|
||||
{ (a), (b) ->
|
||||
collator.compare(a.title.toLowerCase(locale), b.title.toLowerCase(locale))
|
||||
}
|
||||
}
|
||||
Sort.UNREAD -> {
|
||||
{ a, b ->
|
||||
{ (a), (b) ->
|
||||
when {
|
||||
// Ensure unread content comes first
|
||||
(a.unreadCount ?: 0) == (b.unreadCount ?: 0) -> 0
|
||||
@@ -193,7 +206,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
Sort.DATE_ADDED -> {
|
||||
{ a, b ->
|
||||
{ (a), (b) ->
|
||||
a.inLibraryAt.compareTo(b.inLibraryAt)
|
||||
}
|
||||
}
|
||||
@@ -205,11 +218,11 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun filterManga(query: String, mangaList: List<Manga>): List<Manga> {
|
||||
private suspend fun filterManga(query: String, mangaList: List<StableHolder<Manga>>): List<StableHolder<Manga>> {
|
||||
if (query.isBlank()) return mangaList
|
||||
val queries = query.split(" ")
|
||||
return mangaList.asFlow()
|
||||
.filter { manga ->
|
||||
.filter { (manga) ->
|
||||
queries.all { query ->
|
||||
manga.title.contains(query, true) ||
|
||||
manga.author.orEmpty().contains(query, true) ||
|
||||
@@ -224,14 +237,16 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<List<Manga>> {
|
||||
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<StableHolder<Manga>>>): StateFlow<ImmutableList<StableHolder<Manga>>> {
|
||||
return combine(unfilteredItemsFlow, query) { unfilteredItems, query ->
|
||||
filterManga(query, unfilteredItems)
|
||||
}.combine(filter) { filteredManga, filterer ->
|
||||
filteredManga.filter(filterer)
|
||||
}.combine(comparator) { filteredManga, comparator ->
|
||||
filteredManga.sortedWith(comparator)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
}.map {
|
||||
it.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
}
|
||||
|
||||
fun getLibraryForCategoryId(id: Long): StateFlow<CategoryState> {
|
||||
@@ -246,7 +261,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
.onEach {
|
||||
library.mangaMap.setManga(
|
||||
id = category.id,
|
||||
manga = it,
|
||||
manga = it.map(::StableHolder).toImmutableList(),
|
||||
getItemsFlow = ::getMangaItemsFlow
|
||||
)
|
||||
}
|
||||
@@ -260,12 +275,12 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCategoriesToUpdate(mangaId: Long): List<Category> {
|
||||
private fun getCategoriesToUpdate(mangaId: Long): List<StableHolder<Category>> {
|
||||
return library.mangaMap
|
||||
.filter { mangaMapEntry ->
|
||||
(mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.id == mangaId } != null
|
||||
(mangaMapEntry.value.value as? CategoryState.Loaded)?.items?.value?.firstOrNull { it.item.id == mangaId } != null
|
||||
}
|
||||
.map { (id) -> library.categories.value.first { it.id == id } }
|
||||
.map { (id) -> library.categories.value.first { it.item.id == id } }
|
||||
}
|
||||
|
||||
fun removeManga(mangaId: Long) {
|
||||
|
||||
@@ -23,17 +23,19 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun LibraryMangaBadges(
|
||||
modifier: Modifier = Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
showUnread: Boolean,
|
||||
showDownloaded: Boolean,
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val unread = manga.unreadCount
|
||||
val downloaded = manga.downloadCount
|
||||
val isLocal = manga.sourceId == Source.LOCAL_SOURCE_ID
|
||||
|
||||
@@ -24,16 +24,18 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.MangaListItem
|
||||
import ca.gosyer.jui.uicore.components.MangaListItemImage
|
||||
import ca.gosyer.jui.uicore.components.MangaListItemTitle
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryMangaList(
|
||||
library: List<Manga>,
|
||||
library: ImmutableList<StableHolder<Manga>>,
|
||||
onClickManga: (Long) -> Unit,
|
||||
onRemoveMangaClicked: (Long) -> Unit,
|
||||
showUnread: Boolean,
|
||||
@@ -47,13 +49,13 @@ fun LibraryMangaList(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(library) { manga ->
|
||||
items(library) { mangaHolder ->
|
||||
LibraryMangaListItem(
|
||||
modifier = Modifier.libraryMangaModifier(
|
||||
{ onClickManga(manga.id) },
|
||||
{ onRemoveMangaClicked(manga.id) }
|
||||
{ onClickManga(mangaHolder.item.id) },
|
||||
{ onRemoveMangaClicked(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
@@ -73,12 +75,13 @@ fun LibraryMangaList(
|
||||
@Composable
|
||||
private fun LibraryMangaListItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
showUnread: Boolean,
|
||||
showDownloaded: Boolean,
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
MangaListItem(
|
||||
modifier = modifier then Modifier
|
||||
.requiredHeight(56.dp)
|
||||
@@ -99,7 +102,7 @@ private fun LibraryMangaListItem(
|
||||
)
|
||||
Box(Modifier.width(IntrinsicSize.Min)) {
|
||||
LibraryMangaBadges(
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
|
||||
@@ -13,16 +13,18 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.domain.library.model.DisplayMode
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.library.CategoryState
|
||||
import ca.gosyer.jui.uicore.components.ErrorScreen
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryPager(
|
||||
pagerState: PagerState,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
@@ -37,7 +39,7 @@ fun LibraryPager(
|
||||
if (categories.isEmpty()) return
|
||||
|
||||
HorizontalPager(categories.size, state = pagerState) {
|
||||
when (val library = getLibraryForPage(categories[it].id).value) {
|
||||
when (val library = getLibraryForPage(categories[it].item.id).value) {
|
||||
CategoryState.Loading -> LoadingScreen()
|
||||
is CategoryState.Failed -> ErrorScreen(library.e.message)
|
||||
is CategoryState.Loaded -> LibraryLoadedPage(
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.domain.library.model.DisplayMode
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.BackHandler
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
@@ -45,10 +46,12 @@ import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryScreenContent(
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
selectedCategoryIndex: Int,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
@@ -146,7 +149,7 @@ fun LibraryScreenContent(
|
||||
@Composable
|
||||
fun WideLibraryScreenContent(
|
||||
pagerState: PagerState,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
selectedCategoryIndex: Int,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
@@ -242,7 +245,7 @@ fun WideLibraryScreenContent(
|
||||
@Composable
|
||||
fun ThinLibraryScreenContent(
|
||||
pagerState: PagerState,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
selectedCategoryIndex: Int,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
@@ -343,12 +346,12 @@ fun ThinLibraryScreenContent(
|
||||
@Stable
|
||||
private fun getActionItems(
|
||||
onToggleFiltersClick: () -> Unit
|
||||
): List<ActionItem> {
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOfNotNull(
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_filter),
|
||||
icon = Icons.Rounded.FilterList,
|
||||
doAction = onToggleFiltersClick
|
||||
)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -19,14 +19,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryTabs(
|
||||
visible: Boolean,
|
||||
pagerState: PagerState,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
selectedPage: Int,
|
||||
onPageChanged: (Int) -> Unit
|
||||
) {
|
||||
@@ -48,7 +50,7 @@ fun LibraryTabs(
|
||||
)
|
||||
}
|
||||
) {
|
||||
categories.fastForEachIndexed { i, category ->
|
||||
categories.fastForEachIndexed { i, (category) ->
|
||||
Tab(
|
||||
selected = selectedPage == i,
|
||||
onClick = { onPageChanged(i) },
|
||||
|
||||
@@ -31,15 +31,17 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryMangaComfortableGrid(
|
||||
library: List<Manga>,
|
||||
library: ImmutableList<StableHolder<Manga>>,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
onClickManga: (Long) -> Unit,
|
||||
@@ -61,13 +63,13 @@ fun LibraryMangaComfortableGrid(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp)
|
||||
) {
|
||||
items(library) { manga ->
|
||||
items(library) { mangaHolder ->
|
||||
LibraryMangaComfortableGridItem(
|
||||
modifier = Modifier.libraryMangaModifier(
|
||||
{ onClickManga(manga.id) },
|
||||
{ onRemoveMangaClicked(manga.id) }
|
||||
{ onClickManga(mangaHolder.item.id) },
|
||||
{ onRemoveMangaClicked(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
@@ -87,12 +89,13 @@ fun LibraryMangaComfortableGrid(
|
||||
@Composable
|
||||
private fun LibraryMangaComfortableGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
showUnread: Boolean,
|
||||
showDownloaded: Boolean,
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
@@ -123,7 +126,7 @@ private fun LibraryMangaComfortableGridItem(
|
||||
}
|
||||
LibraryMangaBadges(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
|
||||
@@ -34,11 +34,13 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
expect fun Modifier.libraryMangaModifier(
|
||||
onClickManga: () -> Unit,
|
||||
@@ -47,7 +49,7 @@ expect fun Modifier.libraryMangaModifier(
|
||||
|
||||
@Composable
|
||||
fun LibraryMangaCompactGrid(
|
||||
library: List<Manga>,
|
||||
library: ImmutableList<StableHolder<Manga>>,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
onClickManga: (Long) -> Unit,
|
||||
@@ -69,13 +71,13 @@ fun LibraryMangaCompactGrid(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp)
|
||||
) {
|
||||
items(library) { manga ->
|
||||
items(library) { mangaHolder ->
|
||||
LibraryMangaCompactGridItem(
|
||||
modifier = Modifier.libraryMangaModifier(
|
||||
{ onClickManga(manga.id) },
|
||||
{ onRemoveMangaClicked(manga.id) }
|
||||
{ onClickManga(mangaHolder.item.id) },
|
||||
{ onRemoveMangaClicked(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
@@ -95,12 +97,13 @@ fun LibraryMangaCompactGrid(
|
||||
@Composable
|
||||
private fun LibraryMangaCompactGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
showUnread: Boolean,
|
||||
showDownloaded: Boolean,
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
@@ -128,7 +131,7 @@ private fun LibraryMangaCompactGridItem(
|
||||
)
|
||||
LibraryMangaBadges(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
|
||||
@@ -25,15 +25,17 @@ import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun LibraryMangaCoverOnlyGrid(
|
||||
library: List<Manga>,
|
||||
library: ImmutableList<StableHolder<Manga>>,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
onClickManga: (Long) -> Unit,
|
||||
@@ -55,13 +57,13 @@ fun LibraryMangaCoverOnlyGrid(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp)
|
||||
) {
|
||||
items(library) { manga ->
|
||||
items(library) { mangaHolder ->
|
||||
LibraryMangaCoverOnlyGridItem(
|
||||
modifier = Modifier.libraryMangaModifier(
|
||||
{ onClickManga(manga.id) },
|
||||
{ onRemoveMangaClicked(manga.id) }
|
||||
{ onClickManga(mangaHolder.item.id) },
|
||||
{ onRemoveMangaClicked(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
@@ -81,12 +83,13 @@ fun LibraryMangaCoverOnlyGrid(
|
||||
@Composable
|
||||
private fun LibraryMangaCoverOnlyGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
showUnread: Boolean,
|
||||
showDownloaded: Boolean,
|
||||
showLanguage: Boolean,
|
||||
showLocal: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
Box(
|
||||
modifier = Modifier.padding(4.dp)
|
||||
.fillMaxWidth()
|
||||
@@ -102,7 +105,7 @@ private fun LibraryMangaCoverOnlyGridItem(
|
||||
)
|
||||
LibraryMangaBadges(
|
||||
modifier = Modifier.padding(4.dp),
|
||||
manga = manga,
|
||||
mangaHolder = mangaHolder,
|
||||
showUnread = showUnread,
|
||||
showDownloaded = showDownloaded,
|
||||
showLanguage = showLanguage,
|
||||
|
||||
@@ -37,7 +37,7 @@ class AboutScreen : Screen {
|
||||
}
|
||||
}
|
||||
AboutContent(
|
||||
about = vm.about.collectAsState().value,
|
||||
aboutHolder = vm.aboutHolder.collectAsState().value,
|
||||
formattedBuildTime = vm.formattedBuildTime.collectAsState().value,
|
||||
checkForUpdates = vm::checkForUpdates,
|
||||
openSourceLicenses = {
|
||||
|
||||
@@ -11,6 +11,7 @@ import ca.gosyer.jui.domain.settings.interactor.AboutServer
|
||||
import ca.gosyer.jui.domain.settings.model.About
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateChecker
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateChecker.Update
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -32,10 +33,10 @@ class AboutViewModel @Inject constructor(
|
||||
contextWrapper: ContextWrapper
|
||||
) : ViewModel(contextWrapper) {
|
||||
|
||||
private val _about = MutableStateFlow<About?>(null)
|
||||
val about = _about.asStateFlow()
|
||||
private val _aboutHolder = MutableStateFlow<StableHolder<About?>>(StableHolder(null))
|
||||
val aboutHolder = _aboutHolder.asStateFlow()
|
||||
|
||||
val formattedBuildTime = about.map { about ->
|
||||
val formattedBuildTime = aboutHolder.map { (about) ->
|
||||
about ?: return@map ""
|
||||
getFormattedDate(Instant.fromEpochSeconds(about.buildTime))
|
||||
}.stateIn(scope, SharingStarted.Eagerly, "")
|
||||
@@ -49,7 +50,7 @@ class AboutViewModel @Inject constructor(
|
||||
|
||||
private fun getAbout() {
|
||||
scope.launch {
|
||||
_about.value = aboutServer.await()
|
||||
_aboutHolder.value = StableHolder(aboutServer.await())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import ca.gosyer.jui.domain.settings.model.About
|
||||
import ca.gosyer.jui.domain.updates.interactor.UpdateChecker
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.presentation.build.BuildKonfig
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
@@ -46,7 +47,7 @@ import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun AboutContent(
|
||||
about: About?,
|
||||
aboutHolder: StableHolder<About?>,
|
||||
formattedBuildTime: String,
|
||||
checkForUpdates: () -> Unit,
|
||||
openSourceLicenses: () -> Unit
|
||||
@@ -70,7 +71,7 @@ fun AboutContent(
|
||||
ClientVersionInfo()
|
||||
}
|
||||
item {
|
||||
ServerVersionInfo(about, formattedBuildTime)
|
||||
ServerVersionInfo(aboutHolder, formattedBuildTime)
|
||||
}
|
||||
item {
|
||||
WhatsNew()
|
||||
@@ -130,7 +131,8 @@ private fun ClientVersionInfo() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerVersionInfo(about: About?, formattedBuildTime: String) {
|
||||
private fun ServerVersionInfo(aboutHolder: StableHolder<About?>, formattedBuildTime: String) {
|
||||
val about = aboutHolder.item
|
||||
if (about == null) {
|
||||
Box(Modifier.fillMaxWidth().height(48.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@@ -30,13 +31,15 @@ import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
expect fun getLicenses(): Libs?
|
||||
|
||||
@Composable
|
||||
internal expect fun InternalAboutLibraries(
|
||||
libraries: List<Library>,
|
||||
libraries: ImmutableList<Library>,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
@@ -50,7 +53,7 @@ internal expect fun InternalAboutLibraries(
|
||||
|
||||
@Composable
|
||||
fun AboutLibraries(
|
||||
libraries: List<Library>,
|
||||
libraries: ImmutableList<Library>,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
@@ -88,7 +91,7 @@ fun LicensesContent() {
|
||||
val state = rememberLazyListState()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
AboutLibraries(
|
||||
libraries = libs.libraries,
|
||||
libraries = remember(libs) { libs.libraries.toImmutableList() },
|
||||
lazyListState = state,
|
||||
onLibraryClick = {
|
||||
it.website?.let(uriHandler::openUri)
|
||||
|
||||
@@ -26,11 +26,11 @@ class MangaScreen(private val mangaId: Long) : Screen {
|
||||
|
||||
MangaScreenContent(
|
||||
isLoading = vm.isLoading.collectAsState().value,
|
||||
manga = vm.manga.collectAsState().value,
|
||||
mangaHolder = vm.manga.collectAsState().value,
|
||||
chapters = vm.chapters.collectAsState().value,
|
||||
dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value,
|
||||
categoriesExist = vm.categoriesExist.collectAsState().value,
|
||||
chooseCategoriesFlow = vm.chooseCategoriesFlow,
|
||||
chooseCategoriesFlowHolder = vm.chooseCategoriesFlowHolder,
|
||||
availableCategories = vm.categories.collectAsState().value,
|
||||
mangaCategories = vm.mangaCategories.collectAsState().value,
|
||||
addFavorite = vm::addFavorite,
|
||||
|
||||
@@ -28,12 +28,17 @@ import ca.gosyer.jui.domain.manga.interactor.RefreshManga
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.ui.service.UiPreferences
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -64,26 +69,28 @@ class MangaScreenViewModel @Inject constructor(
|
||||
contextWrapper: ContextWrapper,
|
||||
private val params: Params
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val _manga = MutableStateFlow<Manga?>(null)
|
||||
private val _manga = MutableStateFlow<StableHolder<Manga?>>(StableHolder(null))
|
||||
val manga = _manga.asStateFlow()
|
||||
|
||||
private val _chapters = MutableStateFlow(emptyList<ChapterDownloadItem>())
|
||||
private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf())
|
||||
val chapters = _chapters.asStateFlow()
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
val categories = getCategories.asFlow(true)
|
||||
.map { it.map(::StableHolder).toImmutableList() }
|
||||
.catch { log.warn(it) { "Failed to get categories" } }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _mangaCategories = MutableStateFlow(emptyList<Category>())
|
||||
private val _mangaCategories = MutableStateFlow<ImmutableList<StableHolder<Category>>>(persistentListOf())
|
||||
val mangaCategories = _mangaCategories.asStateFlow()
|
||||
|
||||
val categoriesExist = categories.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
val chooseCategoriesFlow = MutableSharedFlow<Unit>()
|
||||
private val chooseCategoriesFlow = MutableSharedFlow<Unit>()
|
||||
val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow())
|
||||
|
||||
val dateTimeFormatter = uiPreferences.dateFormat().changes()
|
||||
.map {
|
||||
@@ -132,7 +139,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
fun setCategories() {
|
||||
scope.launch {
|
||||
manga.value ?: return@launch
|
||||
manga.value.item ?: return@launch
|
||||
chooseCategoriesFlow.emit(Unit)
|
||||
}
|
||||
}
|
||||
@@ -145,14 +152,14 @@ class MangaScreenViewModel @Inject constructor(
|
||||
getManga.await(mangaId)
|
||||
}
|
||||
if (manga != null) {
|
||||
_manga.value = manga
|
||||
_manga.value = StableHolder(manga)
|
||||
} else {
|
||||
// TODO: 2022-07-01 Error toast
|
||||
}
|
||||
|
||||
val mangaCategories = getMangaCategories.await(mangaId)
|
||||
if (mangaCategories != null) {
|
||||
_mangaCategories.value = mangaCategories
|
||||
_mangaCategories.value = mangaCategories.map(::StableHolder).toImmutableList()
|
||||
} else {
|
||||
// TODO: 2022-07-01 Error toast
|
||||
}
|
||||
@@ -176,7 +183,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
fun toggleFavorite() {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
removeMangaFromLibrary.await(manga)
|
||||
refreshMangaAsync(manga.id).await()
|
||||
@@ -193,7 +200,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
fun addFavorite(categories: List<Category>, oldCategories: List<Category>) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
if (manga.inLibrary) {
|
||||
oldCategories.filterNot { it in categories }.forEach {
|
||||
removeMangaFromCategory.await(manga, it)
|
||||
@@ -214,7 +221,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
fun toggleRead(index: Int) {
|
||||
val chapter = findChapter(index) ?: return
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
updateChapterFlags.await(manga, index, read = chapter.read.not())
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
}
|
||||
@@ -224,7 +231,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
fun toggleBookmarked(index: Int) {
|
||||
val chapter = findChapter(index) ?: return
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
updateChapterFlags.await(manga, index, bookmarked = chapter.bookmarked.not())
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
}
|
||||
@@ -233,7 +240,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
fun markPreviousRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
updateChapterFlags.await(manga, index, markPreviousRead = true)
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
}
|
||||
@@ -241,7 +248,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun downloadChapter(index: Int) {
|
||||
manga.value?.let { manga ->
|
||||
manga.value.item?.let { manga ->
|
||||
scope.launch { queueChapterDownload.await(manga, index) }
|
||||
}
|
||||
}
|
||||
@@ -262,7 +269,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
private fun List<Chapter>.toDownloadChapters() = map {
|
||||
ChapterDownloadItem(null, it)
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
data class Params(val mangaId: Long)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
@@ -48,31 +49,33 @@ import com.vanpra.composematerialdialogs.MaterialDialog
|
||||
import com.vanpra.composematerialdialogs.MaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.listItemsMultiChoice
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun MangaItem(manga: Manga) {
|
||||
fun MangaItem(mangaHolder: StableHolder<Manga>) {
|
||||
BoxWithConstraints(Modifier.padding(8.dp)) {
|
||||
if (maxWidth > 720.dp) {
|
||||
Row {
|
||||
Cover(manga, Modifier.width(300.dp))
|
||||
Cover(mangaHolder, Modifier.width(300.dp))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
MangaInfo(manga)
|
||||
MangaInfo(mangaHolder)
|
||||
}
|
||||
} else {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Cover(
|
||||
manga,
|
||||
mangaHolder,
|
||||
Modifier.heightIn(120.dp, 300.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MangaInfo(manga)
|
||||
MangaInfo(mangaHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Cover(manga: Manga, modifier: Modifier = Modifier) {
|
||||
private fun Cover(mangaHolder: StableHolder<Manga>, modifier: Modifier = Modifier) {
|
||||
val manga = mangaHolder.item
|
||||
ImageLoaderImage(
|
||||
data = manga,
|
||||
contentDescription = manga.title,
|
||||
@@ -87,7 +90,8 @@ private fun Cover(manga: Manga, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) {
|
||||
private fun MangaInfo(mangaHolder: StableHolder<Manga>, modifier: Modifier = Modifier) {
|
||||
val manga = mangaHolder.item
|
||||
SelectionContainer {
|
||||
Column(modifier) {
|
||||
Text(
|
||||
@@ -161,8 +165,8 @@ private fun Chip(text: String) {
|
||||
@Composable
|
||||
fun CategorySelectDialog(
|
||||
state: MaterialDialogState,
|
||||
categories: List<Category>,
|
||||
oldCategories: List<Category>,
|
||||
categories: ImmutableList<StableHolder<Category>>,
|
||||
oldCategories: ImmutableList<StableHolder<Category>>,
|
||||
onPositiveClick: (List<Category>, List<Category>) -> Unit
|
||||
) {
|
||||
MaterialDialog(
|
||||
@@ -178,13 +182,13 @@ fun CategorySelectDialog(
|
||||
Box {
|
||||
val listState = rememberLazyListState()
|
||||
listItemsMultiChoice(
|
||||
list = categories.map { it.name },
|
||||
list = categories.map { it.item.name },
|
||||
state = listState,
|
||||
initialSelection = oldCategories.mapNotNull { category ->
|
||||
categories.indexOfFirst { it.id == category.id }.takeUnless { it == -1 }
|
||||
categories.indexOfFirst { it.item.id == category.item.id }.takeUnless { it == -1 }
|
||||
}.toSet(),
|
||||
onCheckedChange = { indexes ->
|
||||
onPositiveClick(indexes.map { categories[it] }, oldCategories)
|
||||
onPositiveClick(indexes.map { categories[it].item }, oldCategories.map { it.item })
|
||||
}
|
||||
)
|
||||
VerticalScrollbar(
|
||||
|
||||
@@ -32,6 +32,7 @@ import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.reader.rememberReaderLauncher
|
||||
@@ -42,19 +43,21 @@ import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@Composable
|
||||
fun MangaScreenContent(
|
||||
isLoading: Boolean,
|
||||
manga: Manga?,
|
||||
chapters: List<ChapterDownloadItem>,
|
||||
mangaHolder: StableHolder<Manga?>,
|
||||
chapters: ImmutableList<ChapterDownloadItem>,
|
||||
dateTimeFormatter: (Instant) -> String,
|
||||
categoriesExist: Boolean,
|
||||
chooseCategoriesFlow: SharedFlow<Unit>,
|
||||
availableCategories: List<Category>,
|
||||
mangaCategories: List<Category>,
|
||||
chooseCategoriesFlowHolder: StableHolder<SharedFlow<Unit>>,
|
||||
availableCategories: ImmutableList<StableHolder<Category>>,
|
||||
mangaCategories: ImmutableList<StableHolder<Category>>,
|
||||
addFavorite: (List<Category>, List<Category>) -> Unit,
|
||||
setCategories: () -> Unit,
|
||||
toggleFavorite: () -> Unit,
|
||||
@@ -70,7 +73,7 @@ fun MangaScreenContent(
|
||||
) {
|
||||
val categoryDialogState = rememberMaterialDialogState()
|
||||
LaunchedEffect(Unit) {
|
||||
chooseCategoriesFlow.collect {
|
||||
chooseCategoriesFlowHolder.item.collect {
|
||||
categoryDialogState.show()
|
||||
}
|
||||
}
|
||||
@@ -82,6 +85,7 @@ fun MangaScreenContent(
|
||||
Toolbar(
|
||||
stringResource(MR.strings.location_manga),
|
||||
actions = {
|
||||
val manga = mangaHolder.item
|
||||
val uriHandler = LocalUriHandler.current
|
||||
getActionItems(
|
||||
refreshManga = refreshManga,
|
||||
@@ -101,20 +105,20 @@ fun MangaScreenContent(
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
manga.let { manga ->
|
||||
if (manga != null) {
|
||||
mangaHolder.let { mangaHolder ->
|
||||
if (mangaHolder.item != null) {
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(state = state) {
|
||||
item {
|
||||
MangaItem(manga)
|
||||
MangaItem(mangaHolder as StableHolder<Manga>)
|
||||
}
|
||||
if (chapters.isNotEmpty()) {
|
||||
items(chapters) { chapter ->
|
||||
ChapterItem(
|
||||
chapter,
|
||||
dateTimeFormatter,
|
||||
onClick = { readerLauncher.launch(it, manga.id) },
|
||||
onClick = { readerLauncher.launch(it, mangaHolder.item.id) },
|
||||
toggleRead = toggleRead,
|
||||
toggleBookmarked = toggleBookmarked,
|
||||
markPreviousAsRead = markPreviousRead,
|
||||
@@ -164,7 +168,7 @@ private fun getActionItems(
|
||||
favoritesButtonEnabled: Boolean,
|
||||
openInBrowserEnabled: Boolean,
|
||||
openInBrowser: () -> Unit
|
||||
): List<ActionItem> {
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOfNotNull(
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_refresh_manga),
|
||||
@@ -195,5 +199,5 @@ private fun getActionItems(
|
||||
enabled = openInBrowserEnabled,
|
||||
doAction = openInBrowser
|
||||
)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import ca.gosyer.jui.domain.reader.model.ImageScale
|
||||
import ca.gosyer.jui.domain.reader.model.NavigationMode
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.LocalViewModels
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.BackHandler
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
@@ -75,6 +76,8 @@ import ca.gosyer.jui.uicore.components.ErrorScreen
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -107,7 +110,7 @@ expect fun rememberReaderLauncher(): ReaderLauncher
|
||||
fun ReaderMenu(
|
||||
chapterIndex: Int,
|
||||
mangaId: Long,
|
||||
hotkeyFlow: SharedFlow<KeyEvent>,
|
||||
hotkeyFlowHolder: StableHolder<SharedFlow<KeyEvent>>,
|
||||
onCloseRequest: () -> Unit
|
||||
) {
|
||||
val viewModels = LocalViewModels.current
|
||||
@@ -137,8 +140,8 @@ fun ReaderMenu(
|
||||
val currentPageOffset by vm.currentPageOffset.collectAsState()
|
||||
val readerSettingsMenuOpen by vm.readerSettingsMenuOpen.collectAsState()
|
||||
|
||||
LaunchedEffect(hotkeyFlow) {
|
||||
hotkeyFlow.collectLatest {
|
||||
LaunchedEffect(hotkeyFlowHolder) {
|
||||
hotkeyFlowHolder.item.collectLatest {
|
||||
when (it.key) {
|
||||
Key.W, Key.DirectionUp -> vm.navigate(Navigation.PREV)
|
||||
Key.S, Key.DirectionDown -> vm.navigate(Navigation.NEXT)
|
||||
@@ -172,7 +175,7 @@ fun ReaderMenu(
|
||||
currentPageOffset = currentPageOffset,
|
||||
navigate = vm::navigate,
|
||||
navigateTap = vm::navigate,
|
||||
pageEmitter = vm.pageEmitter,
|
||||
pageEmitterHolder = vm.pageEmitter,
|
||||
retry = vm::retry,
|
||||
progress = vm::progress,
|
||||
updateLastPageReadOffset = vm::updateLastPageReadOffset,
|
||||
@@ -201,7 +204,7 @@ fun ReaderMenu(
|
||||
currentPageOffset = currentPageOffset,
|
||||
navigate = vm::navigate,
|
||||
navigateTap = vm::navigate,
|
||||
pageEmitter = vm.pageEmitter,
|
||||
pageEmitterHolder = vm.pageEmitter,
|
||||
retry = vm::retry,
|
||||
progress = vm::progress,
|
||||
updateLastPageReadOffset = vm::updateLastPageReadOffset,
|
||||
@@ -232,8 +235,8 @@ fun WideReaderMenu(
|
||||
previousChapter: ReaderChapter?,
|
||||
chapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
pages: List<ReaderPage>,
|
||||
readerModes: List<String>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
readerModes: ImmutableList<String>,
|
||||
readerMode: String,
|
||||
continuous: Boolean,
|
||||
direction: Direction,
|
||||
@@ -246,7 +249,7 @@ fun WideReaderMenu(
|
||||
currentPageOffset: Int,
|
||||
navigate: (Int) -> Unit,
|
||||
navigateTap: (Navigation) -> Unit,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
pageEmitterHolder: StableHolder<SharedFlow<PageMove>>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit,
|
||||
updateLastPageReadOffset: (Int) -> Unit,
|
||||
@@ -302,7 +305,7 @@ fun WideReaderMenu(
|
||||
currentPage = currentPage,
|
||||
currentPageOffset = currentPageOffset,
|
||||
navigateTap = navigateTap,
|
||||
pageEmitter = pageEmitter,
|
||||
pageEmitterHolder = pageEmitterHolder,
|
||||
retry = retry,
|
||||
progress = progress,
|
||||
updateLastPageReadOffset = updateLastPageReadOffset
|
||||
@@ -316,8 +319,8 @@ fun ThinReaderMenu(
|
||||
previousChapter: ReaderChapter?,
|
||||
chapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
pages: List<ReaderPage>,
|
||||
readerModes: List<String>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
readerModes: ImmutableList<String>,
|
||||
readerMode: String,
|
||||
continuous: Boolean,
|
||||
direction: Direction,
|
||||
@@ -330,7 +333,7 @@ fun ThinReaderMenu(
|
||||
currentPageOffset: Int,
|
||||
navigate: (Int) -> Unit,
|
||||
navigateTap: (Navigation) -> Unit,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
pageEmitterHolder: StableHolder<SharedFlow<PageMove>>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit,
|
||||
updateLastPageReadOffset: (Int) -> Unit,
|
||||
@@ -375,7 +378,7 @@ fun ThinReaderMenu(
|
||||
currentPage = currentPage,
|
||||
currentPageOffset = currentPageOffset,
|
||||
navigateTap = navigateTap,
|
||||
pageEmitter = pageEmitter,
|
||||
pageEmitterHolder = pageEmitterHolder,
|
||||
retry = retry,
|
||||
progress = progress,
|
||||
updateLastPageReadOffset = updateLastPageReadOffset
|
||||
@@ -401,7 +404,7 @@ fun ThinReaderMenu(
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -426,7 +429,7 @@ fun ReaderLayout(
|
||||
previousChapter: ReaderChapter?,
|
||||
chapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
pages: List<ReaderPage>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
continuous: Boolean,
|
||||
direction: Direction,
|
||||
padding: Int,
|
||||
@@ -437,7 +440,7 @@ fun ReaderLayout(
|
||||
currentPage: Int,
|
||||
currentPageOffset: Int,
|
||||
navigateTap: (Navigation) -> Unit,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
pageEmitterHolder: StableHolder<SharedFlow<PageMove>>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit,
|
||||
updateLastPageReadOffset: (Int) -> Unit
|
||||
@@ -471,7 +474,7 @@ fun ReaderLayout(
|
||||
} else {
|
||||
ContentScale.Fit
|
||||
},
|
||||
pageEmitter = pageEmitter,
|
||||
pageEmitterHolder = pageEmitterHolder,
|
||||
retry = retry,
|
||||
progress = progress,
|
||||
updateLastPageReadOffset = updateLastPageReadOffset
|
||||
@@ -487,7 +490,7 @@ fun ReaderLayout(
|
||||
nextChapter = nextChapter,
|
||||
loadingModifier = loadingModifier,
|
||||
pageContentScale = imageScale.toContentScale(),
|
||||
pageEmitter = pageEmitter,
|
||||
pageEmitterHolder = pageEmitterHolder,
|
||||
retry = retry,
|
||||
progress = progress
|
||||
)
|
||||
@@ -510,7 +513,7 @@ fun SideMenuButton(sideMenuOpen: Boolean, onOpenSideMenuClicked: () -> Unit) {
|
||||
@Composable
|
||||
fun ReaderImage(
|
||||
imageIndex: Int,
|
||||
drawable: ImageBitmap?,
|
||||
drawableHolder: StableHolder<ImageBitmap?>,
|
||||
progress: Float,
|
||||
status: ReaderPage.Status,
|
||||
error: String?,
|
||||
@@ -519,7 +522,8 @@ fun ReaderImage(
|
||||
contentScale: ContentScale = ContentScale.Fit,
|
||||
retry: (Int) -> Unit
|
||||
) {
|
||||
Crossfade(drawable to status) { (drawable, status) ->
|
||||
Crossfade(drawableHolder to status) { (drawableHolder, status) ->
|
||||
val drawable = drawableHolder.item
|
||||
if (drawable != null) {
|
||||
Image(
|
||||
bitmap = drawable,
|
||||
|
||||
@@ -21,6 +21,7 @@ import ca.gosyer.jui.domain.manga.model.MangaMeta
|
||||
import ca.gosyer.jui.domain.reader.ReaderModeWatch
|
||||
import ca.gosyer.jui.domain.reader.model.Direction
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.loader.PagesState
|
||||
import ca.gosyer.jui.ui.reader.model.MoveTo
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
@@ -31,6 +32,9 @@ import ca.gosyer.jui.ui.reader.model.ViewerChapters
|
||||
import ca.gosyer.jui.uicore.prefs.asStateIn
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
@@ -45,6 +49,7 @@ import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.single
|
||||
import kotlinx.coroutines.flow.singleOrNull
|
||||
@@ -79,7 +84,7 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private val _pages = MutableStateFlow(emptyList<ReaderPage>())
|
||||
private val _pages = MutableStateFlow<ImmutableList<ReaderPage>>(persistentListOf())
|
||||
val pages = _pages.asStateFlow()
|
||||
|
||||
private val _currentPage = MutableStateFlow(1)
|
||||
@@ -92,9 +97,11 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
val readerSettingsMenuOpen = _readerSettingsMenuOpen.asStateFlow()
|
||||
|
||||
private val _pageEmitter = MutableSharedFlow<PageMove>()
|
||||
val pageEmitter = _pageEmitter.asSharedFlow()
|
||||
val pageEmitter = StableHolder(_pageEmitter.asSharedFlow())
|
||||
|
||||
val readerModes = readerPreferences.modes().asStateIn(scope)
|
||||
.map { it.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
val readerMode = combine(readerPreferences.mode().getAsFlow(), _manga) { mode, manga ->
|
||||
if (
|
||||
manga != null &&
|
||||
@@ -167,7 +174,7 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
|
||||
private fun resetValues() {
|
||||
viewerChapters.recycle()
|
||||
_pages.value = emptyList()
|
||||
_pages.value = persistentListOf()
|
||||
_currentPage.value = 1
|
||||
}
|
||||
|
||||
@@ -274,7 +281,7 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
pages
|
||||
.filterIsInstance<PagesState.Success>()
|
||||
.onEach { (pageList) ->
|
||||
_pages.value = pageList
|
||||
_pages.value = pageList.toImmutableList()
|
||||
pageList.getOrNull(_currentPage.value - 1)?.let { chapter.pageLoader?.loadPage(it) }
|
||||
}
|
||||
.launchIn(chapter.scope)
|
||||
|
||||
@@ -59,13 +59,16 @@ import ca.gosyer.jui.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.jui.uicore.components.AroundLayout
|
||||
import ca.gosyer.jui.uicore.components.Spinner
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ReaderSideMenu(
|
||||
chapter: ReaderChapter,
|
||||
currentPage: Int,
|
||||
readerModes: List<String>,
|
||||
readerModes: ImmutableList<String>,
|
||||
selectedMode: String,
|
||||
onNewPageClicked: (Int) -> Unit,
|
||||
onCloseSideMenuClicked: () -> Unit,
|
||||
@@ -202,7 +205,7 @@ fun ReaderExpandBottomMenu(
|
||||
|
||||
@Composable
|
||||
fun ReaderSheet(
|
||||
readerModes: List<String>,
|
||||
readerModes: ImmutableList<String>,
|
||||
selectedMode: String,
|
||||
onSetReaderMode: (String) -> Unit
|
||||
) {
|
||||
@@ -212,8 +215,8 @@ fun ReaderSheet(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderModeSetting(readerModes: List<String>, selectedMode: String, onSetReaderMode: (String) -> Unit) {
|
||||
val modes by derivedStateOf { listOf(MangaMeta.DEFAULT_READER_MODE) + readerModes }
|
||||
fun ReaderModeSetting(readerModes: ImmutableList<String>, selectedMode: String, onSetReaderMode: (String) -> Unit) {
|
||||
val modes by derivedStateOf { persistentListOf(MangaMeta.DEFAULT_READER_MODE) + readerModes }
|
||||
val defaultModeString = stringResource(MR.strings.default_reader_mode)
|
||||
val displayModes by derivedStateOf { modes.replace(0, defaultModeString) }
|
||||
val selectedModeIndex by derivedStateOf { modes.indexOf(selectedMode) }
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.loader
|
||||
|
||||
import ca.gosyer.jui.core.lang.IO
|
||||
import ca.gosyer.jui.core.lang.throwIfCancellation
|
||||
import ca.gosyer.jui.domain.chapter.interactor.GetChapterPage
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.jui.ui.reader.model.ReaderPage
|
||||
import ca.gosyer.jui.ui.util.compose.toImageBitmap
|
||||
@@ -69,12 +69,12 @@ class TachideskPageLoader(
|
||||
}
|
||||
}
|
||||
.onEach {
|
||||
page.bitmap.value = it.toImageBitmap()
|
||||
page.bitmap.value = StableHolder(it.toImageBitmap())
|
||||
page.status.value = ReaderPage.Status.READY
|
||||
page.error.value = null
|
||||
}
|
||||
.catch {
|
||||
page.bitmap.value = null
|
||||
page.bitmap.value = StableHolder(null)
|
||||
page.status.value = ReaderPage.Status.ERROR
|
||||
page.error.value = it.message
|
||||
log.warn(it) { "Failed to get page ${page.index} for chapter ${chapter.chapter.index} for ${chapter.chapter.mangaId}" }
|
||||
@@ -125,7 +125,7 @@ class TachideskPageLoader(
|
||||
pageRange.map {
|
||||
ReaderPage(
|
||||
index = it,
|
||||
bitmap = MutableStateFlow(null),
|
||||
bitmap = MutableStateFlow(StableHolder(null)),
|
||||
progress = MutableStateFlow(0.0F),
|
||||
status = MutableStateFlow(ReaderPage.Status.QUEUE),
|
||||
error = MutableStateFlow(null),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||
import ca.gosyer.jui.ui.reader.loader.PageLoader
|
||||
import ca.gosyer.jui.ui.reader.loader.PagesState
|
||||
@@ -18,19 +19,19 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.lighthousegames.logging.logging
|
||||
|
||||
@Immutable
|
||||
data class ReaderChapter(val chapter: Chapter) {
|
||||
val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||
|
||||
var state: State =
|
||||
State.Wait
|
||||
private val _state = MutableStateFlow<State>(State.Wait)
|
||||
|
||||
var state: State
|
||||
get() = _state.value
|
||||
set(value) {
|
||||
field = value
|
||||
stateRelay.value = value
|
||||
_state.value = value
|
||||
}
|
||||
|
||||
private val stateRelay by lazy { MutableStateFlow(state) }
|
||||
|
||||
val stateObserver by lazy { stateRelay.asStateFlow() }
|
||||
val stateObserver by lazy { _state.asStateFlow() }
|
||||
|
||||
val pages: StateFlow<PagesState>?
|
||||
get() = (state as? State.Loaded)?.pages
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Immutable
|
||||
data class ReaderPage(
|
||||
val index: Int,
|
||||
val bitmap: MutableStateFlow<ImageBitmap?>,
|
||||
val bitmap: MutableStateFlow<StableHolder<ImageBitmap?>>,
|
||||
val progress: MutableStateFlow<Float>,
|
||||
val status: MutableStateFlow<Status>,
|
||||
val error: MutableStateFlow<String?>,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.navigation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
* | N | P | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
@Immutable
|
||||
class EdgeNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.navigation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
* | P | N | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
@Immutable
|
||||
class KindlishNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.navigation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
* | N | N | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
@Immutable
|
||||
open class LNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.navigation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
* | N | N | P | N: Move Left
|
||||
* +---+---+---+
|
||||
*/
|
||||
@Immutable
|
||||
class RightAndLeftNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
package ca.gosyer.jui.ui.reader.navigation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import ca.gosyer.jui.domain.reader.model.TappingInvertMode
|
||||
import ca.gosyer.jui.ui.reader.model.Navigation
|
||||
|
||||
@Immutable
|
||||
abstract class ViewerNavigation {
|
||||
data class Rect(val xRange: IntRange, val yRange: IntRange) {
|
||||
private val right get() = xRange.last
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.reader.model.Direction
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.ChapterSeparator
|
||||
import ca.gosyer.jui.ui.reader.ReaderImage
|
||||
import ca.gosyer.jui.ui.reader.model.MoveTo
|
||||
@@ -42,6 +43,7 @@ import ca.gosyer.jui.uicore.components.HorizontalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.mapLatest
|
||||
@Composable
|
||||
fun ContinuousReader(
|
||||
modifier: Modifier,
|
||||
pages: List<ReaderPage>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
direction: Direction,
|
||||
maxSize: Int,
|
||||
padding: Int,
|
||||
@@ -61,7 +63,7 @@ fun ContinuousReader(
|
||||
nextChapter: ReaderChapter?,
|
||||
loadingModifier: Modifier,
|
||||
pageContentScale: ContentScale,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
pageEmitterHolder: StableHolder<SharedFlow<PageMove>>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit,
|
||||
updateLastPageReadOffset: (Int) -> Unit
|
||||
@@ -70,7 +72,7 @@ fun ContinuousReader(
|
||||
val state = rememberLazyListState(currentPage, currentPageOffset)
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(Unit) {
|
||||
pageEmitter
|
||||
pageEmitterHolder.item
|
||||
.mapLatest { pageMove ->
|
||||
when (pageMove) {
|
||||
is PageMove.Direction -> {
|
||||
@@ -183,7 +185,7 @@ fun ContinuousReader(
|
||||
|
||||
private fun LazyListScope.items(
|
||||
modifier: Modifier,
|
||||
pages: List<ReaderPage>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
previousChapter: ReaderChapter?,
|
||||
currentChapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
@@ -199,7 +201,7 @@ private fun LazyListScope.items(
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
ReaderImage(
|
||||
imageIndex = image.index,
|
||||
drawable = image.bitmap.collectAsState().value,
|
||||
drawableHolder = image.bitmap.collectAsState().value,
|
||||
progress = image.progress.collectAsState().value,
|
||||
status = image.status.collectAsState().value,
|
||||
error = image.error.collectAsState().value,
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import ca.gosyer.jui.domain.reader.model.Direction
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.reader.ChapterSeparator
|
||||
import ca.gosyer.jui.ui.reader.ReaderImage
|
||||
import ca.gosyer.jui.ui.reader.model.MoveTo
|
||||
@@ -22,6 +23,7 @@ import ca.gosyer.jui.ui.reader.model.ReaderPage
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.VerticalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -31,13 +33,13 @@ fun PagerReader(
|
||||
parentModifier: Modifier,
|
||||
direction: Direction,
|
||||
currentPage: Int,
|
||||
pages: List<ReaderPage>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
previousChapter: ReaderChapter?,
|
||||
currentChapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
loadingModifier: Modifier,
|
||||
pageContentScale: ContentScale,
|
||||
pageEmitter: SharedFlow<PageMove>,
|
||||
pageEmitterHolder: StableHolder<SharedFlow<PageMove>>,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit
|
||||
) {
|
||||
@@ -45,7 +47,7 @@ fun PagerReader(
|
||||
|
||||
LaunchedEffect(pages.size) {
|
||||
val pageRange = 0..(pages.size + 1)
|
||||
pageEmitter
|
||||
pageEmitterHolder.item
|
||||
.mapLatest { pageMove ->
|
||||
when (pageMove) {
|
||||
is PageMove.Direction -> {
|
||||
@@ -118,7 +120,7 @@ fun PagerReader(
|
||||
|
||||
@Composable
|
||||
fun HandlePager(
|
||||
pages: List<ReaderPage>,
|
||||
pages: ImmutableList<ReaderPage>,
|
||||
page: Int,
|
||||
previousChapter: ReaderChapter?,
|
||||
currentChapter: ReaderChapter,
|
||||
@@ -134,7 +136,7 @@ fun HandlePager(
|
||||
val image = pages[page - 1]
|
||||
ReaderImage(
|
||||
imageIndex = image.index,
|
||||
drawable = image.bitmap.collectAsState().value,
|
||||
drawableHolder = image.bitmap.collectAsState().value,
|
||||
progress = image.progress.collectAsState().value,
|
||||
status = image.status.collectAsState().value,
|
||||
error = image.error.collectAsState().value,
|
||||
|
||||
@@ -57,6 +57,7 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class SettingsAppearanceScreen : Screen {
|
||||
@@ -120,7 +121,7 @@ fun SettingsAppearanceScreenContent(
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = themeMode,
|
||||
choices = mapOf(
|
||||
choices = persistentMapOf(
|
||||
ThemeMode.System to stringResource(MR.strings.theme_follow_system),
|
||||
ThemeMode.Light to stringResource(MR.strings.theme_light),
|
||||
ThemeMode.Dark to stringResource(MR.strings.theme_dark)
|
||||
|
||||
@@ -40,6 +40,7 @@ import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||
import ca.gosyer.jui.ui.base.file.rememberFileChooser
|
||||
import ca.gosyer.jui.ui.base.file.rememberFileSaver
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.jui.ui.util.lang.toSource
|
||||
@@ -61,6 +62,9 @@ import com.vanpra.composematerialdialogs.title
|
||||
import io.ktor.client.plugins.onDownload
|
||||
import io.ktor.client.plugins.onUpload
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -93,8 +97,8 @@ class SettingsBackupScreen : Screen {
|
||||
SettingsBackupScreenContent(
|
||||
restoreStatus = vm.restoreStatus.collectAsState().value,
|
||||
creatingStatus = vm.creatingStatus.collectAsState().value,
|
||||
missingSourceFlow = vm.missingSourceFlow,
|
||||
createFlow = vm.createFlow,
|
||||
missingSourceFlowHolder = vm.missingSourceFlowHolder,
|
||||
createFlowHolder = vm.createFlowHolder,
|
||||
restoreFile = vm::restoreFile,
|
||||
restoreBackup = vm::restoreBackup,
|
||||
stopRestore = vm::stopRestore,
|
||||
@@ -113,13 +117,13 @@ class SettingsBackupViewModel @Inject constructor(
|
||||
private val _restoreStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val restoreStatus = _restoreStatus.asStateFlow()
|
||||
|
||||
private val _missingSourceFlow = MutableSharedFlow<Pair<Path, List<String>>>()
|
||||
val missingSourceFlow = _missingSourceFlow.asSharedFlow()
|
||||
private val _missingSourceFlow = MutableSharedFlow<Pair<Path, ImmutableList<String>>>()
|
||||
val missingSourceFlowHolder = StableHolder(_missingSourceFlow.asSharedFlow())
|
||||
|
||||
private val _creatingStatus = MutableStateFlow<Status>(Status.Nothing)
|
||||
val creatingStatus = _creatingStatus.asStateFlow()
|
||||
private val _createFlow = MutableSharedFlow<String>()
|
||||
val createFlow = _createFlow.asSharedFlow()
|
||||
val createFlowHolder = StableHolder(_createFlow.asSharedFlow())
|
||||
fun restoreFile(source: Source) {
|
||||
scope.launch {
|
||||
val file = try {
|
||||
@@ -140,7 +144,7 @@ class SettingsBackupViewModel @Inject constructor(
|
||||
if (missingSources.isEmpty()) {
|
||||
restoreBackup(file)
|
||||
} else {
|
||||
_missingSourceFlow.emit(file to missingSources)
|
||||
_missingSourceFlow.emit(file to missingSources.toImmutableList())
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
@@ -257,8 +261,8 @@ sealed class Status {
|
||||
private fun SettingsBackupScreenContent(
|
||||
restoreStatus: Status,
|
||||
creatingStatus: Status,
|
||||
missingSourceFlow: SharedFlow<Pair<Path, List<String>>>,
|
||||
createFlow: SharedFlow<String>,
|
||||
missingSourceFlowHolder: StableHolder<SharedFlow<Pair<Path, ImmutableList<String>>>>,
|
||||
createFlowHolder: StableHolder<SharedFlow<String>>,
|
||||
restoreFile: (Source) -> Unit,
|
||||
restoreBackup: (Path) -> Unit,
|
||||
stopRestore: () -> Unit,
|
||||
@@ -266,20 +270,20 @@ private fun SettingsBackupScreenContent(
|
||||
exportBackupFileFound: (Sink) -> Unit
|
||||
) {
|
||||
var backupFile by remember { mutableStateOf<Path?>(null) }
|
||||
var missingSources by remember { mutableStateOf(emptyList<String>()) }
|
||||
var missingSources: ImmutableList<String> by remember { mutableStateOf(persistentListOf()) }
|
||||
val dialogState = rememberMaterialDialogState()
|
||||
val fileSaver = rememberFileSaver(exportBackupFileFound)
|
||||
val fileChooser = rememberFileChooser(restoreFile)
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
missingSourceFlow.collect { (backup, sources) ->
|
||||
missingSourceFlowHolder.item.collect { (backup, sources) ->
|
||||
backupFile = backup
|
||||
missingSources = sources
|
||||
dialogState.show()
|
||||
}
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
createFlow.collect { filename ->
|
||||
createFlowHolder.item.collect { filename ->
|
||||
fileSaver.save(filename)
|
||||
}
|
||||
}
|
||||
@@ -330,7 +334,7 @@ private fun SettingsBackupScreenContent(
|
||||
@Composable
|
||||
private fun MissingSourcesDialog(
|
||||
state: MaterialDialogState,
|
||||
missingSources: List<String>,
|
||||
missingSources: ImmutableList<String>,
|
||||
onPositiveClick: () -> Unit,
|
||||
onNegativeClick: () -> Unit
|
||||
) {
|
||||
|
||||
@@ -41,6 +41,9 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -82,7 +85,7 @@ class SettingsGeneralViewModel @Inject constructor(
|
||||
private val currentLocale = Locale.current
|
||||
|
||||
@Composable
|
||||
fun getStartScreenChoices() = mapOf(
|
||||
fun getStartScreenChoices(): ImmutableMap<StartScreen, String> = 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),
|
||||
@@ -90,7 +93,7 @@ class SettingsGeneralViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun getLanguageChoices(): Map<String, String> {
|
||||
fun getLanguageChoices(): ImmutableMap<String, String> {
|
||||
val langJsonState = MR.files.languages.readTextAsync()
|
||||
val langs by produceState(emptyMap(), langJsonState.value) {
|
||||
val langJson = langJsonState.value
|
||||
@@ -106,15 +109,17 @@ class SettingsGeneralViewModel @Inject constructor(
|
||||
}
|
||||
return mapOf("" to stringResource(MR.strings.language_system_default, currentLocale.getDisplayName(currentLocale)))
|
||||
.plus(langs)
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDateChoices(): Map<String, String> {
|
||||
fun getDateChoices(): ImmutableMap<String, String> {
|
||||
return dateHandler.formatOptions
|
||||
.associateWith {
|
||||
it.ifEmpty { stringResource(MR.strings.date_system_default) } +
|
||||
" (${getFormattedDate(it)})"
|
||||
}
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -126,12 +131,12 @@ class SettingsGeneralViewModel @Inject constructor(
|
||||
@Composable
|
||||
fun SettingsGeneralScreenContent(
|
||||
startScreen: PreferenceMutableStateFlow<StartScreen>,
|
||||
startScreenChoices: Map<StartScreen, String>,
|
||||
startScreenChoices: ImmutableMap<StartScreen, String>,
|
||||
confirmExit: PreferenceMutableStateFlow<Boolean>,
|
||||
language: PreferenceMutableStateFlow<String>,
|
||||
languageChoices: Map<String, String>,
|
||||
languageChoices: ImmutableMap<String, String>,
|
||||
dateFormat: PreferenceMutableStateFlow<String>,
|
||||
dateFormatChoices: Map<String, String>
|
||||
dateFormatChoices: ImmutableMap<String, String>
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
||||
@@ -57,6 +57,8 @@ import com.vanpra.composematerialdialogs.MaterialDialog
|
||||
import com.vanpra.composematerialdialogs.MaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -110,12 +112,13 @@ class SettingsLibraryViewModel @Inject constructor(
|
||||
@Composable
|
||||
fun getDisplayModeChoices() = DisplayMode.values()
|
||||
.associateWith { stringResource(it.res) }
|
||||
.toImmutableMap()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsLibraryScreenContent(
|
||||
displayMode: PreferenceMutableStateFlow<DisplayMode>,
|
||||
displayModeChoices: Map<DisplayMode, String>,
|
||||
displayModeChoices: ImmutableMap<DisplayMode, String>,
|
||||
gridColumns: PreferenceMutableStateFlow<Int>,
|
||||
gridSize: PreferenceMutableStateFlow<Int>,
|
||||
showAllCategory: PreferenceMutableStateFlow<Boolean>,
|
||||
|
||||
@@ -26,6 +26,7 @@ import ca.gosyer.jui.domain.reader.model.NavigationMode
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderModePreferences
|
||||
import ca.gosyer.jui.domain.reader.service.ReaderPreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.prefs.ChoicePreference
|
||||
import ca.gosyer.jui.ui.base.prefs.ExpandablePreference
|
||||
@@ -42,11 +43,21 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class SettingsReaderScreen : Screen {
|
||||
@@ -56,7 +67,7 @@ class SettingsReaderScreen : Screen {
|
||||
override fun Content() {
|
||||
val vm = viewModel { settingsReaderViewModel() }
|
||||
SettingsReaderScreenContent(
|
||||
modes = vm.modes.collectAsState().value.associateWith { it },
|
||||
modes = vm.modes.collectAsState().value,
|
||||
selectedMode = vm.selectedMode,
|
||||
modeSettings = vm.modeSettings.collectAsState().value,
|
||||
directionChoices = vm.getDirectionChoices(),
|
||||
@@ -73,31 +84,39 @@ class SettingsReaderViewModel @Inject constructor(
|
||||
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(emptyList<ReaderModePreference>())
|
||||
private val _modeSettings = MutableStateFlow<ImmutableList<StableHolder<ReaderModePreference>>>(
|
||||
persistentListOf()
|
||||
)
|
||||
val modeSettings = _modeSettings.asStateFlow()
|
||||
|
||||
init {
|
||||
modes.onEach { modes ->
|
||||
val modeSettings = _modeSettings.value
|
||||
val modesInSettings = modeSettings.map { it.mode }
|
||||
_modeSettings.value = modeSettings.filter { it.mode in modes } + modes.filter {
|
||||
val modesInSettings = modeSettings.map { it.item.mode }
|
||||
_modeSettings.value = modeSettings.filter { it.item.mode in modes }.toPersistentList() + modes.filter { (it) ->
|
||||
it !in modesInSettings
|
||||
}.map {
|
||||
ReaderModePreference(scope, it, readerPreferences.getMode(it))
|
||||
}.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(
|
||||
@@ -113,11 +132,13 @@ class SettingsReaderViewModel @Inject constructor(
|
||||
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(
|
||||
@@ -149,14 +170,14 @@ data class ReaderModePreference(
|
||||
|
||||
@Composable
|
||||
fun SettingsReaderScreenContent(
|
||||
modes: Map<String, String>,
|
||||
modes: ImmutableMap<String, String>,
|
||||
selectedMode: PreferenceMutableStateFlow<String>,
|
||||
modeSettings: List<ReaderModePreference>,
|
||||
directionChoices: Map<Direction, String>,
|
||||
paddingChoices: Map<Int, String>,
|
||||
getMaxSizeChoices: (Direction) -> Map<Int, String>,
|
||||
imageScaleChoices: Map<ImageScale, String>,
|
||||
navigationModeChoices: Map<NavigationMode, String>
|
||||
modeSettings: ImmutableList<StableHolder<ReaderModePreference>>,
|
||||
directionChoices: ImmutableMap<Direction, String>,
|
||||
paddingChoices: ImmutableMap<Int, String>,
|
||||
getMaxSizeChoices: (Direction) -> ImmutableMap<Int, String>,
|
||||
imageScaleChoices: ImmutableMap<ImageScale, String>,
|
||||
navigationModeChoices: ImmutableMap<NavigationMode, String>
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -176,7 +197,7 @@ fun SettingsReaderScreenContent(
|
||||
item {
|
||||
Divider()
|
||||
}
|
||||
modeSettings.fastForEach {
|
||||
modeSettings.fastForEach { (it) ->
|
||||
item {
|
||||
ExpandablePreference(it.mode) {
|
||||
ChoicePreference(
|
||||
|
||||
@@ -42,6 +42,8 @@ import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
@@ -88,7 +90,7 @@ class SettingsServerViewModel @Inject constructor(
|
||||
val proxy = serverPreferences.proxy().asStateIn(scope)
|
||||
|
||||
@Composable
|
||||
fun getProxyChoices() = mapOf(
|
||||
fun getProxyChoices(): ImmutableMap<Proxy, String> = 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)
|
||||
@@ -102,7 +104,7 @@ class SettingsServerViewModel @Inject constructor(
|
||||
val auth = serverPreferences.auth().asStateIn(scope)
|
||||
|
||||
@Composable
|
||||
fun getAuthChoices() = mapOf(
|
||||
fun getAuthChoices(): ImmutableMap<Auth, String> = 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)
|
||||
@@ -126,13 +128,13 @@ fun SettingsServerScreenContent(
|
||||
serverPort: PreferenceMutableStateFlow<String>,
|
||||
serverPathPrefix: PreferenceMutableStateFlow<String>,
|
||||
proxy: PreferenceMutableStateFlow<Proxy>,
|
||||
proxyChoices: Map<Proxy, String>,
|
||||
proxyChoices: ImmutableMap<Proxy, String>,
|
||||
httpHost: PreferenceMutableStateFlow<String>,
|
||||
httpPort: PreferenceMutableStateFlow<String>,
|
||||
socksHost: PreferenceMutableStateFlow<String>,
|
||||
socksPort: PreferenceMutableStateFlow<String>,
|
||||
auth: PreferenceMutableStateFlow<Auth>,
|
||||
authChoices: Map<Auth, String>,
|
||||
authChoices: ImmutableMap<Auth, String>,
|
||||
authUsername: PreferenceMutableStateFlow<String>,
|
||||
authPassword: PreferenceMutableStateFlow<String>
|
||||
) {
|
||||
|
||||
@@ -8,7 +8,9 @@ package ca.gosyer.jui.ui.sources.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.manga.MangaScreen
|
||||
import ca.gosyer.jui.ui.sources.browse.components.SourceScreenContent
|
||||
import ca.gosyer.jui.ui.sources.browse.filter.SourceFiltersViewModel
|
||||
@@ -22,10 +24,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
|
||||
class SourceScreen(val source: Source, private val initialQuery: String? = null) : Screen {
|
||||
|
||||
|
||||
override val key: ScreenKey = source.id.toString()
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val sourceHolder = remember { StableHolder(source) }
|
||||
val sourceVM = viewModel {
|
||||
sourceViewModel(SourceScreenViewModel.Params(source, initialQuery))
|
||||
}
|
||||
@@ -35,7 +39,7 @@ class SourceScreen(val source: Source, private val initialQuery: String? = null)
|
||||
val sourcesNavigator = LocalSourcesNavigator.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
SourceScreenContent(
|
||||
source = source,
|
||||
sourceHolder = sourceHolder,
|
||||
onMangaClick = { navigator push MangaScreen(it) },
|
||||
onCloseSourceTabClick = if (sourcesNavigator != null) {
|
||||
{ sourcesNavigator.remove(it) }
|
||||
|
||||
@@ -13,8 +13,14 @@ import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.source.model.MangaPage
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@@ -52,7 +58,7 @@ class SourceScreenViewModel(
|
||||
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
|
||||
val gridSize = libraryPreferences.gridSize().stateIn(scope)
|
||||
|
||||
private val _mangas = MutableStateFlow(emptyList<Manga>())
|
||||
private val _mangas = MutableStateFlow<ImmutableList<StableHolder<Manga>>>(persistentListOf())
|
||||
val mangas = _mangas.asStateFlow()
|
||||
|
||||
private val _hasNextPage = MutableStateFlow(false)
|
||||
@@ -82,7 +88,7 @@ class SourceScreenViewModel(
|
||||
init {
|
||||
scope.launch {
|
||||
getPage()?.let { (mangas, hasNextPage) ->
|
||||
_mangas.value = mangas
|
||||
_mangas.value = mangas.map(::StableHolder).toImmutableList()
|
||||
_hasNextPage.value = hasNextPage
|
||||
}
|
||||
|
||||
@@ -96,7 +102,7 @@ class SourceScreenViewModel(
|
||||
_pageNum.value++
|
||||
val page = getPage()
|
||||
if (page != null) {
|
||||
_mangas.value += page.mangaList
|
||||
_mangas.value = _mangas.value.toPersistentList() + page.mangaList.map(::StableHolder)
|
||||
_hasNextPage.value = page.hasNextPage
|
||||
} else {
|
||||
_pageNum.value--
|
||||
@@ -114,7 +120,7 @@ class SourceScreenViewModel(
|
||||
_pageNum.value = 0
|
||||
_loading.value = true
|
||||
_query.value = null
|
||||
_mangas.value = emptyList()
|
||||
_mangas.value = persistentListOf()
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
@@ -136,7 +142,7 @@ class SourceScreenViewModel(
|
||||
_hasNextPage.value = true
|
||||
_loading.value = true
|
||||
_query.value = query
|
||||
_mangas.value = emptyList()
|
||||
_mangas.value = persistentListOf()
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
|
||||
@@ -33,15 +33,17 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceMangaComfortableGrid(
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
onClickManga: (Long) -> Unit,
|
||||
@@ -60,16 +62,16 @@ fun SourceMangaComfortableGrid(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp)
|
||||
) {
|
||||
itemsIndexed(mangas) { index, manga ->
|
||||
itemsIndexed(mangas) { index, mangaHolder ->
|
||||
if (hasNextPage && index == mangas.lastIndex) {
|
||||
LaunchedEffect(Unit) { onLoadNextPage() }
|
||||
}
|
||||
SourceMangaComfortableGridItem(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = { onClickManga(manga.id) }
|
||||
onClick = { onClickManga(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
inLibrary = manga.inLibrary
|
||||
mangaHolder = mangaHolder,
|
||||
inLibrary = mangaHolder.item.inLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,9 +87,10 @@ fun SourceMangaComfortableGrid(
|
||||
@Composable
|
||||
private fun SourceMangaComfortableGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
@@ -36,15 +36,17 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberVerticalScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceMangaCompactGrid(
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
onClickManga: (Long) -> Unit,
|
||||
@@ -63,16 +65,16 @@ fun SourceMangaCompactGrid(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize().padding(4.dp)
|
||||
) {
|
||||
itemsIndexed(mangas) { index, manga ->
|
||||
itemsIndexed(mangas) { index, mangaHolder ->
|
||||
if (hasNextPage && index == mangas.lastIndex) {
|
||||
LaunchedEffect(Unit) { onLoadNextPage() }
|
||||
}
|
||||
SourceMangaCompactGridItem(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = { onClickManga(manga.id) }
|
||||
onClick = { onClickManga(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
inLibrary = manga.inLibrary
|
||||
mangaHolder = mangaHolder,
|
||||
inLibrary = mangaHolder.item.inLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -88,9 +90,10 @@ fun SourceMangaCompactGrid(
|
||||
@Composable
|
||||
private fun SourceMangaCompactGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
@@ -24,16 +24,18 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.components.MangaListItem
|
||||
import ca.gosyer.jui.uicore.components.MangaListItemImage
|
||||
import ca.gosyer.jui.uicore.components.MangaListItemTitle
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceMangaList(
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
onClickManga: (Long) -> Unit,
|
||||
hasNextPage: Boolean = false,
|
||||
onLoadNextPage: () -> Unit
|
||||
@@ -44,16 +46,16 @@ fun SourceMangaList(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
itemsIndexed(mangas) { index, manga ->
|
||||
itemsIndexed(mangas) { index, mangaHolder ->
|
||||
if (hasNextPage && index == mangas.lastIndex) {
|
||||
LaunchedEffect(Unit) { onLoadNextPage() }
|
||||
}
|
||||
MangaListItem(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = { onClickManga(manga.id) }
|
||||
onClick = { onClickManga(mangaHolder.item.id) }
|
||||
),
|
||||
manga = manga,
|
||||
inLibrary = manga.inLibrary
|
||||
mangaHolder = mangaHolder,
|
||||
inLibrary = mangaHolder.item.inLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,9 +71,10 @@ fun SourceMangaList(
|
||||
@Composable
|
||||
private fun MangaListItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
MangaListItem(
|
||||
modifier = modifier then Modifier
|
||||
.requiredHeight(56.dp)
|
||||
|
||||
@@ -47,6 +47,7 @@ import ca.gosyer.jui.domain.library.model.DisplayMode
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.BackHandler
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
@@ -56,17 +57,19 @@ import ca.gosyer.jui.uicore.components.DropdownMenu
|
||||
import ca.gosyer.jui.uicore.components.DropdownMenuItem
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceScreenContent(
|
||||
source: Source,
|
||||
sourceHolder: StableHolder<Source>,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
onCloseSourceTabClick: (Source) -> Unit,
|
||||
onSourceSettingsClick: (Long) -> Unit,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
hasNextPage: Boolean,
|
||||
loading: Boolean,
|
||||
isLatest: Boolean,
|
||||
@@ -80,12 +83,13 @@ fun SourceScreenContent(
|
||||
setUsingFilters: (Boolean) -> Unit,
|
||||
onSelectDisplayMode: (DisplayMode) -> Unit,
|
||||
// filter
|
||||
filters: List<SourceFiltersView<*, *>>,
|
||||
filters: ImmutableList<StableHolder<SourceFiltersView<*, *>>>,
|
||||
showingFilters: Boolean,
|
||||
showFilterButton: Boolean,
|
||||
setShowingFilters: (Boolean) -> Unit,
|
||||
resetFiltersClicked: () -> Unit
|
||||
) {
|
||||
val source = sourceHolder.item
|
||||
LaunchedEffect(source) {
|
||||
enableLatest(source.supportsLatest)
|
||||
}
|
||||
@@ -101,7 +105,7 @@ fun SourceScreenContent(
|
||||
BoxWithConstraints {
|
||||
if (maxWidth > 720.dp) {
|
||||
SourceWideScreenContent(
|
||||
source = source,
|
||||
sourceHolder = sourceHolder,
|
||||
onMangaClick = onMangaClick,
|
||||
onCloseSourceTabClick = onCloseSourceTabClick,
|
||||
onSourceSettingsClick = onSourceSettingsClick,
|
||||
@@ -128,7 +132,7 @@ fun SourceScreenContent(
|
||||
)
|
||||
} else {
|
||||
SourceThinScreenContent(
|
||||
source = source,
|
||||
sourceHolder = sourceHolder,
|
||||
onMangaClick = onMangaClick,
|
||||
onCloseSourceTabClick = onCloseSourceTabClick,
|
||||
onSourceSettingsClick = onSourceSettingsClick,
|
||||
@@ -159,14 +163,14 @@ fun SourceScreenContent(
|
||||
|
||||
@Composable
|
||||
private fun SourceWideScreenContent(
|
||||
source: Source,
|
||||
sourceHolder: StableHolder<Source>,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
onCloseSourceTabClick: (Source) -> Unit,
|
||||
onSourceSettingsClick: (Long) -> Unit,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
hasNextPage: Boolean,
|
||||
loading: Boolean,
|
||||
isLatest: Boolean,
|
||||
@@ -178,7 +182,7 @@ private fun SourceWideScreenContent(
|
||||
loadNextPage: () -> Unit,
|
||||
setUsingFilters: (Boolean) -> Unit,
|
||||
// filter
|
||||
filters: List<SourceFiltersView<*, *>>,
|
||||
filters: ImmutableList<StableHolder<SourceFiltersView<*, *>>>,
|
||||
showingFilters: Boolean,
|
||||
showFilterButton: Boolean,
|
||||
setShowingFilters: (Boolean) -> Unit,
|
||||
@@ -188,7 +192,7 @@ private fun SourceWideScreenContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SourceToolbar(
|
||||
source = source,
|
||||
sourceHolder = sourceHolder,
|
||||
onCloseSourceTabClick = onCloseSourceTabClick,
|
||||
sourceSearchQuery = sourceSearchQuery,
|
||||
onSearch = search,
|
||||
@@ -249,14 +253,14 @@ private fun SourceWideScreenContent(
|
||||
|
||||
@Composable
|
||||
private fun SourceThinScreenContent(
|
||||
source: Source,
|
||||
sourceHolder: StableHolder<Source>,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
onCloseSourceTabClick: (Source) -> Unit,
|
||||
onSourceSettingsClick: (Long) -> Unit,
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
hasNextPage: Boolean,
|
||||
loading: Boolean,
|
||||
isLatest: Boolean,
|
||||
@@ -268,7 +272,7 @@ private fun SourceThinScreenContent(
|
||||
loadNextPage: () -> Unit,
|
||||
setUsingFilters: (Boolean) -> Unit,
|
||||
// filter
|
||||
filters: List<SourceFiltersView<*, *>>,
|
||||
filters: ImmutableList<StableHolder<SourceFiltersView<*, *>>>,
|
||||
showingFilters: Boolean,
|
||||
showFilterButton: Boolean,
|
||||
setShowingFilters: (Boolean) -> Unit,
|
||||
@@ -296,7 +300,7 @@ private fun SourceThinScreenContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SourceToolbar(
|
||||
source = source,
|
||||
sourceHolder = sourceHolder,
|
||||
onCloseSourceTabClick = onCloseSourceTabClick,
|
||||
sourceSearchQuery = sourceSearchQuery,
|
||||
onSearch = search,
|
||||
@@ -375,7 +379,7 @@ private fun SourceThinScreenContent(
|
||||
|
||||
@Composable
|
||||
fun SourceToolbar(
|
||||
source: Source,
|
||||
sourceHolder: StableHolder<Source>,
|
||||
onCloseSourceTabClick: (Source) -> Unit,
|
||||
sourceSearchQuery: String?,
|
||||
onSearch: (String) -> Unit,
|
||||
@@ -389,6 +393,7 @@ fun SourceToolbar(
|
||||
onToggleFiltersClick: (Boolean) -> Unit,
|
||||
onSelectDisplayMode: (DisplayMode) -> Unit
|
||||
) {
|
||||
val source = sourceHolder.item
|
||||
Toolbar(
|
||||
source.name,
|
||||
closable = true,
|
||||
@@ -454,7 +459,7 @@ private fun MangaTable(
|
||||
displayMode: DisplayMode,
|
||||
gridColumns: Int,
|
||||
gridSize: Int,
|
||||
mangas: List<Manga>,
|
||||
mangas: ImmutableList<StableHolder<Manga>>,
|
||||
isLoading: Boolean = false,
|
||||
hasNextPage: Boolean = false,
|
||||
onLoadNextPage: () -> Unit,
|
||||
@@ -502,7 +507,7 @@ private fun getActionItems(
|
||||
onToggleFiltersClick: () -> Unit,
|
||||
onClickMode: () -> Unit,
|
||||
openDisplayModeSelect: () -> Unit
|
||||
): List<ActionItem> {
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOfNotNull(
|
||||
if (showFilterButton) {
|
||||
ActionItem(
|
||||
@@ -541,5 +546,5 @@ private fun getActionItems(
|
||||
doAction = onSourceSettingsClick
|
||||
)
|
||||
} else null
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import ca.gosyer.jui.domain.source.model.sourcefilters.SortFilter
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.prefs.ExpandablePreference
|
||||
import ca.gosyer.jui.ui.sources.browse.filter.model.SourceFiltersView
|
||||
import ca.gosyer.jui.uicore.components.Spinner
|
||||
@@ -68,12 +69,13 @@ import ca.gosyer.jui.uicore.components.keyboardHandler
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
@Composable
|
||||
fun SourceFiltersMenu(
|
||||
modifier: Modifier,
|
||||
filters: List<SourceFiltersView<*, *>>,
|
||||
filters: ImmutableList<StableHolder<SourceFiltersView<*, *>>>,
|
||||
onSearchClicked: () -> Unit,
|
||||
resetFiltersClicked: () -> Unit
|
||||
) {
|
||||
@@ -98,7 +100,7 @@ fun SourceFiltersMenu(
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
filters.fastForEach { item ->
|
||||
item.toView(startExpanded = item.index in expandedGroups) { expanded, index ->
|
||||
item.toView(startExpanded = item.item.index in expandedGroups) { expanded, index ->
|
||||
if (expanded) {
|
||||
expandedGroups += index
|
||||
} else {
|
||||
@@ -118,17 +120,32 @@ fun SourceFiltersMenu(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun StableHolder<SourceFiltersView<*, *>>.toView(startExpanded: Boolean = false, onExpandChanged: ((Boolean, Int) -> Unit)? = null) {
|
||||
when (this.item) {
|
||||
is SourceFiltersView.CheckBox -> CheckboxView(this as StableHolder<SourceFiltersView.CheckBox>)
|
||||
is SourceFiltersView.Group -> GroupView(this as StableHolder<SourceFiltersView.Group>, startExpanded, onExpandChanged)
|
||||
is SourceFiltersView.Header -> HeaderView(this as StableHolder<SourceFiltersView.Header>)
|
||||
is SourceFiltersView.Select -> SelectView(this as StableHolder<SourceFiltersView.Select>)
|
||||
is SourceFiltersView.Separator -> SeparatorView()
|
||||
is SourceFiltersView.Sort -> SortView(this as StableHolder<SourceFiltersView.Sort>, startExpanded, onExpandChanged)
|
||||
is SourceFiltersView.Text -> TextView(this as StableHolder<SourceFiltersView.Text>)
|
||||
is SourceFiltersView.TriState -> TriStateView(this as StableHolder<SourceFiltersView.TriState>)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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)
|
||||
is SourceFiltersView.Header -> HeaderView(this)
|
||||
is SourceFiltersView.Select -> SelectView(this)
|
||||
is SourceFiltersView.CheckBox -> CheckboxView(StableHolder(this))
|
||||
is SourceFiltersView.Group -> GroupView(StableHolder(this), startExpanded, onExpandChanged)
|
||||
is SourceFiltersView.Header -> HeaderView(StableHolder(this))
|
||||
is SourceFiltersView.Select -> SelectView(StableHolder(this))
|
||||
is SourceFiltersView.Separator -> SeparatorView()
|
||||
is SourceFiltersView.Sort -> SortView(this, startExpanded, onExpandChanged)
|
||||
is SourceFiltersView.Text -> TextView(this)
|
||||
is SourceFiltersView.TriState -> TriStateView(this)
|
||||
is SourceFiltersView.Sort -> SortView(StableHolder(this), startExpanded, onExpandChanged)
|
||||
is SourceFiltersView.Text -> TextView(StableHolder(this))
|
||||
is SourceFiltersView.TriState -> TriStateView(StableHolder(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +175,8 @@ fun SourceFilterAction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupView(group: SourceFiltersView.Group, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)? = null) {
|
||||
fun GroupView(groupHolder: StableHolder<SourceFiltersView.Group>, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)? = null) {
|
||||
val group = groupHolder.item
|
||||
val state by key(group.hashCode()) { group.state.collectAsState() }
|
||||
ExpandablePreference(
|
||||
title = group.name,
|
||||
@@ -174,7 +192,8 @@ fun GroupView(group: SourceFiltersView.Group, startExpanded: Boolean, onExpandCh
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckboxView(checkBox: SourceFiltersView.CheckBox) {
|
||||
fun CheckboxView(checkBoxHolder: StableHolder<SourceFiltersView.CheckBox>) {
|
||||
val checkBox = checkBoxHolder.item
|
||||
val state by key(checkBox.hashCode()) { checkBox.state.collectAsState() }
|
||||
SourceFilterAction(
|
||||
name = checkBox.name,
|
||||
@@ -186,7 +205,8 @@ fun CheckboxView(checkBox: SourceFiltersView.CheckBox) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeaderView(header: SourceFiltersView.Header) {
|
||||
fun HeaderView(headerHolder: StableHolder<SourceFiltersView.Header>) {
|
||||
val header = headerHolder.item
|
||||
Box(Modifier.padding(horizontal = 16.dp, vertical = 8.dp).fillMaxWidth()) {
|
||||
Text(
|
||||
text = header.name,
|
||||
@@ -199,7 +219,8 @@ fun HeaderView(header: SourceFiltersView.Header) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectView(select: SourceFiltersView.Select) {
|
||||
fun SelectView(selectHolder: StableHolder<SourceFiltersView.Select>) {
|
||||
val select = selectHolder.item
|
||||
val state by key(select.hashCode()) { select.state.collectAsState() }
|
||||
Row(
|
||||
Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp)
|
||||
@@ -258,7 +279,8 @@ fun SortRow(name: String, selected: Boolean, asc: Boolean, onClick: () -> Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SortView(sort: SourceFiltersView.Sort, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)?) {
|
||||
fun SortView(sortHolder: StableHolder<SourceFiltersView.Sort>, startExpanded: Boolean, onExpandChanged: ((Boolean, Int) -> Unit)?) {
|
||||
val sort = sortHolder.item
|
||||
val state by key(sort.hashCode()) { sort.state.collectAsState() }
|
||||
ExpandablePreference(
|
||||
sort.name,
|
||||
@@ -289,7 +311,8 @@ fun SortView(sort: SourceFiltersView.Sort, startExpanded: Boolean, onExpandChang
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextView(text: SourceFiltersView.Text) {
|
||||
fun TextView(textHolder: StableHolder<SourceFiltersView.Text>) {
|
||||
val text = textHolder.item
|
||||
val placeholderText = remember(text) { text.filter.name }
|
||||
val state by key(text.hashCode()) { text.state.collectAsState() }
|
||||
var stateText by remember(text, state) {
|
||||
@@ -323,7 +346,8 @@ fun TextView(text: SourceFiltersView.Text) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TriStateView(triState: SourceFiltersView.TriState) {
|
||||
fun TriStateView(triStateHolder: StableHolder<SourceFiltersView.TriState>) {
|
||||
val triState = triStateHolder.item
|
||||
val state by key(triState.hashCode()) { triState.state.collectAsState() }
|
||||
SourceFilterAction(
|
||||
name = triState.name,
|
||||
|
||||
@@ -8,9 +8,13 @@ package ca.gosyer.jui.ui.sources.browse.filter
|
||||
|
||||
import ca.gosyer.jui.data.source.SourceRepositoryImpl
|
||||
import ca.gosyer.jui.domain.source.model.sourcefilters.SourceFilter
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.browse.filter.model.SourceFiltersView
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@@ -42,7 +46,7 @@ class SourceFiltersViewModel(
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
|
||||
private val _filters = MutableStateFlow<List<SourceFiltersView<*, *>>>(emptyList())
|
||||
private val _filters = MutableStateFlow<ImmutableList<StableHolder<SourceFiltersView<*, *>>>>(persistentListOf())
|
||||
val filters = _filters.asStateFlow()
|
||||
|
||||
private val _showingFilters = MutableStateFlow(false)
|
||||
@@ -57,7 +61,7 @@ class SourceFiltersViewModel(
|
||||
filters.mapLatest { settings ->
|
||||
_filterButtonEnabled.value = settings.isNotEmpty()
|
||||
supervisorScope {
|
||||
settings.forEach { filter ->
|
||||
settings.forEach { (filter) ->
|
||||
if (filter is SourceFiltersView.Group) {
|
||||
filter.state.value.forEach { childFilter ->
|
||||
childFilter.state.drop(1)
|
||||
@@ -117,7 +121,7 @@ class SourceFiltersViewModel(
|
||||
|
||||
private fun List<SourceFilter>.toView() = mapIndexed { index, sourcePreference ->
|
||||
SourceFiltersView(index, sourcePreference)
|
||||
}
|
||||
}.map(::StableHolder).toImmutableList()
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
|
||||
@@ -36,12 +36,14 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.components.CursorPoint
|
||||
import ca.gosyer.jui.ui.base.components.TooltipArea
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.home.SourceHomeScreen
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
expect fun Modifier.sourceSideMenuItem(
|
||||
onSourceTabClick: () -> Unit,
|
||||
@@ -50,11 +52,11 @@ expect fun Modifier.sourceSideMenuItem(
|
||||
|
||||
@Composable
|
||||
fun SourcesMenu() {
|
||||
val homeScreen = remember { SourceHomeScreen() }
|
||||
val homeScreenHolder = remember { StableHolder(SourceHomeScreen()) }
|
||||
BoxWithConstraints {
|
||||
if (maxWidth > 720.dp) {
|
||||
SourcesNavigator(
|
||||
homeScreen
|
||||
homeScreenHolder
|
||||
) { navigator ->
|
||||
Row {
|
||||
SourcesSideMenu(
|
||||
@@ -75,14 +77,14 @@ fun SourcesMenu() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
homeScreen.Content()
|
||||
homeScreenHolder.item.Content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourcesSideMenu(
|
||||
sourceTabs: List<SourceNavigatorScreen>,
|
||||
sourceTabs: ImmutableList<SourceNavigatorScreen>,
|
||||
onSourceTabClick: (SourceNavigatorScreen) -> Unit,
|
||||
onCloseSourceTabClick: (SourceNavigatorScreen.SourceScreen) -> Unit
|
||||
) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.browse.SourceScreen
|
||||
import ca.gosyer.jui.ui.sources.globalsearch.GlobalSearchScreen
|
||||
import ca.gosyer.jui.ui.sources.home.SourceHomeScreen
|
||||
@@ -29,6 +30,7 @@ import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.stack.StackEvent
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
typealias SourcesNavigatorContent = @Composable (sourcesNavigator: SourcesNavigator) -> Unit
|
||||
|
||||
@@ -37,11 +39,11 @@ val LocalSourcesNavigator: ProvidableCompositionLocal<SourcesNavigator?> =
|
||||
|
||||
@Composable
|
||||
fun SourcesNavigator(
|
||||
homeScreen: SourceHomeScreen,
|
||||
homeScreenHolder: StableHolder<SourceHomeScreen>,
|
||||
content: SourcesNavigatorContent = { CurrentSource() }
|
||||
) {
|
||||
Navigator(homeScreen, autoDispose = false, onBackPressed = null) { navigator ->
|
||||
val sourcesNavigator = rememberNavigator(navigator, homeScreen)
|
||||
Navigator(homeScreenHolder.item, autoDispose = false, onBackPressed = null) { navigator ->
|
||||
val sourcesNavigator = rememberNavigator(navigator, homeScreenHolder.item)
|
||||
|
||||
DisposableEffect(sourcesNavigator) {
|
||||
onDispose(sourcesNavigator::dispose)
|
||||
@@ -176,7 +178,7 @@ class SourcesNavigator internal constructor(
|
||||
screens.values.filterIsInstance<SourceScreen>().map {
|
||||
SourceNavigatorScreen.SourceScreen(it.source)
|
||||
}.let(::addAll)
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun clearEvent() = navigator.clearEvent()
|
||||
|
||||
@@ -7,14 +7,17 @@
|
||||
package ca.gosyer.jui.ui.sources.globalsearch
|
||||
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import ca.gosyer.jui.core.lang.IO
|
||||
import ca.gosyer.jui.data.source.SourceRepositoryImpl
|
||||
import ca.gosyer.jui.domain.source.model.MangaPage
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -56,8 +59,8 @@ class GlobalSearchViewModel @Inject constructor(
|
||||
val sources = combine(installedSources, languages) { installedSources, languages ->
|
||||
installedSources.filter {
|
||||
it.lang in languages || it.id == Source.LOCAL_SOURCE_ID
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
}.map(::StableHolder).toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val search = MutableStateFlow(params.initialQuery)
|
||||
|
||||
@@ -96,7 +99,7 @@ class GlobalSearchViewModel @Inject constructor(
|
||||
.mapLatest { (query, sources) ->
|
||||
results.clear()
|
||||
supervisorScope {
|
||||
sources.map { source ->
|
||||
sources.map { (source) ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
sourceHandler
|
||||
@@ -105,7 +108,7 @@ class GlobalSearchViewModel @Inject constructor(
|
||||
if (it.mangaList.isEmpty()) {
|
||||
Search.Failure(MR.strings.no_results_found.toPlatformString())
|
||||
} else {
|
||||
Search.Success(it)
|
||||
Search.Success(it.mangaList.map(::StableHolder).toImmutableList())
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
@@ -140,7 +143,7 @@ class GlobalSearchViewModel @Inject constructor(
|
||||
|
||||
sealed class Search {
|
||||
object Searching : Search()
|
||||
data class Success(val mangaPage: MangaPage) : Search()
|
||||
data class Success(val mangaList: ImmutableList<StableHolder<Manga>>) : Search()
|
||||
data class Failure(val e: String?) : Search() {
|
||||
constructor(e: Throwable) : this(e.message)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.times
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.browse.components.SourceMangaBadges
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
@@ -33,9 +34,10 @@ import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
@Composable
|
||||
fun GlobalSearchMangaComfortableGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.browse.components.SourceMangaBadges
|
||||
import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
@@ -36,9 +37,10 @@ import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
@Composable
|
||||
fun GlobalSearchMangaCompactGridItem(
|
||||
modifier: Modifier,
|
||||
manga: Manga,
|
||||
mangaHolder: StableHolder<Manga>,
|
||||
inLibrary: Boolean
|
||||
) {
|
||||
val manga = mangaHolder.item
|
||||
val fontStyle = LocalTextStyle.current.merge(
|
||||
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
|
||||
)
|
||||
|
||||
@@ -28,6 +28,9 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -38,6 +41,7 @@ import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.source.model.Source
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.components.localeToString
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.sources.globalsearch.GlobalSearchViewModel.Search
|
||||
import ca.gosyer.jui.uicore.components.ErrorScreen
|
||||
@@ -46,10 +50,11 @@ import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreenContent(
|
||||
sources: List<Source>,
|
||||
sources: ImmutableList<StableHolder<Source>>,
|
||||
results: SnapshotStateMap<Long, Search>,
|
||||
displayMode: DisplayMode,
|
||||
query: String,
|
||||
@@ -70,14 +75,26 @@ fun GlobalSearchScreenContent(
|
||||
) { padding ->
|
||||
Box(Modifier.padding(padding)) {
|
||||
val state = rememberLazyListState()
|
||||
val sourcesSuccess by remember(sources) {
|
||||
derivedStateOf {
|
||||
sources.filter { results[it.item.id] is Search.Success }
|
||||
}
|
||||
}
|
||||
val loadingSources by remember(sources) {
|
||||
derivedStateOf {
|
||||
sources.filter { results[it.item.id] == null }
|
||||
}
|
||||
}
|
||||
val failedSources by remember(sources) {
|
||||
derivedStateOf {
|
||||
sources.filter { results[it.item.id] is Search.Failure }
|
||||
}
|
||||
}
|
||||
LazyColumn(state = state) {
|
||||
val sourcesSuccess = sources.filter { results[it.id] is Search.Success }
|
||||
val loadingSources = sources.filter { results[it.id] == null }
|
||||
val failedSources = sources.filter { results[it.id] is Search.Failure }
|
||||
items(sourcesSuccess) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
sourceHolder = it,
|
||||
search = results[it.item.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
@@ -85,8 +102,8 @@ fun GlobalSearchScreenContent(
|
||||
}
|
||||
items(loadingSources) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
sourceHolder = it,
|
||||
search = results[it.item.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
@@ -94,8 +111,8 @@ fun GlobalSearchScreenContent(
|
||||
}
|
||||
items(failedSources) {
|
||||
GlobalSearchItem(
|
||||
source = it,
|
||||
search = results[it.id] ?: Search.Searching,
|
||||
sourceHolder = it,
|
||||
search = results[it.item.id] ?: Search.Searching,
|
||||
displayMode = displayMode,
|
||||
onSourceClick = onSourceClick,
|
||||
onMangaClick = onMangaClick
|
||||
@@ -114,12 +131,13 @@ fun GlobalSearchScreenContent(
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchItem(
|
||||
source: Source,
|
||||
sourceHolder: StableHolder<Source>,
|
||||
search: Search,
|
||||
displayMode: DisplayMode,
|
||||
onSourceClick: (Source) -> Unit,
|
||||
onMangaClick: (Manga) -> Unit
|
||||
) {
|
||||
val source = sourceHolder.item
|
||||
Column {
|
||||
Row(
|
||||
Modifier.fillMaxWidth()
|
||||
@@ -159,18 +177,18 @@ fun GlobalSearchItem(
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
LazyRow(Modifier.fillMaxSize(), state) {
|
||||
items(search.mangaPage.mangaList) {
|
||||
items(search.mangaList) {
|
||||
if (displayMode == DisplayMode.ComfortableGrid) {
|
||||
GlobalSearchMangaComfortableGridItem(
|
||||
Modifier.clickable { onMangaClick(it) },
|
||||
Modifier.clickable { onMangaClick(it.item) },
|
||||
it,
|
||||
it.inLibrary
|
||||
it.item.inLibrary
|
||||
)
|
||||
} else {
|
||||
GlobalSearchMangaCompactGridItem(
|
||||
Modifier.clickable { onMangaClick(it) },
|
||||
Modifier.clickable { onMangaClick(it.item) },
|
||||
it,
|
||||
it.inLibrary
|
||||
it.item.inLibrary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
package ca.gosyer.jui.ui.sources.home
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import ca.gosyer.jui.core.lang.displayName
|
||||
import ca.gosyer.jui.data.source.SourceRepositoryImpl
|
||||
@@ -14,6 +15,10 @@ import ca.gosyer.jui.domain.source.service.CatalogPreferences
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -38,6 +43,8 @@ class SourceHomeScreenViewModel @Inject constructor(
|
||||
|
||||
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()
|
||||
@@ -72,11 +79,13 @@ class SourceHomeScreenViewModel @Inject constructor(
|
||||
.flatMap { (key, value) ->
|
||||
listOf(SourceUI.Header(key)) + value
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
val sourceLanguages = installedSources.map { sources ->
|
||||
sources.map { it.lang }.distinct() - Source.LOCAL_SOURCE_LANG
|
||||
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
sources.map { it.lang }.distinct().minus(Source.LOCAL_SOURCE_LANG)
|
||||
.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _query = MutableStateFlow("")
|
||||
val query = _query.asStateFlow()
|
||||
@@ -112,7 +121,10 @@ class SourceHomeScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
sealed class SourceUI {
|
||||
@Stable
|
||||
data class Header(val header: String) : SourceUI()
|
||||
@Stable
|
||||
data class SourceItem(val source: Source) : SourceUI()
|
||||
}
|
||||
|
||||
@@ -60,14 +60,18 @@ import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.image.ImageLoaderImage
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceHomeScreenContent(
|
||||
onAddSource: (Source) -> Unit,
|
||||
isLoading: Boolean,
|
||||
sources: List<SourceUI>,
|
||||
languages: Set<String>,
|
||||
sourceLanguages: List<String>,
|
||||
sources: ImmutableList<SourceUI>,
|
||||
languages: ImmutableSet<String>,
|
||||
sourceLanguages: ImmutableList<String>,
|
||||
setEnabledLanguages: (Set<String>) -> Unit,
|
||||
query: String,
|
||||
setQuery: (String) -> Unit,
|
||||
@@ -125,7 +129,7 @@ fun SourceHomeScreenToolbar(
|
||||
|
||||
@Composable
|
||||
fun WideSourcesMenu(
|
||||
sources: List<SourceUI>,
|
||||
sources: ImmutableList<SourceUI>,
|
||||
onAddSource: (Source) -> Unit
|
||||
) {
|
||||
Box {
|
||||
@@ -159,7 +163,7 @@ fun WideSourcesMenu(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
is SourceUI.SourceItem -> WideSourceItem(
|
||||
sourceUI.source,
|
||||
sourceUI,
|
||||
onSourceClicked = onAddSource
|
||||
)
|
||||
}
|
||||
@@ -176,9 +180,10 @@ fun WideSourcesMenu(
|
||||
|
||||
@Composable
|
||||
fun WideSourceItem(
|
||||
source: Source,
|
||||
sourceItem: SourceUI.SourceItem,
|
||||
onSourceClicked: (Source) -> Unit
|
||||
) {
|
||||
val source = sourceItem.source
|
||||
TooltipArea(
|
||||
{
|
||||
Surface(
|
||||
@@ -216,7 +221,7 @@ fun WideSourceItem(
|
||||
|
||||
@Composable
|
||||
fun ThinSourcesMenu(
|
||||
sources: List<SourceUI>,
|
||||
sources: ImmutableList<SourceUI>,
|
||||
onAddSource: (Source) -> Unit
|
||||
) {
|
||||
Box {
|
||||
@@ -243,7 +248,7 @@ fun ThinSourcesMenu(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
is SourceUI.SourceItem -> ThinSourceItem(
|
||||
sourceUI.source,
|
||||
sourceUI,
|
||||
onSourceClicked = onAddSource
|
||||
)
|
||||
}
|
||||
@@ -260,9 +265,10 @@ fun ThinSourcesMenu(
|
||||
|
||||
@Composable
|
||||
fun ThinSourceItem(
|
||||
source: Source,
|
||||
sourceItem: SourceUI.SourceItem,
|
||||
onSourceClicked: (Source) -> Unit
|
||||
) {
|
||||
val source = sourceItem.source
|
||||
Row(
|
||||
Modifier.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
@@ -301,12 +307,12 @@ fun ThinSourceItem(
|
||||
@Stable
|
||||
private fun getActionItems(
|
||||
openEnabledLanguagesClick: () -> Unit
|
||||
): List<ActionItem> {
|
||||
return listOf(
|
||||
): ImmutableList<ActionItem> {
|
||||
return persistentListOf(
|
||||
ActionItem(
|
||||
stringResource(MR.strings.enabled_languages),
|
||||
Icons.Rounded.Translate,
|
||||
doAction = openEnabledLanguagesClick
|
||||
)
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ package ca.gosyer.jui.ui.sources.settings
|
||||
|
||||
import ca.gosyer.jui.data.source.SourceRepositoryImpl
|
||||
import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.sources.settings.model.SourceSettingsView
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@@ -32,14 +36,14 @@ class SourceSettingsScreenViewModel @Inject constructor(
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
|
||||
private val _sourceSettings = MutableStateFlow<List<SourceSettingsView<*, *>>>(emptyList())
|
||||
private val _sourceSettings = MutableStateFlow<ImmutableList<StableHolder<SourceSettingsView<*, *>>>>(persistentListOf())
|
||||
val sourceSettings = _sourceSettings.asStateFlow()
|
||||
|
||||
init {
|
||||
getSourceSettings()
|
||||
sourceSettings.mapLatest { settings ->
|
||||
supervisorScope {
|
||||
settings.forEach { setting ->
|
||||
settings.forEach { (setting) ->
|
||||
setting.state.drop(1)
|
||||
.filterNotNull()
|
||||
.onEach {
|
||||
@@ -73,7 +77,7 @@ class SourceSettingsScreenViewModel @Inject constructor(
|
||||
|
||||
private fun List<SourcePreference>.toView() = mapIndexed { index, sourcePreference ->
|
||||
SourceSettingsView(index, sourcePreference)
|
||||
}
|
||||
}.map(::StableHolder).toImmutableList()
|
||||
|
||||
private companion object {
|
||||
private val log = logging()
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.presentation.build.BuildKonfig
|
||||
import ca.gosyer.jui.ui.base.dialog.getMaterialDialogProperties
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.prefs.ChoiceDialog
|
||||
import ca.gosyer.jui.ui.base.prefs.MultiSelectDialog
|
||||
@@ -47,11 +48,11 @@ import com.vanpra.composematerialdialogs.input
|
||||
import com.vanpra.composematerialdialogs.message
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import com.vanpra.composematerialdialogs.title
|
||||
import kotlin.collections.List as KtList
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun SourceSettingsScreenContent(
|
||||
settings: KtList<SourceSettingsView<*, *>>
|
||||
settings: ImmutableList<StableHolder<SourceSettingsView<*, *>>>
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -61,19 +62,20 @@ fun SourceSettingsScreenContent(
|
||||
Box(Modifier.padding(padding)) {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
items(settings, { it.props.hashCode() }) {
|
||||
when (it) {
|
||||
items(settings, { it.item.props.hashCode() }) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (it.item) {
|
||||
is CheckBox, is Switch -> {
|
||||
TwoStatePreference(it as TwoState, it is CheckBox)
|
||||
TwoStatePreference(it as StableHolder<TwoState>, it.item is CheckBox)
|
||||
}
|
||||
is List -> {
|
||||
ListPreference(it)
|
||||
ListPreference(it as StableHolder<List>)
|
||||
}
|
||||
is EditText -> {
|
||||
EditTextPreference(it)
|
||||
EditTextPreference(it as StableHolder<EditText>)
|
||||
}
|
||||
is MultiSelect -> {
|
||||
MultiSelectPreference(it)
|
||||
MultiSelectPreference(it as StableHolder<MultiSelect>)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +91,8 @@ fun SourceSettingsScreenContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TwoStatePreference(twoState: TwoState, checkbox: Boolean) {
|
||||
private fun TwoStatePreference(twoStateHolder: StableHolder<TwoState>, checkbox: Boolean) {
|
||||
val twoState = twoStateHolder.item
|
||||
val state by twoState.state.collectAsState()
|
||||
val title = remember(state) { twoState.title ?: twoState.summary ?: "No title" }
|
||||
val subtitle = remember(state) {
|
||||
@@ -114,7 +117,8 @@ private fun TwoStatePreference(twoState: TwoState, checkbox: Boolean) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListPreference(list: List) {
|
||||
private fun ListPreference(listHolder: StableHolder<List>) {
|
||||
val list = listHolder.item
|
||||
val state by list.state.collectAsState()
|
||||
val title = remember(state) { list.title ?: list.summary ?: "No title" }
|
||||
val subtitle = remember(state) {
|
||||
@@ -142,7 +146,8 @@ private fun ListPreference(list: List) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MultiSelectPreference(multiSelect: MultiSelect) {
|
||||
private fun MultiSelectPreference(multiSelectHolder: StableHolder<MultiSelect>) {
|
||||
val multiSelect = multiSelectHolder.item
|
||||
val state by multiSelect.state.collectAsState()
|
||||
val title = remember(state) { multiSelect.title ?: multiSelect.summary ?: "No title" }
|
||||
val subtitle = remember(state) {
|
||||
@@ -171,7 +176,8 @@ private fun MultiSelectPreference(multiSelect: MultiSelect) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditTextPreference(editText: EditText) {
|
||||
private fun EditTextPreference(editTextHolder: StableHolder<EditText>) {
|
||||
val editText = editTextHolder.item
|
||||
val state by editText.state.collectAsState()
|
||||
val title = remember(state) { editText.title ?: editText.summary ?: "No title" }
|
||||
val subtitle = remember(state) {
|
||||
|
||||
@@ -14,10 +14,14 @@ import ca.gosyer.jui.domain.source.model.sourcepreference.SourcePreference
|
||||
import ca.gosyer.jui.domain.source.model.sourcepreference.SwitchPreference
|
||||
import ca.gosyer.jui.domain.source.model.sourcepreference.TwoStateProps
|
||||
import ca.gosyer.jui.ui.util.lang.stringFormat
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.minus
|
||||
import kotlinx.collections.immutable.plus
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlin.collections.List as KtList
|
||||
|
||||
sealed class SourceSettingsView<T, R : Any?> {
|
||||
abstract val index: Int
|
||||
@@ -98,7 +102,7 @@ sealed class SourceSettingsView<T, R : Any?> {
|
||||
|
||||
fun getOptions() = props.entryValues.mapIndexed { index, s ->
|
||||
s to props.entries[index]
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
data class MultiSelect internal constructor(
|
||||
@@ -106,12 +110,12 @@ sealed class SourceSettingsView<T, R : Any?> {
|
||||
override val title: String?,
|
||||
override val subtitle: String?,
|
||||
override val props: MultiSelectListPreference.MultiSelectListProps
|
||||
) : SourceSettingsView<MultiSelectListPreference.MultiSelectListProps, KtList<String>?>() {
|
||||
) : SourceSettingsView<MultiSelectListPreference.MultiSelectListProps, ImmutableList<String>?>() {
|
||||
private val _state = MutableStateFlow(
|
||||
props.currentValue ?: props.defaultValue
|
||||
props.currentValue?.toImmutableList() ?: props.defaultValue?.toImmutableList()
|
||||
)
|
||||
override val state: StateFlow<KtList<String>?> = _state.asStateFlow()
|
||||
override fun updateState(value: KtList<String>?) {
|
||||
override val state: StateFlow<ImmutableList<String>?> = _state.asStateFlow()
|
||||
override fun updateState(value: ImmutableList<String>?) {
|
||||
_state.value = value
|
||||
}
|
||||
internal constructor(index: Int, preference: MultiSelectListPreference) : this(
|
||||
@@ -123,13 +127,13 @@ sealed class SourceSettingsView<T, R : Any?> {
|
||||
|
||||
fun getOptions() = props.entryValues.mapIndexed { index, s ->
|
||||
s to props.entries[index]
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
fun toggleOption(key: String) {
|
||||
if (key in state.value.orEmpty()) {
|
||||
updateState(state.value.orEmpty() - key)
|
||||
updateState(state.value.orEmpty().toPersistentList() - key)
|
||||
} else {
|
||||
updateState(state.value.orEmpty() + key)
|
||||
updateState(state.value.orEmpty().toPersistentList() + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import ca.gosyer.jui.domain.updates.interactor.GetRecentUpdates
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -50,8 +52,8 @@ class UpdatesScreenViewModel @Inject constructor(
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val _updates = mutableStateMapOf<LocalDate, SnapshotStateList<ChapterDownloadItem>>()
|
||||
val updates = snapshotFlow { _updates.toList().sortedByDescending { it.first } }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptyList())
|
||||
val updates = snapshotFlow { _updates.toList().sortedByDescending { it.first }.toImmutableList() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val currentPage = MutableStateFlow(1)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -44,12 +45,13 @@ import ca.gosyer.jui.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
@Composable
|
||||
fun UpdatesScreenContent(
|
||||
isLoading: Boolean,
|
||||
dateWithUpdates: List<Pair<LocalDate, List<ChapterDownloadItem>>>,
|
||||
dateWithUpdates: ImmutableList<Pair<LocalDate, SnapshotStateList<ChapterDownloadItem>>>,
|
||||
loadNextPage: () -> Unit,
|
||||
openChapter: (Int, Long) -> Unit,
|
||||
openManga: (Long) -> Unit,
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import com.mikepenz.aboutlibraries.ui.compose.Libraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.LibraryColors
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
actual fun getLicenses(): Libs? {
|
||||
@@ -34,7 +35,7 @@ actual fun getLicenses(): Libs? {
|
||||
|
||||
@Composable
|
||||
actual fun InternalAboutLibraries(
|
||||
libraries: List<Library>,
|
||||
libraries: ImmutableList<Library>,
|
||||
modifier: Modifier,
|
||||
lazyListState: LazyListState,
|
||||
contentPadding: PaddingValues,
|
||||
|
||||
@@ -17,16 +17,17 @@ import androidx.compose.ui.window.rememberTrayState
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.presentation.build.BuildKonfig
|
||||
import ca.gosyer.jui.ui.base.LocalViewModels
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ApplicationScope.Tray(icon: Painter) {
|
||||
fun ApplicationScope.Tray(icon: StableHolder<Painter>) {
|
||||
val viewModels = LocalViewModels.current
|
||||
val vm = remember { viewModels.trayViewModel() }
|
||||
val trayState = rememberTrayState()
|
||||
Tray(
|
||||
icon,
|
||||
icon.item,
|
||||
trayState,
|
||||
tooltip = BuildKonfig.NAME,
|
||||
menu = {
|
||||
@@ -48,3 +49,4 @@ fun ApplicationScope.Tray(icon: Painter) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,9 +25,11 @@ import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowPosition
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import ca.gosyer.jui.presentation.build.BuildKonfig
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.util.lang.launchApplication
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
actual class ReaderLauncher {
|
||||
@@ -50,6 +52,7 @@ actual class ReaderLauncher {
|
||||
launchApplication {
|
||||
val scope = rememberCoroutineScope()
|
||||
val hotkeyFlow = remember { MutableSharedFlow<KeyEvent>() }
|
||||
val hotkeyFlowHolder = remember { StableHolder(hotkeyFlow.asSharedFlow()) }
|
||||
val windowState = rememberWindowState(
|
||||
position = WindowPosition.Aligned(Alignment.Center)
|
||||
)
|
||||
@@ -71,7 +74,7 @@ actual class ReaderLauncher {
|
||||
ReaderMenu(
|
||||
chapterIndex = chapterIndex,
|
||||
mangaId = mangaId,
|
||||
hotkeyFlow = hotkeyFlow,
|
||||
hotkeyFlowHolder = hotkeyFlowHolder,
|
||||
onCloseRequest = ::exitApplication
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ kotlin {
|
||||
api(libs.imageloader)
|
||||
api(libs.voyager.core)
|
||||
api(libs.dateTime)
|
||||
api(libs.immutableCollections)
|
||||
api(projects.core)
|
||||
api(projects.i18n)
|
||||
api(compose.desktop.currentOs)
|
||||
|
||||
Reference in New Issue
Block a user