Optimize and cleanup library code (#2329)

This commit is contained in:
AntsyLich
2025-08-02 09:04:23 +06:00
committed by GitHub
parent 1365b28106
commit e62cd0e816
18 changed files with 370 additions and 338 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,18 +156,18 @@ class LibraryScreenModel(
combine(
getLibraryItemPreferencesFlow(),
getTrackingFilterFlow(),
) { prefs, trackFilter ->
(
listOf(
prefs.filterDownloaded,
prefs.filterUnread,
prefs.filterStarted,
prefs.filterBookmarked,
prefs.filterCompleted,
prefs.filterIntervalCustom,
) + trackFilter.values
).any { it != TriState.DISABLED }
getTrackingFiltersFlow(),
) { prefs, trackFilters ->
listOf(
prefs.filterDownloaded,
prefs.filterUnread,
prefs.filterStarted,
prefs.filterBookmarked,
prefs.filterCompleted,
prefs.filterIntervalCustom,
*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
LibraryItem(
libraryManga,
downloadCount = if (prefs.downloadBadge) {
downloadManager.getDownloadCount(libraryManga.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
} 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
) { libraryManga, preferences, _ ->
libraryManga.map { manga ->
LibraryItem(
libraryManga = manga,
downloadCount = if (preferences.downloadBadge) {
downloadManager.getDownloadCount(manga.manga).toLong()
} else {
0
},
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 {
""
},
)
}
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()
}
combine(prefFlows) {
loggedInTrackers
.mapIndexed { index, tracker -> tracker.id to it[index] }
.toMap()
if (loggedInTrackers.isEmpty()) {
flowOf(emptyMap())
} else {
val filterFlows = loggedInTrackers.map { tracker ->
libraryPreferences.filterTracking(tracker.id.toInt()).changes().map { tracker.id to it }
}
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 }
}
list.addAll(newSelections)
selectionRange.mapNotNull { items[it] }.let(list::addAll)
}
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)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package mihon.core.common.utils
fun <T> Set<T>.mutate(action: (MutableSet<T>) -> Unit): Set<T> {
return toMutableSet().apply(action)
}

View File

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

View 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;

View File

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

View File

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