Performance improvements

This commit is contained in:
Syer10
2022-08-25 15:02:53 -04:00
parent e16ec9c6e3
commit b22b7b7694
80 changed files with 729 additions and 400 deletions

View File

@@ -75,6 +75,7 @@ dependencies {
// Utility
implementation(libs.krokiCoroutines)
implementation(libs.dateTime)
implementation(libs.immutableCollections)
// Localization
implementation(libs.moko.core)

View File

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

View File

@@ -83,6 +83,7 @@ dependencies {
// Utility
implementation(libs.krokiCoroutines)
implementation(libs.dateTime)
implementation(libs.immutableCollections)
// Localization
implementation(libs.moko.core)

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
.mapIndexed { i, menuCategory ->
menuCategory.copy(order = i + 1)
}
_categories.value = categories.sortedBy { it.order }.toList()
.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
.mapIndexed { i, menuCategory ->
menuCategory.copy(order = i + 1)
}
_categories.value = categories.sortedBy { it.order }.toList()
.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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?>,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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