mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Optimize and cleanup library code (#2329)
This commit is contained in:
@@ -23,6 +23,9 @@ ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_class-signature = disabled
|
||||
ktlint_standard_comment-wrapping = disabled
|
||||
ktlint_standard_discouraged-comment-location = disabled
|
||||
ktlint_standard_function-expression-body = disabled
|
||||
ktlint_standard_function-signature = disabled
|
||||
ktlint_standard_type-argument-comment = disabled
|
||||
ktlint_standard_type-parameter-comment = disabled
|
||||
|
@@ -34,6 +34,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Added autofill support to tracker login dialog ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069))
|
||||
- Added option to hide missing chapter count ([@User826](https://github.com/User826), [@AntsyLich](https://github.com/AntsyLich)) ([#2108](https://github.com/mihonapp/mihon/pull/2108))
|
||||
- Use median to determine smart update interval, making it more resilient to long hiatuses ([@Kladki](https://github.com/Kladki)) ([#2251](https://github.com/mihonapp/mihon/pull/2251))
|
||||
- Optimize library code to potentially better handle big user libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#2329](https://github.com/mihonapp/mihon/pull/2329))
|
||||
|
||||
### Changed
|
||||
- Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861))
|
||||
|
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -15,7 +14,7 @@ internal fun LibraryComfortableGrid(
|
||||
items: List<LibraryItem>,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -35,7 +34,7 @@ internal fun LibraryComfortableGrid(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaComfortableGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -16,7 +15,7 @@ internal fun LibraryCompactGrid(
|
||||
showTitle: Boolean,
|
||||
columns: Int,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -36,7 +35,7 @@ internal fun LibraryCompactGrid(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaCompactGridItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title.takeIf { showTitle },
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds
|
||||
fun LibraryContent(
|
||||
categories: List<Category>,
|
||||
searchQuery: String?,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
contentPadding: PaddingValues,
|
||||
currentPage: () -> Int,
|
||||
hasActiveFilters: Boolean,
|
||||
showPageTabs: Boolean,
|
||||
onChangeCurrentPage: (Int) -> Unit,
|
||||
onMangaClicked: (Long) -> Unit,
|
||||
onClickManga: (Long) -> Unit,
|
||||
onContinueReadingClicked: ((LibraryManga) -> Unit)?,
|
||||
onToggleSelection: (LibraryManga) -> Unit,
|
||||
onToggleRangeSelection: (LibraryManga) -> Unit,
|
||||
onRefresh: (Category?) -> Boolean,
|
||||
onToggleSelection: (Category, LibraryManga) -> Unit,
|
||||
onToggleRangeSelection: (Category, LibraryManga) -> Unit,
|
||||
onRefresh: () -> Boolean,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
getItemCountForCategory: (Category) -> Int?,
|
||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
@@ -59,7 +59,7 @@ fun LibraryContent(
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
|
||||
|
||||
if (showPageTabs && categories.size > 1) {
|
||||
if (showPageTabs && categories.isNotEmpty()) {
|
||||
LaunchedEffect(categories) {
|
||||
if (categories.size <= pagerState.currentPage) {
|
||||
pagerState.scrollToPage(categories.size - 1)
|
||||
@@ -68,23 +68,20 @@ fun LibraryContent(
|
||||
LibraryTabs(
|
||||
categories = categories,
|
||||
pagerState = pagerState,
|
||||
getNumberOfMangaForCategory = getNumberOfMangaForCategory,
|
||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
||||
}
|
||||
|
||||
val notSelectionMode = selection.isEmpty()
|
||||
val onClickManga = { manga: LibraryManga ->
|
||||
if (notSelectionMode) {
|
||||
onMangaClicked(manga.manga.id)
|
||||
} else {
|
||||
onToggleSelection(manga)
|
||||
getItemCountForCategory = getItemCountForCategory,
|
||||
onTabItemClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PullRefresh(
|
||||
refreshing = isRefreshing,
|
||||
enabled = selection.isEmpty(),
|
||||
onRefresh = {
|
||||
val started = onRefresh(categories[currentPage()])
|
||||
val started = onRefresh()
|
||||
if (!started) return@PullRefresh
|
||||
scope.launch {
|
||||
// Fake refresh status but hide it after a second as it's a long running task
|
||||
@@ -93,19 +90,25 @@ fun LibraryContent(
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
enabled = notSelectionMode,
|
||||
) {
|
||||
LibraryPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
selectedManga = selection,
|
||||
selection = selection,
|
||||
searchQuery = searchQuery,
|
||||
onGlobalSearchClicked = onGlobalSearchClicked,
|
||||
getCategoryForPage = { page -> categories[page] },
|
||||
getDisplayMode = getDisplayMode,
|
||||
getColumnsForOrientation = getColumnsForOrientation,
|
||||
getLibraryForPage = getLibraryForPage,
|
||||
onClickManga = onClickManga,
|
||||
getItemsForCategory = getItemsForCategory,
|
||||
onClickManga = { category, manga ->
|
||||
if (selection.isNotEmpty()) {
|
||||
onToggleSelection(category, manga)
|
||||
} else {
|
||||
onClickManga(manga.manga.id)
|
||||
}
|
||||
},
|
||||
onLongClickManga = onToggleRangeSelection,
|
||||
onClickContinueReading = onContinueReadingClicked,
|
||||
)
|
||||
|
@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
@@ -18,7 +17,7 @@ import tachiyomi.presentation.core.util.plus
|
||||
internal fun LibraryList(
|
||||
items: List<LibraryItem>,
|
||||
contentPadding: PaddingValues,
|
||||
selection: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
onClick: (LibraryManga) -> Unit,
|
||||
onLongClick: (LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
@@ -45,7 +44,7 @@ internal fun LibraryList(
|
||||
) { libraryItem ->
|
||||
val manga = libraryItem.libraryManga.manga
|
||||
MangaListItem(
|
||||
isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id },
|
||||
isSelected = manga.id in selection,
|
||||
title = manga.title,
|
||||
coverData = MangaCover(
|
||||
mangaId = manga.id,
|
||||
|
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.i18n.MR
|
||||
@@ -31,14 +32,15 @@ fun LibraryPager(
|
||||
state: PagerState,
|
||||
contentPadding: PaddingValues,
|
||||
hasActiveFilters: Boolean,
|
||||
selectedManga: List<LibraryManga>,
|
||||
selection: Set<Long>,
|
||||
searchQuery: String?,
|
||||
onGlobalSearchClicked: () -> Unit,
|
||||
getCategoryForPage: (Int) -> Category,
|
||||
getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>,
|
||||
getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
|
||||
getLibraryForPage: (Int) -> List<LibraryItem>,
|
||||
onClickManga: (LibraryManga) -> Unit,
|
||||
onLongClickManga: (LibraryManga) -> Unit,
|
||||
getItemsForCategory: (Category) -> List<LibraryItem>,
|
||||
onClickManga: (Category, LibraryManga) -> Unit,
|
||||
onLongClickManga: (Category, LibraryManga) -> Unit,
|
||||
onClickContinueReading: ((LibraryManga) -> Unit)?,
|
||||
) {
|
||||
HorizontalPager(
|
||||
@@ -50,9 +52,10 @@ fun LibraryPager(
|
||||
// To make sure only one offscreen page is being composed
|
||||
return@HorizontalPager
|
||||
}
|
||||
val library = getLibraryForPage(page)
|
||||
val category = getCategoryForPage(page)
|
||||
val items = getItemsForCategory(category)
|
||||
|
||||
if (library.isEmpty()) {
|
||||
if (items.isEmpty()) {
|
||||
LibraryPagerEmptyScreen(
|
||||
searchQuery = searchQuery,
|
||||
hasActiveFilters = hasActiveFilters,
|
||||
@@ -72,12 +75,15 @@ fun LibraryPager(
|
||||
remember { mutableIntStateOf(0) }
|
||||
}
|
||||
|
||||
val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) }
|
||||
val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) }
|
||||
|
||||
when (displayMode) {
|
||||
LibraryDisplayMode.List -> {
|
||||
LibraryList(
|
||||
items = library,
|
||||
items = items,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
@@ -87,11 +93,11 @@ fun LibraryPager(
|
||||
}
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
LibraryCompactGrid(
|
||||
items = library,
|
||||
items = items,
|
||||
showTitle = displayMode is LibraryDisplayMode.CompactGrid,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
@@ -101,10 +107,10 @@ fun LibraryPager(
|
||||
}
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
LibraryComfortableGrid(
|
||||
items = library,
|
||||
items = items,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
selection = selectedManga,
|
||||
selection = selection,
|
||||
onClick = onClickManga,
|
||||
onLongClick = onLongClickManga,
|
||||
onClickContinueReading = onClickContinueReading,
|
||||
|
@@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText
|
||||
internal fun LibraryTabs(
|
||||
categories: List<Category>,
|
||||
pagerState: PagerState,
|
||||
getNumberOfMangaForCategory: (Category) -> Int?,
|
||||
getItemCountForCategory: (Category) -> Int?,
|
||||
onTabItemClick: (Int) -> Unit,
|
||||
) {
|
||||
val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex)
|
||||
Column(
|
||||
modifier = Modifier.zIndex(1f),
|
||||
) {
|
||||
Column(modifier = Modifier.zIndex(2f)) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = currentPageIndex,
|
||||
edgePadding = 0.dp,
|
||||
@@ -39,7 +37,7 @@ internal fun LibraryTabs(
|
||||
text = {
|
||||
TabText(
|
||||
text = category.visualName,
|
||||
badgeCount = getNumberOfMangaForCategory(category),
|
||||
badgeCount = getItemCountForCategory(category),
|
||||
)
|
||||
},
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
|
@@ -158,25 +158,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
val libraryManga = getLibraryManga.await()
|
||||
|
||||
val listToUpdate = if (categoryId != -1L) {
|
||||
libraryManga.filter { it.category == categoryId }
|
||||
libraryManga.filter { categoryId in it.categories }
|
||||
} else {
|
||||
val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() }
|
||||
val includedManga = if (categoriesToUpdate.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in categoriesToUpdate }
|
||||
} else {
|
||||
libraryManga
|
||||
}
|
||||
val includedCategories = libraryPreferences.updateCategories().get().map { it.toLong() }
|
||||
val excludedCategories = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }
|
||||
|
||||
val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }
|
||||
val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id }
|
||||
} else {
|
||||
emptyList()
|
||||
libraryManga.filter {
|
||||
val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty()
|
||||
val excluded = it.categories.intersect(excludedCategories).isNotEmpty()
|
||||
included && !excluded
|
||||
}
|
||||
|
||||
includedManga
|
||||
.filterNot { it.manga.id in excludedMangaIds }
|
||||
.distinctBy { it.manga.id }
|
||||
}
|
||||
|
||||
val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get()
|
||||
|
@@ -14,6 +14,8 @@ data class LibraryItem(
|
||||
val sourceLanguage: String = "",
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
) {
|
||||
val id: Long = libraryManga.id
|
||||
|
||||
/**
|
||||
* Checks if a query matches the manga
|
||||
*
|
||||
@@ -23,8 +25,7 @@ data class LibraryItem(
|
||||
fun matches(constraint: String): Boolean {
|
||||
val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() }
|
||||
if (constraint.startsWith("id:", true)) {
|
||||
val id = constraint.substringAfter("id:").toLongOrNull()
|
||||
return libraryManga.id == id
|
||||
return id == constraint.substringAfter("id:").toLongOrNull()
|
||||
}
|
||||
return libraryManga.manga.title.contains(constraint, true) ||
|
||||
(libraryManga.manga.author?.contains(constraint, true) ?: false) ||
|
||||
|
@@ -4,16 +4,13 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastDistinctBy
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.compose.ui.util.fastMapNotNull
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
import eu.kanade.core.util.fastPartition
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
@@ -29,28 +26,25 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.getNextUnread
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.core.common.utils.mutate
|
||||
import tachiyomi.core.common.preference.CheckboxState
|
||||
import tachiyomi.core.common.preference.TriState
|
||||
import tachiyomi.core.common.util.lang.compareToWithCollator
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
@@ -74,11 +68,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Typealias for the library manga, using the category as keys, and list of manga as values.
|
||||
*/
|
||||
typealias LibraryMap = Map<Category, List<LibraryItem>>
|
||||
|
||||
class LibraryScreenModel(
|
||||
private val getLibraryManga: GetLibraryManga = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
@@ -98,32 +87,52 @@ class LibraryScreenModel(
|
||||
) : StateScreenModel<LibraryScreenModel.State>(State()) {
|
||||
|
||||
var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope)
|
||||
val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex]
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
combine(
|
||||
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
|
||||
getLibraryFlow(),
|
||||
getTracksPerManga.subscribe(),
|
||||
getTrackingFilterFlow(),
|
||||
downloadCache.changes,
|
||||
) { searchQuery, library, tracks, trackingFilter, _ ->
|
||||
library
|
||||
.applyFilters(tracks, trackingFilter)
|
||||
.applySort(tracks, trackingFilter.keys)
|
||||
.mapValues { (_, value) ->
|
||||
if (searchQuery != null) {
|
||||
value.filter { it.matches(searchQuery) }
|
||||
} else {
|
||||
value
|
||||
getCategories.subscribe(),
|
||||
getFavoritesFlow(),
|
||||
combine(getTracksPerManga.subscribe(), getTrackingFiltersFlow(), ::Pair),
|
||||
getLibraryItemPreferencesFlow(),
|
||||
) { searchQuery, categories, favorites, (tracksMap, trackingFilters), itemPreferences ->
|
||||
val filteredFavorites = favorites
|
||||
.applyFilters(tracksMap, trackingFilters, itemPreferences)
|
||||
.let { if (searchQuery == null) it else it.filter { m -> m.matches(searchQuery) } }
|
||||
|
||||
LibraryData(
|
||||
isInitialized = true,
|
||||
categories = categories,
|
||||
favorites = filteredFavorites,
|
||||
tracksMap = tracksMap,
|
||||
loggedInTrackerIds = trackingFilters.keys,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { libraryData ->
|
||||
mutableState.update { state ->
|
||||
state.copy(libraryData = libraryData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenModelScope.launchIO {
|
||||
state
|
||||
.dropWhile { !it.libraryData.isInitialized }
|
||||
.map { it.libraryData }
|
||||
.distinctUntilChanged()
|
||||
.map { data ->
|
||||
data.favorites
|
||||
.applyGrouping(data.categories)
|
||||
.applySort(data.favoritesById, data.tracksMap, data.loggedInTrackerIds)
|
||||
}
|
||||
.collectLatest {
|
||||
mutableState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
library = it,
|
||||
groupedFavorites = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -147,9 +156,8 @@ class LibraryScreenModel(
|
||||
|
||||
combine(
|
||||
getLibraryItemPreferencesFlow(),
|
||||
getTrackingFilterFlow(),
|
||||
) { prefs, trackFilter ->
|
||||
(
|
||||
getTrackingFiltersFlow(),
|
||||
) { prefs, trackFilters ->
|
||||
listOf(
|
||||
prefs.filterDownloaded,
|
||||
prefs.filterUnread,
|
||||
@@ -157,8 +165,9 @@ class LibraryScreenModel(
|
||||
prefs.filterBookmarked,
|
||||
prefs.filterCompleted,
|
||||
prefs.filterIntervalCustom,
|
||||
) + trackFilter.values
|
||||
).any { it != TriState.DISABLED }
|
||||
*trackFilters.values.toTypedArray(),
|
||||
)
|
||||
.any { it != TriState.DISABLED }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
@@ -169,19 +178,19 @@ class LibraryScreenModel(
|
||||
.launchIn(screenModelScope)
|
||||
}
|
||||
|
||||
private suspend fun LibraryMap.applyFilters(
|
||||
private fun List<LibraryItem>.applyFilters(
|
||||
trackMap: Map<Long, List<Track>>,
|
||||
trackingFilter: Map<Long, TriState>,
|
||||
): LibraryMap {
|
||||
val prefs = getLibraryItemPreferencesFlow().first()
|
||||
val downloadedOnly = prefs.globalFilterDownloaded
|
||||
val skipOutsideReleasePeriod = prefs.skipOutsideReleasePeriod
|
||||
val filterDownloaded = if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
|
||||
val filterUnread = prefs.filterUnread
|
||||
val filterStarted = prefs.filterStarted
|
||||
val filterBookmarked = prefs.filterBookmarked
|
||||
val filterCompleted = prefs.filterCompleted
|
||||
val filterIntervalCustom = prefs.filterIntervalCustom
|
||||
preferences: ItemPreferences,
|
||||
): List<LibraryItem> {
|
||||
val downloadedOnly = preferences.globalFilterDownloaded
|
||||
val skipOutsideReleasePeriod = preferences.skipOutsideReleasePeriod
|
||||
val filterDownloaded = if (downloadedOnly) TriState.ENABLED_IS else preferences.filterDownloaded
|
||||
val filterUnread = preferences.filterUnread
|
||||
val filterStarted = preferences.filterStarted
|
||||
val filterBookmarked = preferences.filterBookmarked
|
||||
val filterCompleted = preferences.filterCompleted
|
||||
val filterIntervalCustom = preferences.filterIntervalCustom
|
||||
|
||||
val isNotLoggedInAnyTrack = trackingFilter.isEmpty()
|
||||
|
||||
@@ -225,7 +234,7 @@ class LibraryScreenModel(
|
||||
if (isNotLoggedInAnyTrack || trackFiltersIsIgnored) return@tracking true
|
||||
|
||||
val mangaTracks = trackMap
|
||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.libraryManga.id]
|
||||
.mapValues { entry -> entry.value.map { it.trackerId } }[item.id]
|
||||
.orEmpty()
|
||||
|
||||
val isExcluded = excludedTracks.isNotEmpty() && mangaTracks.fastAny { it in excludedTracks }
|
||||
@@ -234,7 +243,7 @@ class LibraryScreenModel(
|
||||
!isExcluded && isIncluded
|
||||
}
|
||||
|
||||
val filterFn: (LibraryItem) -> Boolean = {
|
||||
return fastFilter {
|
||||
filterFnDownloaded(it) &&
|
||||
filterFnUnread(it) &&
|
||||
filterFnStarted(it) &&
|
||||
@@ -243,13 +252,31 @@ class LibraryScreenModel(
|
||||
filterFnIntervalCustom(it) &&
|
||||
filterFnTracking(it)
|
||||
}
|
||||
|
||||
return mapValues { (_, value) -> value.fastFilter(filterFn) }
|
||||
}
|
||||
|
||||
private fun LibraryMap.applySort(trackMap: Map<Long, List<Track>>, loggedInTrackerIds: Set<Long>): LibraryMap {
|
||||
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase())
|
||||
private fun List<LibraryItem>.applyGrouping(
|
||||
categories: List<Category>,
|
||||
): Map<Category, List</* LibraryItem */ Long>> {
|
||||
val groupCache = mutableMapOf</* Category */ Long, MutableList</* LibraryItem */ Long>>()
|
||||
forEach { item ->
|
||||
item.libraryManga.categories.forEach { categoryId ->
|
||||
groupCache.getOrPut(categoryId) { mutableListOf() }.add(item.id)
|
||||
}
|
||||
}
|
||||
val showSystemCategory = groupCache.containsKey(0L)
|
||||
return categories.filter { showSystemCategory || !it.isSystemCategory }
|
||||
.associateWith { groupCache[it.id]?.toList().orEmpty() }
|
||||
}
|
||||
|
||||
private fun Map<Category, List</* LibraryItem */ Long>>.applySort(
|
||||
favoritesById: Map<Long, LibraryItem>,
|
||||
trackMap: Map<Long, List<Track>>,
|
||||
loggedInTrackerIds: Set<Long>,
|
||||
): Map<Category, List</* LibraryItem */ Long>> {
|
||||
val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { manga1, manga2 ->
|
||||
val title1 = manga1.libraryManga.manga.title.lowercase()
|
||||
val title2 = manga2.libraryManga.manga.title.lowercase()
|
||||
title1.compareToWithCollator(title2)
|
||||
}
|
||||
|
||||
val defaultTrackerScoreSortValue = -1.0
|
||||
@@ -266,39 +293,39 @@ class LibraryScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun LibrarySort.comparator(): Comparator<LibraryItem> = Comparator { i1, i2 ->
|
||||
fun LibrarySort.comparator(): Comparator<LibraryItem> = Comparator { manga1, manga2 ->
|
||||
when (this.type) {
|
||||
LibrarySort.Type.Alphabetical -> {
|
||||
sortAlphabetically(i1, i2)
|
||||
sortAlphabetically(manga1, manga2)
|
||||
}
|
||||
LibrarySort.Type.LastRead -> {
|
||||
i1.libraryManga.lastRead.compareTo(i2.libraryManga.lastRead)
|
||||
manga1.libraryManga.lastRead.compareTo(manga2.libraryManga.lastRead)
|
||||
}
|
||||
LibrarySort.Type.LastUpdate -> {
|
||||
i1.libraryManga.manga.lastUpdate.compareTo(i2.libraryManga.manga.lastUpdate)
|
||||
manga1.libraryManga.manga.lastUpdate.compareTo(manga2.libraryManga.manga.lastUpdate)
|
||||
}
|
||||
LibrarySort.Type.UnreadCount -> when {
|
||||
// Ensure unread content comes first
|
||||
i1.libraryManga.unreadCount == i2.libraryManga.unreadCount -> 0
|
||||
i1.libraryManga.unreadCount == 0L -> if (this.isAscending) 1 else -1
|
||||
i2.libraryManga.unreadCount == 0L -> if (this.isAscending) -1 else 1
|
||||
else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount)
|
||||
manga1.libraryManga.unreadCount == manga2.libraryManga.unreadCount -> 0
|
||||
manga1.libraryManga.unreadCount == 0L -> if (this.isAscending) 1 else -1
|
||||
manga2.libraryManga.unreadCount == 0L -> if (this.isAscending) -1 else 1
|
||||
else -> manga1.libraryManga.unreadCount.compareTo(manga2.libraryManga.unreadCount)
|
||||
}
|
||||
LibrarySort.Type.TotalChapters -> {
|
||||
i1.libraryManga.totalChapters.compareTo(i2.libraryManga.totalChapters)
|
||||
manga1.libraryManga.totalChapters.compareTo(manga2.libraryManga.totalChapters)
|
||||
}
|
||||
LibrarySort.Type.LatestChapter -> {
|
||||
i1.libraryManga.latestUpload.compareTo(i2.libraryManga.latestUpload)
|
||||
manga1.libraryManga.latestUpload.compareTo(manga2.libraryManga.latestUpload)
|
||||
}
|
||||
LibrarySort.Type.ChapterFetchDate -> {
|
||||
i1.libraryManga.chapterFetchedAt.compareTo(i2.libraryManga.chapterFetchedAt)
|
||||
manga1.libraryManga.chapterFetchedAt.compareTo(manga2.libraryManga.chapterFetchedAt)
|
||||
}
|
||||
LibrarySort.Type.DateAdded -> {
|
||||
i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded)
|
||||
manga1.libraryManga.manga.dateAdded.compareTo(manga2.libraryManga.manga.dateAdded)
|
||||
}
|
||||
LibrarySort.Type.TrackerMean -> {
|
||||
val item1Score = trackerScores[i1.libraryManga.id] ?: defaultTrackerScoreSortValue
|
||||
val item2Score = trackerScores[i2.libraryManga.id] ?: defaultTrackerScoreSortValue
|
||||
val item1Score = trackerScores[manga1.id] ?: defaultTrackerScoreSortValue
|
||||
val item2Score = trackerScores[manga2.id] ?: defaultTrackerScoreSortValue
|
||||
item1Score.compareTo(item2Score)
|
||||
}
|
||||
LibrarySort.Type.Random -> {
|
||||
@@ -312,11 +339,13 @@ class LibraryScreenModel(
|
||||
return@mapValues value.shuffled(Random(libraryPreferences.randomSortSeed().get()))
|
||||
}
|
||||
|
||||
val manga = value.mapNotNull { favoritesById[it] }
|
||||
|
||||
val comparator = key.sort.comparator()
|
||||
.let { if (key.sort.isAscending) it else it.reversed() }
|
||||
.thenComparator(sortAlphabetically)
|
||||
|
||||
value.sortedWith(comparator)
|
||||
manga.sortedWith(comparator).map { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,45 +382,37 @@ class LibraryScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories and all its manga from the database.
|
||||
*/
|
||||
private fun getLibraryFlow(): Flow<LibraryMap> {
|
||||
val libraryMangasFlow = combine(
|
||||
private fun getFavoritesFlow(): Flow<List<LibraryItem>> {
|
||||
return combine(
|
||||
getLibraryManga.subscribe(),
|
||||
getLibraryItemPreferencesFlow(),
|
||||
downloadCache.changes,
|
||||
) { libraryMangaList, prefs, _ ->
|
||||
libraryMangaList
|
||||
.map { libraryManga ->
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
) { libraryManga, preferences, _ ->
|
||||
libraryManga.map { manga ->
|
||||
LibraryItem(
|
||||
libraryManga,
|
||||
downloadCount = if (prefs.downloadBadge) {
|
||||
downloadManager.getDownloadCount(libraryManga.manga).toLong()
|
||||
libraryManga = manga,
|
||||
downloadCount = if (preferences.downloadBadge) {
|
||||
downloadManager.getDownloadCount(manga.manga).toLong()
|
||||
} else {
|
||||
0
|
||||
},
|
||||
unreadCount = if (prefs.unreadBadge) libraryManga.unreadCount else 0,
|
||||
isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false,
|
||||
sourceLanguage = if (prefs.languageBadge) {
|
||||
sourceManager.getOrStub(libraryManga.manga.source).lang
|
||||
unreadCount = if (preferences.unreadBadge) {
|
||||
manga.unreadCount
|
||||
} else {
|
||||
0
|
||||
},
|
||||
isLocal = if (preferences.localBadge) {
|
||||
manga.manga.isLocal()
|
||||
} else {
|
||||
false
|
||||
},
|
||||
sourceLanguage = if (preferences.languageBadge) {
|
||||
sourceManager.getOrStub(manga.manga.source).lang
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
.groupBy { it.libraryManga.category }
|
||||
}
|
||||
|
||||
return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga ->
|
||||
val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) {
|
||||
categories.fastFilterNot { it.isSystemCategory }
|
||||
} else {
|
||||
categories
|
||||
}
|
||||
|
||||
displayCategories.associateWith { libraryManga[it.id].orEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,17 +421,15 @@ class LibraryScreenModel(
|
||||
*
|
||||
* @return map of track id with the filter value
|
||||
*/
|
||||
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
|
||||
private fun getTrackingFiltersFlow(): Flow<Map<Long, TriState>> {
|
||||
return trackerManager.loggedInTrackersFlow().flatMapLatest { loggedInTrackers ->
|
||||
if (loggedInTrackers.isEmpty()) return@flatMapLatest flowOf(emptyMap())
|
||||
|
||||
val prefFlows = loggedInTrackers.map { tracker ->
|
||||
libraryPreferences.filterTracking(tracker.id.toInt()).changes()
|
||||
if (loggedInTrackers.isEmpty()) {
|
||||
flowOf(emptyMap())
|
||||
} else {
|
||||
val filterFlows = loggedInTrackers.map { tracker ->
|
||||
libraryPreferences.filterTracking(tracker.id.toInt()).changes().map { tracker.id to it }
|
||||
}
|
||||
combine(prefFlows) {
|
||||
loggedInTrackers
|
||||
.mapIndexed { index, tracker -> tracker.id to it[index] }
|
||||
.toMap()
|
||||
combine(filterFlows) { it.toMap() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,26 +462,19 @@ class LibraryScreenModel(
|
||||
return mangaCategories.flatten().distinct().subtract(common)
|
||||
}
|
||||
|
||||
fun runDownloadActionSelection(action: DownloadAction) {
|
||||
val selection = state.value.selection
|
||||
val mangas = selection.map { it.manga }.toList()
|
||||
when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> downloadUnreadChapters(mangas, 1)
|
||||
DownloadAction.NEXT_5_CHAPTERS -> downloadUnreadChapters(mangas, 5)
|
||||
DownloadAction.NEXT_10_CHAPTERS -> downloadUnreadChapters(mangas, 10)
|
||||
DownloadAction.NEXT_25_CHAPTERS -> downloadUnreadChapters(mangas, 25)
|
||||
DownloadAction.UNREAD_CHAPTERS -> downloadUnreadChapters(mangas, null)
|
||||
/**
|
||||
* Queues the amount specified of unread chapters from the list of selected manga
|
||||
*/
|
||||
fun performDownloadAction(action: DownloadAction) {
|
||||
val mangas = state.value.selectedManga
|
||||
val amount = when (action) {
|
||||
DownloadAction.NEXT_1_CHAPTER -> 1
|
||||
DownloadAction.NEXT_5_CHAPTERS -> 5
|
||||
DownloadAction.NEXT_10_CHAPTERS -> 10
|
||||
DownloadAction.NEXT_25_CHAPTERS -> 25
|
||||
DownloadAction.UNREAD_CHAPTERS -> null
|
||||
}
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the amount specified of unread chapters from the list of mangas given.
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
* @param amount the amount to queue or null to queue all
|
||||
*/
|
||||
private fun downloadUnreadChapters(mangas: List<Manga>, amount: Int?) {
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
val chapters = getNextChapters.await(manga.id)
|
||||
@@ -486,11 +498,10 @@ class LibraryScreenModel(
|
||||
* Marks mangas' chapters read status.
|
||||
*/
|
||||
fun markReadSelection(read: Boolean) {
|
||||
val mangas = state.value.selection.toList()
|
||||
screenModelScope.launchNonCancellable {
|
||||
mangas.forEach { manga ->
|
||||
state.value.selectedManga.forEach { manga ->
|
||||
setReadStatus.await(
|
||||
manga = manga.manga,
|
||||
manga = manga,
|
||||
read = read,
|
||||
)
|
||||
}
|
||||
@@ -501,16 +512,14 @@ class LibraryScreenModel(
|
||||
/**
|
||||
* Remove the selected manga.
|
||||
*
|
||||
* @param mangaList the list of manga to delete.
|
||||
* @param mangas the list of manga to delete.
|
||||
* @param deleteFromLibrary whether to delete manga from library.
|
||||
* @param deleteChapters whether to delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
|
||||
fun removeMangas(mangas: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
|
||||
screenModelScope.launchNonCancellable {
|
||||
val mangaToDelete = mangaList.distinctBy { it.id }
|
||||
|
||||
if (deleteFromLibrary) {
|
||||
val toDelete = mangaToDelete.map {
|
||||
val toDelete = mangas.map {
|
||||
it.removeCovers(coverCache)
|
||||
MangaUpdate(
|
||||
favorite = false,
|
||||
@@ -521,7 +530,7 @@ class LibraryScreenModel(
|
||||
}
|
||||
|
||||
if (deleteChapters) {
|
||||
mangaToDelete.forEach { manga ->
|
||||
mangas.forEach { manga ->
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
@@ -556,38 +565,32 @@ class LibraryScreenModel(
|
||||
return libraryPreferences.displayMode().asState(screenModelScope)
|
||||
}
|
||||
|
||||
fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
fun getColumnsForOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
|
||||
return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns())
|
||||
.asState(screenModelScope)
|
||||
}
|
||||
|
||||
suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
|
||||
if (state.value.categories.isEmpty()) return null
|
||||
|
||||
return withIOContext {
|
||||
state.value
|
||||
.getLibraryItemsByCategoryId(state.value.categories[activeCategoryIndex].id)
|
||||
?.randomOrNull()
|
||||
}
|
||||
fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
|
||||
return state.value.getItemsForCategoryId(activeCategory.id).randomOrNull()
|
||||
}
|
||||
|
||||
fun showSettingsDialog() {
|
||||
mutableState.update { it.copy(dialog = Dialog.SettingsSheet) }
|
||||
}
|
||||
|
||||
private var lastSelectionCategory: Long? = null
|
||||
|
||||
fun clearSelection() {
|
||||
mutableState.update { it.copy(selection = persistentListOf()) }
|
||||
lastSelectionCategory = null
|
||||
mutableState.update { it.copy(selection = setOf()) }
|
||||
}
|
||||
|
||||
fun toggleSelection(manga: LibraryManga) {
|
||||
fun toggleSelection(category: Category, manga: LibraryManga) {
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.mutate { list ->
|
||||
if (list.fastAny { it.id == manga.id }) {
|
||||
list.removeAll { it.id == manga.id }
|
||||
} else {
|
||||
list.add(manga)
|
||||
}
|
||||
val newSelection = state.selection.mutate { set ->
|
||||
if (!set.remove(manga.id)) set.add(manga.id)
|
||||
}
|
||||
lastSelectionCategory = category.id.takeIf { newSelection.isNotEmpty() }
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
@@ -596,60 +599,49 @@ class LibraryScreenModel(
|
||||
* Selects all mangas between and including the given manga and the last pressed manga from the
|
||||
* same category as the given manga
|
||||
*/
|
||||
fun toggleRangeSelection(manga: LibraryManga) {
|
||||
fun toggleRangeSelection(category: Category, manga: LibraryManga) {
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.mutate { list ->
|
||||
val lastSelected = list.lastOrNull()
|
||||
if (lastSelected?.category != manga.category) {
|
||||
list.add(manga)
|
||||
if (lastSelectionCategory != category.id) {
|
||||
list.add(manga.id)
|
||||
return@mutate
|
||||
}
|
||||
|
||||
val items = state.getLibraryItemsByCategoryId(manga.category)
|
||||
?.fastMap { it.libraryManga }.orEmpty()
|
||||
val items = state.getItemsForCategoryId(category.id).fastMap { it.id }
|
||||
val lastMangaIndex = items.indexOf(lastSelected)
|
||||
val curMangaIndex = items.indexOf(manga)
|
||||
val curMangaIndex = items.indexOf(manga.id)
|
||||
|
||||
val selectedIds = list.fastMap { it.id }
|
||||
val selectionRange = when {
|
||||
lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex)
|
||||
curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex)
|
||||
lastMangaIndex < curMangaIndex -> lastMangaIndex..curMangaIndex
|
||||
curMangaIndex < lastMangaIndex -> curMangaIndex..lastMangaIndex
|
||||
// We shouldn't reach this point
|
||||
else -> return@mutate
|
||||
}
|
||||
val newSelections = selectionRange.mapNotNull { index ->
|
||||
items[index].takeUnless { it.id in selectedIds }
|
||||
selectionRange.mapNotNull { items[it] }.let(list::addAll)
|
||||
}
|
||||
list.addAll(newSelections)
|
||||
lastSelectionCategory = category.id
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
lastSelectionCategory = null
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.mutate { list ->
|
||||
state.getItemsForCategoryId(activeCategory.id).map { it.id }.let(list::addAll)
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll(index: Int) {
|
||||
fun invertSelection() {
|
||||
lastSelectionCategory = null
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.mutate { list ->
|
||||
val categoryId = state.categories.getOrNull(index)?.id ?: -1
|
||||
val selectedIds = list.fastMap { it.id }
|
||||
state.getLibraryItemsByCategoryId(categoryId)
|
||||
?.fastMapNotNull { item ->
|
||||
item.libraryManga.takeUnless { it.id in selectedIds }
|
||||
}
|
||||
?.let { list.addAll(it) }
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection(index: Int) {
|
||||
mutableState.update { state ->
|
||||
val newSelection = state.selection.mutate { list ->
|
||||
val categoryId = state.categories[index].id
|
||||
val items = state.getLibraryItemsByCategoryId(categoryId)?.fastMap { it.libraryManga }.orEmpty()
|
||||
val selectedIds = list.fastMap { it.id }
|
||||
val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds }
|
||||
val toRemoveIds = toRemove.fastMap { it.id }
|
||||
list.removeAll { it.id in toRemoveIds }
|
||||
val itemIds = state.getItemsForCategoryId(activeCategory.id).fastMap { it.id }
|
||||
val (toRemove, toAdd) = itemIds.partition { it in list }
|
||||
list.removeAll(toRemove)
|
||||
list.addAll(toAdd)
|
||||
}
|
||||
state.copy(selection = newSelection)
|
||||
@@ -663,10 +655,10 @@ class LibraryScreenModel(
|
||||
fun openChangeCategoryDialog() {
|
||||
screenModelScope.launchIO {
|
||||
// Create a copy of selected manga
|
||||
val mangaList = state.value.selection.map { it.manga }
|
||||
val mangaList = state.value.selectedManga
|
||||
|
||||
// Hide the default category because it has a different behavior than the ones from db.
|
||||
val categories = state.value.categories.filter { it.id != 0L }
|
||||
val categories = state.value.displayedCategories.filter { it.id != 0L }
|
||||
|
||||
// Get indexes of the common categories to preselect.
|
||||
val common = getCommonCategories(mangaList)
|
||||
@@ -686,8 +678,7 @@ class LibraryScreenModel(
|
||||
}
|
||||
|
||||
fun openDeleteMangaDialog() {
|
||||
val mangaList = state.value.selection.map { it.manga }
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteManga(mangaList)) }
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteManga(state.value.selectedManga)) }
|
||||
}
|
||||
|
||||
fun closeDialog() {
|
||||
@@ -720,41 +711,50 @@ class LibraryScreenModel(
|
||||
val filterIntervalCustom: TriState,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class LibraryData(
|
||||
val isInitialized: Boolean = false,
|
||||
val categories: List<Category> = emptyList(),
|
||||
val favorites: List<LibraryItem> = emptyList(),
|
||||
val tracksMap: Map</* Manga */ Long, List<Track>> = emptyMap(),
|
||||
val loggedInTrackerIds: Set<Long> = emptySet(),
|
||||
) {
|
||||
val favoritesById by lazy { favorites.associateBy { it.id } }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val isInitialized: Boolean = false,
|
||||
val isLoading: Boolean = true,
|
||||
val library: LibraryMap = emptyMap(),
|
||||
val searchQuery: String? = null,
|
||||
val selection: PersistentList<LibraryManga> = persistentListOf(),
|
||||
val selection: Set</* Manga */ Long> = setOf(),
|
||||
val hasActiveFilters: Boolean = false,
|
||||
val showCategoryTabs: Boolean = false,
|
||||
val showMangaCount: Boolean = false,
|
||||
val showMangaContinueButton: Boolean = false,
|
||||
val dialog: Dialog? = null,
|
||||
val libraryData: LibraryData = LibraryData(),
|
||||
private val groupedFavorites: Map<Category, List</* LibraryItem */ Long>> = emptyMap(),
|
||||
) {
|
||||
private val libraryCount by lazy {
|
||||
library.values
|
||||
.flatten()
|
||||
.fastDistinctBy { it.libraryManga.manga.id }
|
||||
.size
|
||||
}
|
||||
|
||||
val isLibraryEmpty by lazy { libraryCount == 0 }
|
||||
val isLibraryEmpty = libraryData.favorites.isEmpty()
|
||||
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
|
||||
val categories = library.keys.toList()
|
||||
val selectedManga by lazy { selection.mapNotNull { libraryData.favoritesById[it]?.libraryManga?.manga } }
|
||||
|
||||
fun getLibraryItemsByCategoryId(categoryId: Long): List<LibraryItem>? {
|
||||
return library.firstNotNullOfOrNull { (k, v) -> v.takeIf { k.id == categoryId } }
|
||||
val displayedCategories = groupedFavorites.keys.toList()
|
||||
|
||||
fun getItemsForCategoryId(categoryId: Long): List<LibraryItem> {
|
||||
val category = displayedCategories.find { it.id == categoryId } ?: return emptyList()
|
||||
return getItemsForCategory(category)
|
||||
}
|
||||
|
||||
fun getLibraryItemsByPage(page: Int): List<LibraryItem> {
|
||||
return library.values.toTypedArray().getOrNull(page).orEmpty()
|
||||
fun getItemsForCategory(category: Category): List<LibraryItem> {
|
||||
return groupedFavorites[category].orEmpty().mapNotNull { libraryData.favoritesById[it] }
|
||||
}
|
||||
|
||||
fun getMangaCountForCategory(category: Category): Int? {
|
||||
return if (showMangaCount || !searchQuery.isNullOrEmpty()) library[category]?.size else null
|
||||
fun getItemCountForCategory(category: Category): Int? {
|
||||
return if (showMangaCount || !searchQuery.isNullOrEmpty()) groupedFavorites[category]?.size else null
|
||||
}
|
||||
|
||||
fun getToolbarTitle(
|
||||
@@ -762,18 +762,17 @@ class LibraryScreenModel(
|
||||
defaultCategoryTitle: String,
|
||||
page: Int,
|
||||
): LibraryToolbarTitle {
|
||||
val category = categories.getOrNull(page) ?: return LibraryToolbarTitle(defaultTitle)
|
||||
val category = displayedCategories.getOrNull(page) ?: return LibraryToolbarTitle(defaultTitle)
|
||||
val categoryName = category.let {
|
||||
if (it.isSystemCategory) defaultCategoryTitle else it.name
|
||||
}
|
||||
val title = if (showCategoryTabs) defaultTitle else categoryName
|
||||
val count = when {
|
||||
!showMangaCount -> null
|
||||
!showCategoryTabs -> getMangaCountForCategory(category)
|
||||
!showCategoryTabs -> getItemCountForCategory(category)
|
||||
// Whole library count
|
||||
else -> libraryCount
|
||||
else -> libraryData.favorites.size
|
||||
}
|
||||
|
||||
return LibraryToolbarTitle(title, count)
|
||||
}
|
||||
}
|
||||
|
@@ -112,16 +112,15 @@ data object LibraryTab : Tab {
|
||||
defaultCategoryTitle = stringResource(MR.strings.label_default),
|
||||
page = screenModel.activeCategoryIndex,
|
||||
)
|
||||
val tabVisible = state.showCategoryTabs && state.categories.size > 1
|
||||
LibraryToolbar(
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
selectedCount = state.selection.size,
|
||||
title = title,
|
||||
onClickUnselectAll = screenModel::clearSelection,
|
||||
onClickSelectAll = { screenModel.selectAll(screenModel.activeCategoryIndex) },
|
||||
onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategoryIndex) },
|
||||
onClickSelectAll = screenModel::selectAll,
|
||||
onClickInvertSelection = screenModel::invertSelection,
|
||||
onClickFilter = screenModel::showSettingsDialog,
|
||||
onClickRefresh = { onClickRefresh(state.categories[screenModel.activeCategoryIndex]) },
|
||||
onClickRefresh = { onClickRefresh(screenModel.activeCategory) },
|
||||
onClickGlobalUpdate = { onClickRefresh(null) },
|
||||
onClickOpenRandomManga = {
|
||||
scope.launch {
|
||||
@@ -137,7 +136,8 @@ data object LibraryTab : Tab {
|
||||
},
|
||||
searchQuery = state.searchQuery,
|
||||
onSearchQueryChange = screenModel::search,
|
||||
scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
|
||||
// For scroll overlay when no tab
|
||||
scrollBehavior = scrollBehavior.takeIf { !state.showCategoryTabs },
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -146,15 +146,17 @@ data object LibraryTab : Tab {
|
||||
onChangeCategoryClicked = screenModel::openChangeCategoryDialog,
|
||||
onMarkAsReadClicked = { screenModel.markReadSelection(true) },
|
||||
onMarkAsUnreadClicked = { screenModel.markReadSelection(false) },
|
||||
onDownloadClicked = screenModel::runDownloadActionSelection
|
||||
.takeIf { state.selection.fastAll { !it.manga.isLocal() } },
|
||||
onDownloadClicked = screenModel::performDownloadAction
|
||||
.takeIf { state.selectedManga.fastAll { !it.isLocal() } },
|
||||
onDeleteClicked = screenModel::openDeleteMangaDialog,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
) { contentPadding ->
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||
state.isLoading -> {
|
||||
LoadingScreen(Modifier.padding(contentPadding))
|
||||
}
|
||||
state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> {
|
||||
val handler = LocalUriHandler.current
|
||||
EmptyScreen(
|
||||
@@ -171,7 +173,7 @@ data object LibraryTab : Tab {
|
||||
}
|
||||
else -> {
|
||||
LibraryContent(
|
||||
categories = state.categories,
|
||||
categories = state.displayedCategories,
|
||||
searchQuery = state.searchQuery,
|
||||
selection = state.selection,
|
||||
contentPadding = contentPadding,
|
||||
@@ -179,7 +181,7 @@ data object LibraryTab : Tab {
|
||||
hasActiveFilters = state.hasActiveFilters,
|
||||
showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(),
|
||||
onChangeCurrentPage = { screenModel.activeCategoryIndex = it },
|
||||
onMangaClicked = { navigator.push(MangaScreen(it)) },
|
||||
onClickManga = { navigator.push(MangaScreen(it)) },
|
||||
onContinueReadingClicked = { it: LibraryManga ->
|
||||
scope.launchIO {
|
||||
val chapter = screenModel.getNextUnreadChapter(it.manga)
|
||||
@@ -194,18 +196,19 @@ data object LibraryTab : Tab {
|
||||
Unit
|
||||
}.takeIf { state.showMangaContinueButton },
|
||||
onToggleSelection = screenModel::toggleSelection,
|
||||
onToggleRangeSelection = {
|
||||
screenModel.toggleRangeSelection(it)
|
||||
onToggleRangeSelection = { category, manga ->
|
||||
screenModel.toggleRangeSelection(category, manga)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
onRefresh = onClickRefresh,
|
||||
onRefresh = { onClickRefresh(screenModel.activeCategory) },
|
||||
onGlobalSearchClicked = {
|
||||
navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: ""))
|
||||
},
|
||||
getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
|
||||
getItemCountForCategory = { state.getItemCountForCategory(it) },
|
||||
getDisplayMode = { screenModel.getDisplayMode() },
|
||||
getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) },
|
||||
) { state.getLibraryItemsByPage(it) }
|
||||
getColumnsForOrientation = { screenModel.getColumnsForOrientation(it) },
|
||||
getItemsForCategory = { state.getItemsForCategory(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,15 +216,10 @@ data object LibraryTab : Tab {
|
||||
val onDismissRequest = screenModel::closeDialog
|
||||
when (val dialog = state.dialog) {
|
||||
is LibraryScreenModel.Dialog.SettingsSheet -> run {
|
||||
val category = state.categories.getOrNull(screenModel.activeCategoryIndex)
|
||||
if (category == null) {
|
||||
onDismissRequest()
|
||||
return@run
|
||||
}
|
||||
LibrarySettingsDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
screenModel = settingsScreenModel,
|
||||
category = category,
|
||||
category = screenModel.activeCategory,
|
||||
)
|
||||
}
|
||||
is LibraryScreenModel.Dialog.ChangeCategory -> {
|
||||
|
@@ -2,11 +2,9 @@ package eu.kanade.tachiyomi.ui.stats
|
||||
|
||||
import androidx.compose.ui.util.fastDistinctBy
|
||||
import androidx.compose.ui.util.fastFilter
|
||||
import androidx.compose.ui.util.fastMapNotNull
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.util.fastCountNot
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
import eu.kanade.presentation.more.stats.StatsScreenState
|
||||
import eu.kanade.presentation.more.stats.data.StatsData
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
@@ -88,25 +86,14 @@ class StatsScreenModel(
|
||||
|
||||
private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int {
|
||||
val includedCategories = preferences.updateCategories().get().map { it.toLong() }
|
||||
val includedManga = if (includedCategories.isNotEmpty()) {
|
||||
libraryManga.filter { it.category in includedCategories }
|
||||
} else {
|
||||
libraryManga
|
||||
}
|
||||
|
||||
val excludedCategories = preferences.updateCategoriesExclude().get().map { it.toLong() }
|
||||
val excludedMangaIds = if (excludedCategories.isNotEmpty()) {
|
||||
libraryManga.fastMapNotNull { manga ->
|
||||
manga.id.takeIf { manga.category in excludedCategories }
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val updateRestrictions = preferences.autoUpdateMangaRestrictions().get()
|
||||
return includedManga
|
||||
.fastFilterNot { it.manga.id in excludedMangaIds }
|
||||
.fastDistinctBy { it.manga.id }
|
||||
|
||||
return libraryManga.filter {
|
||||
val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty()
|
||||
val excluded = it.categories.intersect(excludedCategories).isNotEmpty()
|
||||
included && !excluded
|
||||
}
|
||||
.fastCountNot {
|
||||
(MANGA_NON_COMPLETED in updateRestrictions && it.manga.status.toInt() == SManga.COMPLETED) ||
|
||||
(MANGA_HAS_UNREAD in updateRestrictions && it.unreadCount != 0L) ||
|
||||
|
@@ -0,0 +1,5 @@
|
||||
package mihon.core.common.utils
|
||||
|
||||
fun <T> Set<T>.mutate(action: (MutableSet<T>) -> Unit): Set<T> {
|
||||
return toMutableSet().apply(action)
|
||||
}
|
@@ -92,7 +92,7 @@ object MangaMapper {
|
||||
chapterFetchedAt: Long,
|
||||
lastRead: Long,
|
||||
bookmarkCount: Double,
|
||||
category: Long,
|
||||
categories: String,
|
||||
): LibraryManga = LibraryManga(
|
||||
manga = mapManga(
|
||||
id,
|
||||
@@ -121,7 +121,7 @@ object MangaMapper {
|
||||
isSyncing,
|
||||
notes,
|
||||
),
|
||||
category = category,
|
||||
categories = categories.split(",").map { it.toLong() },
|
||||
totalChapters = totalCount,
|
||||
readCount = readCount.toLong(),
|
||||
bookmarkCount = bookmarkCount.toLong(),
|
||||
|
39
data/src/main/sqldelight/tachiyomi/migrations/6.sqm
Normal file
39
data/src/main/sqldelight/tachiyomi/migrations/6.sqm
Normal file
@@ -0,0 +1,39 @@
|
||||
DROP VIEW IF EXISTS libraryView;
|
||||
|
||||
CREATE VIEW libraryView AS
|
||||
SELECT
|
||||
M.*,
|
||||
coalesce(C.total, 0) AS totalCount,
|
||||
coalesce(C.readCount, 0) AS readCount,
|
||||
coalesce(C.latestUpload, 0) AS latestUpload,
|
||||
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
|
||||
coalesce(MC.categories, '0') AS categories
|
||||
FROM mangas M
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
sum(read) AS readCount,
|
||||
coalesce(max(chapters.date_upload), 0) AS latestUpload,
|
||||
coalesce(max(history.last_read), 0) AS lastRead,
|
||||
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
|
||||
sum(chapters.bookmark) AS bookmarkCount
|
||||
FROM chapters
|
||||
LEFT JOIN excluded_scanlators
|
||||
ON chapters.manga_id = excluded_scanlators.manga_id
|
||||
AND chapters.scanlator = excluded_scanlators.scanlator
|
||||
LEFT JOIN history
|
||||
ON chapters._id = history.chapter_id
|
||||
WHERE excluded_scanlators.scanlator IS NULL
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN (
|
||||
SELECT manga_id, group_concat(category_id) AS categories
|
||||
FROM mangas_categories
|
||||
GROUP BY manga_id
|
||||
) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1;
|
@@ -7,9 +7,9 @@ SELECT
|
||||
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
|
||||
coalesce(C.lastRead, 0) AS lastRead,
|
||||
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
|
||||
coalesce(MC.category_id, 0) AS category
|
||||
coalesce(MC.categories, '0') AS categories
|
||||
FROM mangas M
|
||||
LEFT JOIN(
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
chapters.manga_id,
|
||||
count(*) AS total,
|
||||
@@ -28,7 +28,11 @@ LEFT JOIN(
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN mangas_categories AS MC
|
||||
LEFT JOIN (
|
||||
SELECT manga_id, group_concat(category_id) AS categories
|
||||
FROM mangas_categories
|
||||
GROUP BY manga_id
|
||||
) AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1;
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
data class LibraryManga(
|
||||
val manga: Manga,
|
||||
val category: Long,
|
||||
val categories: List<Long>,
|
||||
val totalChapters: Long,
|
||||
val readCount: Long,
|
||||
val bookmarkCount: Long,
|
||||
|
Reference in New Issue
Block a user