diff --git a/.editorconfig b/.editorconfig index c2f10b814..fa7d450f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,9 +23,13 @@ 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 + # SY ktlint_standard_filename = disabled ktlint_standard_argument-list-wrapping = disabled @@ -33,8 +37,6 @@ ktlint_standard_function-naming = disabled ktlint_standard_property-naming = disabled ktlint_standard_multiline-expression-wrapping = disabled ktlint_standard_string-template-indent = disabled -ktlint_standard_comment-wrapping = disabled ktlint_standard_max-line-length = disabled -ktlint_standard_type-argument-comment = disabled ktlint_standard_value-argument-comment = disabled -ktlint_standard_value-parameter-comment = disabled +ktlint_standard_value-parameter-comment = disabled \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt index dd209ce40..6a119f552 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt @@ -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, columns: Int, contentPadding: PaddingValues, - selection: List, + selection: Set, 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, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt index 23cd51cfe..41f33d80c 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt @@ -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, + selection: Set, 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, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index 23f280fe9..a0e6095f3 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds fun LibraryContent( categories: List, searchQuery: String?, - selection: List, + selection: Set, 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, getColumnsForOrientation: (Boolean) -> PreferenceMutableState, - getLibraryForPage: (Int) -> List, + getItemsForCategory: (Category) -> List, ) { Column( modifier = Modifier.padding( @@ -55,13 +55,13 @@ fun LibraryContent( ) { // SY --> val coercedCurrentPage = remember(categories) { currentPage().coerceIn(0, categories.lastIndex) } - val pagerState = rememberPagerState(coercedCurrentPage) { categories.size } // SY <-- + val pagerState = rememberPagerState(coercedCurrentPage) { categories.size } 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) @@ -70,23 +70,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.getOrNull(currentPage()) ?: return@PullRefresh) + 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 @@ -95,19 +92,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, ) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt index 4f20f0274..8f9feddb8 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt @@ -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, contentPadding: PaddingValues, - selection: List, + selection: Set, 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, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt index 6487ab39f..b3bfd0f3c 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt @@ -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, + selection: Set, searchQuery: String?, onGlobalSearchClicked: () -> Unit, + getCategoryForPage: (Int) -> Category, getDisplayMode: (Int) -> PreferenceMutableState, getColumnsForOrientation: (Boolean) -> PreferenceMutableState, - getLibraryForPage: (Int) -> List, - onClickManga: (LibraryManga) -> Unit, - onLongClickManga: (LibraryManga) -> Unit, + getItemsForCategory: (Category) -> List, + 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, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index a3d48d404..6aa87a01e 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText internal fun LibraryTabs( categories: List, 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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index 537b06fc0..0d31ca218 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -130,8 +130,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private val insertTrack: InsertTrack = Injekt.get() private val trackerManager: TrackerManager = Injekt.get() private val mdList = trackerManager.mdList - private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get() - private val setReadStatus: SetReadStatus = Injekt.get() // SY <-- private val notifier = LibraryUpdateNotifier(context) @@ -156,7 +154,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet setForegroundSafely() - val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } ?: Target.CHAPTERS + val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } + ?: Target.CHAPTERS // If this is a chapter update, set the last update time to now if (target == Target.CHAPTERS) { @@ -220,28 +219,23 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet // SY <-- val listToUpdate = if (categoryId != -1L) { - libraryManga.filter { it.category == categoryId } + libraryManga.filter { categoryId in it.categories } + // SY --> } else if ( group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED) ) { - val categoriesToUpdate = libraryPreferences.updateCategories().get().map(String::toLong) - val includedManga = if (categoriesToUpdate.isNotEmpty()) { - libraryManga.filter { it.category in categoriesToUpdate } - } else { - libraryManga - } + // SY <-- + val includedCategories = libraryPreferences.updateCategories().get().map { it.toLong() }.toSet() + val excludedCategories = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() }.toSet() - 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 } + // SY --> } else { when (group) { LibraryGroup.BY_TRACK_STATUS -> { @@ -255,6 +249,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet status.int == trackingExtra } } + LibraryGroup.BY_SOURCE -> { val sourceExtra = groupExtra?.nullIfBlank()?.toIntOrNull() val source = libraryManga.map { it.manga.source } @@ -264,12 +259,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet if (source != null) libraryManga.filter { it.manga.source == source } else emptyList() } + LibraryGroup.BY_STATUS -> { val statusExtra = groupExtra?.toLongOrNull() ?: -1 libraryManga.filter { it.manga.status == statusExtra } } + LibraryGroup.UNGROUPED -> libraryManga else -> libraryManga } @@ -288,8 +285,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet when { it.manga.updateStrategy == UpdateStrategy.ONLY_FETCH_ONCE && it.totalChapters > 0L -> { skippedUpdates.add( - it.manga to - context.stringResource(MR.strings.skipped_reason_not_always_update), + it.manga to context.stringResource(MR.strings.skipped_reason_not_always_update), ) false } @@ -311,11 +307,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && it.manga.nextUpdate > fetchWindowUpperBound -> { skippedUpdates.add( - it.manga to - context.stringResource(MR.strings.skipped_reason_not_in_release_period), + it.manga to context.stringResource(MR.strings.skipped_reason_not_in_release_period), ) false } + else -> true } } @@ -328,9 +324,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet logcat { skippedUpdates .groupBy { it.second } - .map { (reason, entries) -> - "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" - } + .map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" } .joinToString() } } @@ -421,13 +415,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } } catch (e: Throwable) { val errorMessage = when (e) { - is NoChaptersException -> - context.stringResource(MR.strings.no_chapters_error) - // failedUpdates will already have the source, - // don't need to copy it into the message + is NoChaptersException -> context.stringResource( + MR.strings.no_chapters_error, + ) + // failedUpdates will already have the source, don't need to copy it into the message is SourceNotInstalledException -> context.stringResource( MR.strings.loader_not_implemented_error, ) + else -> e.message } failedUpdates.add(manga to errorMessage) @@ -539,7 +534,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet .copyFrom(networkManga) try { updateManga.await(updatedManga.toMangaUpdate()) - } catch (e: Exception) { + } catch (_: Exception) { logcat(LogPriority.ERROR) { "Manga doesn't exist anymore" } } } catch (e: Throwable) { @@ -580,7 +575,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet count++ notifier.showProgressNotification( - listOf(Manga.create().copy(ogTitle = networkManga.title)), count, size, + listOf(Manga.create().copy(ogTitle = networkManga.title)), + count, + size, ) var dbManga = getManga.await(networkManga.url, mangaDex.id) @@ -697,7 +694,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet } return file } - } catch (_: Exception) {} + } catch (_: Exception) { + } return File("") } @@ -722,8 +720,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet private const val ERROR_LOG_HELP_URL = "https://mihon.app/docs/guides/troubleshooting/" - private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60 - /** * Key for category to update. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 47e7b7621..3dc33ad50 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -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 * @@ -22,6 +24,9 @@ data class LibraryItem( */ fun matches(constraint: String): Boolean { val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo(null) } + if (constraint.startsWith("id:", true)) { + return id == constraint.substringAfter("id:").toLongOrNull() + } return libraryManga.manga.title.contains(constraint, true) || (libraryManga.manga.author?.contains(constraint, true) ?: false) || (libraryManga.manga.artist?.contains(constraint, true) ?: false) || diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index fb3d7cc7f..9178f3ba6 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastDistinctBy import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap @@ -17,7 +16,6 @@ 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 @@ -58,9 +56,6 @@ import exh.util.cancellable import exh.util.isLewd import exh.util.nullIfBlank 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.DelicateCoroutinesApi import kotlinx.coroutines.Job @@ -70,8 +65,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn @@ -80,13 +75,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking +import mihon.core.common.utils.mutate import tachiyomi.core.common.i18n.stringResource 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 @@ -122,11 +117,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> - class LibraryScreenModel( private val getLibraryManga: GetLibraryManga = Injekt.get(), private val getCategories: GetCategories = Injekt.get(), @@ -154,11 +144,13 @@ class LibraryScreenModel( private val searchEngine: SearchEngine = Injekt.get(), private val setCustomMangaInfo: SetCustomMangaInfo = Injekt.get(), private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId = Injekt.get(), - private val syncPreferences: SyncPreferences = Injekt.get(), + + syncPreferences: SyncPreferences = Injekt.get(), // SY <-- ) : StateScreenModel(State()) { var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope) + val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex] // SY --> val favoritesSync = FavoritesSyncHelper(preferences.context) @@ -170,12 +162,15 @@ class LibraryScreenModel( init { screenModelScope.launchIO { combine( - state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), - getLibraryFlow(), - getTracksPerManga.subscribe(), combine( - getTrackingFilterFlow(), - downloadCache.changes, + state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), + getCategories.subscribe(), + getFavoritesFlow(), + ::Triple, + ), + combine( + getTracksPerManga.subscribe(), + getTrackingFiltersFlow(), ::Pair, ), // SY --> @@ -185,32 +180,83 @@ class LibraryScreenModel( ::Pair, ), // SY <-- - ) { searchQuery, library, tracks, (trackingFilter, _), (groupType, sort) -> - library - // SY --> - .applyGrouping(groupType) - // SY <-- - .applyFilters(tracks, trackingFilter) - .applySort( - tracks, trackingFilter.keys, /* SY --> */sort.takeIf { - groupType != LibraryGroup.BY_DEFAULT - }, /* SY <-- */ - ) - .mapValues { (_, value) -> - if (searchQuery != null) { - // SY --> - filterLibrary(value, searchQuery, trackingFilter) - // SY <-- + getLibraryItemPreferencesFlow(), + ) { (searchQuery, categories, favorites), (tracksMap, trackingFilters), /* SY --> */ (groupType, sortingMode) /* <-- SY */, itemPreferences -> + val filteredFavorites = favorites + .applyFilters(tracksMap, trackingFilters, itemPreferences) + .let { + if (searchQuery == null) { + it } else { - value + // SY --> + // it.filter { m -> m.matches(searchQuery) } } + filterLibrary(it, searchQuery, trackingFilters) + // SY <-- } } + + 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 { + Pair( + it.libraryData, + // SY --> + it.groupType, + // SY <-- + ) + } + .distinctUntilChanged() + .map { (data, groupType) -> + data.favorites + .applyGrouping( + data.categories, + // SY --> + groupType, + // SY <-- + ) + .applySort( + data.favoritesById, + data.tracksMap, + data.loggedInTrackerIds, + // SY --> + libraryPreferences.sortingMode().get().takeIf { groupType != LibraryGroup.BY_DEFAULT }, + // SY <-- + ) + .let { + it.ifEmpty { + mapOf( + Category( + 0, + preferences.context.stringResource(MR.strings.default_category), + 0, + 0, + ) to emptyList(), + ) + } + } + } .collectLatest { mutableState.update { state -> state.copy( isLoading = false, - library = it, + groupedFavorites = it, ) } } @@ -234,21 +280,21 @@ class LibraryScreenModel( combine( getLibraryItemPreferencesFlow(), - getTrackingFilterFlow(), - ) { prefs, trackFilter -> - ( - listOf( - prefs.filterDownloaded, - prefs.filterUnread, - prefs.filterStarted, - prefs.filterBookmarked, - prefs.filterCompleted, - prefs.filterIntervalCustom, - // SY --> - prefs.filterLewd, - // SY <-- - ) + trackFilter.values - ).any { it != TriState.DISABLED } + getTrackingFiltersFlow(), + ) { prefs, trackFilters -> + listOf( + prefs.filterDownloaded, + prefs.filterUnread, + prefs.filterStarted, + prefs.filterBookmarked, + prefs.filterCompleted, + prefs.filterIntervalCustom, + // SY --> + prefs.filterLewd, + // SY <-- + *trackFilters.values.toTypedArray(), + ) + .any { it != TriState.DISABLED } } .distinctUntilChanged() .onEach { @@ -291,19 +337,19 @@ class LibraryScreenModel( // SY <-- } - private suspend fun LibraryMap.applyFilters( + private fun List.applyFilters( trackMap: Map>, trackingFilter: Map, - ): 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 { + 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() @@ -312,7 +358,7 @@ class LibraryScreenModel( val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() // SY --> - val filterLewd = prefs.filterLewd + val filterLewd = preferences.filterLewd // SY <-- val filterFnDownloaded: (LibraryItem) -> Boolean = { @@ -357,7 +403,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 } @@ -366,7 +412,7 @@ class LibraryScreenModel( !isExcluded && isIncluded } - val filterFn: (LibraryItem) -> Boolean = { + return fastFilter { filterFnDownloaded(it) && filterFnUnread(it) && filterFnStarted(it) && @@ -378,19 +424,58 @@ class LibraryScreenModel( filterFnLewd(it) // SY <-- } - - return mapValues { (_, value) -> value.fastFilter(filterFn) } } - /** - * Applies library sorting to the given map of manga. - */ - private fun LibraryMap.applySort( + private fun List.applyGrouping( + categories: List, + // SY --> + groupType: Int, + // <-- SY + ): Map> { + // SY --> + when (groupType) { + LibraryGroup.BY_DEFAULT -> { + // SY <-- + val groupCache = mutableMapOf>() + 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() } + } + // SY --> + LibraryGroup.UNGROUPED -> { + return mapOf( + Category( + 0, + preferences.context.stringResource(SYMR.strings.ungrouped), + 0, + 0, + ) to + map { it.id }, + ) + } + + else -> { + return getGroupedMangaItems( + groupType = groupType, + ) + } + } + // SY <-- + } + + private fun Map>.applySort( + favoritesById: Map, trackMap: Map>, loggedInTrackerIds: Set, - /* SY --> */ - groupSort: LibrarySort? = null, /* SY <-- */ - ): LibraryMap { + // SY --> + groupSort: LibrarySort? = null, + // SY <-- + ): Map> { // SY --> val listOfTags by lazy { libraryPreferences.sortTagsForLibrary().get() @@ -406,8 +491,10 @@ class LibraryScreenModel( } // SY <-- - val sortAlphabetically: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> - i1.libraryManga.manga.title.lowercase().compareToWithCollator(i2.libraryManga.manga.title.lowercase()) + 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 @@ -424,54 +511,63 @@ class LibraryScreenModel( } } - fun LibrarySort.comparator(): Comparator = Comparator { i1, i2 -> + fun LibrarySort.comparator(): Comparator = Comparator { manga1, manga2 -> // SY --> val sort = groupSort ?: this // SY <-- when (sort.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 (sort.isAscending) 1 else -1 - i2.libraryManga.unreadCount == 0L -> if (sort.isAscending) -1 else 1 - else -> i1.libraryManga.unreadCount.compareTo(i2.libraryManga.unreadCount) + manga1.libraryManga.unreadCount == manga2.libraryManga.unreadCount -> 0 + manga1.libraryManga.unreadCount == 0L -> if (sort.isAscending) 1 else -1 + manga2.libraryManga.unreadCount == 0L -> if (sort.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 -> { error("Why Are We Still Here? Just To Suffer?") } // SY --> LibrarySort.Type.TagList -> { val manga1IndexOfTag = listOfTags.indexOfFirst { - i1.libraryManga.manga.genre?.contains(it) ?: false + manga1.libraryManga.manga.genre?.contains(it) ?: false } val manga2IndexOfTag = listOfTags.indexOfFirst { - i2.libraryManga.manga.genre?.contains(it) ?: false + manga2.libraryManga.manga.genre?.contains(it) ?: false } manga1IndexOfTag.compareTo(manga2IndexOfTag) } @@ -483,14 +579,19 @@ class LibraryScreenModel( // SY --> val sort = groupSort ?: key.sort if (sort.type == LibrarySort.Type.Random) { + // SY <-- return@mapValues value.shuffled(Random(libraryPreferences.randomSortSeed().get())) } + + val manga = value.mapNotNull { favoritesById[it] } + + // SY --> val comparator = sort.comparator() // SY <-- .let { if (/* SY --> */ sort.isAscending /* SY <-- */) it else it.reversed() } .thenComparator(sortAlphabetically) - value.sortedWith(comparator) + manga.sortedWith(comparator).map { it.id } } } @@ -533,104 +634,62 @@ class LibraryScreenModel( } } - /** - * Get the categories and all its manga from the database. - */ - private fun getLibraryFlow(): Flow { - val libraryMangasFlow = combine( + private fun getFavoritesFlow(): Flow> { + 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) { - // SY --> - if (libraryManga.manga.source == MERGED_SOURCE_ID) { - runBlocking { - getMergedMangaById.await(libraryManga.manga.id) - }.sumOf { downloadManager.getDownloadCount(it) }.toLong() - } else { - downloadManager.getDownloadCount(libraryManga.manga).toLong() - } - // SY <-- + ) { libraryManga, preferences, _ -> + libraryManga.map { manga -> + LibraryItem( + libraryManga = manga, + downloadCount = if (preferences.downloadBadge) { + // SY --> + if (manga.manga.source == MERGED_SOURCE_ID) { + runBlocking { + getMergedMangaById.await(manga.manga.id) + }.sumOf { downloadManager.getDownloadCount(it) }.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 - } - - // SY --> - mutableState.update { state -> - state.copy(ogCategories = displayCategories) - } - // SY <-- - displayCategories.associateWith { libraryManga[it.id].orEmpty() } - } - } - - // SY --> - private fun LibraryMap.applyGrouping(groupType: Int): LibraryMap { - val items = when (groupType) { - LibraryGroup.BY_DEFAULT -> this - LibraryGroup.UNGROUPED -> { - mapOf( - Category( - 0, - preferences.context.stringResource(SYMR.strings.ungrouped), - 0, - 0, - ) to - values.flatten().distinctBy { it.libraryManga.manga.id }, - ) - } - else -> { - getGroupedMangaItems( - groupType = groupType, - libraryManga = this.values.flatten().distinctBy { it.libraryManga.manga.id }, + downloadManager.getDownloadCount(manga.manga).toLong() + } + // SY <-- + } 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 { + "" + }, ) } } - - return items } - // SY <-- /** * Flow of tracking filter preferences * * @return map of track id with the filter value */ - private fun getTrackingFilterFlow(): Flow> { + private fun getTrackingFiltersFlow(): Flow> { 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() } } } } @@ -670,26 +729,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, amount: Int?) { screenModelScope.launchNonCancellable { mangas.forEach { manga -> // SY --> @@ -716,8 +768,8 @@ class LibraryScreenModel( return@forEach } - // SY <-- + val chapters = getNextChapters.await(manga.id) .fastFilterNot { chapter -> downloadManager.getQueuedDownloadOrNull(chapter.id) != null || @@ -728,7 +780,6 @@ class LibraryScreenModel( manga.ogTitle, // SY <-- manga.source, - ) } .let { if (amount != null) it.take(amount) else it } @@ -740,17 +791,19 @@ class LibraryScreenModel( // SY --> fun cleanTitles() { - state.value.selection.fastFilter { - it.manga.isEhBasedManga() || - it.manga.source in nHentaiSourceIds - }.fastForEach { (manga) -> - val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let { - if (it.contains("|")) { - it.replace(".*\\|".toRegex(), "").trim() - } else { - it - } - } + state.value.selectedManga.fastFilter { + it.isEhBasedManga() || + it.source in nHentaiSourceIds + }.fastForEach { manga -> + val editedTitle = + manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim() + .replace("\\{.*?\\}".toRegex(), "").trim().let { + if (it.contains("|")) { + it.replace(".*\\|".toRegex(), "").trim() + } else { + it + } + } if (manga.title == editedTitle) return@fastForEach val mangaInfo = CustomMangaInfo( id = manga.id, @@ -760,7 +813,7 @@ class LibraryScreenModel( thumbnailUrl = manga.thumbnailUrl.takeUnless { it == manga.ogThumbnailUrl }, description = manga.description.takeUnless { it == manga.ogDescription }, genre = manga.genre.takeUnless { it == manga.ogGenre }, - status = manga.status.takeUnless { it == manga.ogStatus }?.toLong(), + status = manga.status.takeUnless { it == manga.ogStatus }, ) setCustomMangaInfo.set(mangaInfo) @@ -772,7 +825,7 @@ class LibraryScreenModel( fun syncMangaToDex() { launchIO { MdUtil.getEnabledMangaDex(sourcePreferences, sourceManager)?.let { mdex -> - state.value.selection.fastFilter { it.manga.source in mangaDexSourceIds }.fastForEach { (manga) -> + state.value.selectedManga.fastFilter { it.source in mangaDexSourceIds }.fastForEach { manga -> mdex.updateFollowStatus(MdUtil.getMangaId(manga.url), FollowStatus.READING) } } @@ -781,7 +834,7 @@ class LibraryScreenModel( } fun resetInfo() { - state.value.selection.fastForEach { (manga) -> + state.value.selectedManga.fastForEach { manga -> val mangaInfo = CustomMangaInfo( id = manga.id, title = null, @@ -803,11 +856,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, ) } @@ -818,16 +870,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, deleteFromLibrary: Boolean, deleteChapters: Boolean) { + fun removeMangas(mangas: List, 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, @@ -838,7 +888,7 @@ class LibraryScreenModel( } if (deleteChapters) { - mangaToDelete.forEach { manga -> + mangas.forEach { manga -> val source = sourceManager.get(manga.source) as? HttpSource if (source != null) { if (source is MergedSource) { @@ -885,19 +935,13 @@ class LibraryScreenModel( return libraryPreferences.displayMode().asState(screenModelScope) } - fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState { + fun getColumnsForOrientation(isLandscape: Boolean): PreferenceMutableState { 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() { @@ -906,42 +950,15 @@ class LibraryScreenModel( // SY --> fun showRecommendationSearchDialog() { - val mangaList = state.value.selection.map { it.manga } + val mangaList = state.value.selectedManga mutableState.update { it.copy(dialog = Dialog.RecommendationSearchSheet(mangaList)) } } - fun getCategoryName( - context: Context, - category: Category?, - groupType: Int, - categoryName: String, - ): String { - return when (groupType) { - LibraryGroup.BY_STATUS -> when (category?.id) { - SManga.ONGOING.toLong() -> context.stringResource(MR.strings.ongoing) - SManga.LICENSED.toLong() -> context.stringResource(MR.strings.licensed) - SManga.CANCELLED.toLong() -> context.stringResource(MR.strings.cancelled) - SManga.ON_HIATUS.toLong() -> context.stringResource(MR.strings.on_hiatus) - SManga.PUBLISHING_FINISHED.toLong() -> context.stringResource(MR.strings.publishing_finished) - SManga.COMPLETED.toLong() -> context.stringResource(MR.strings.completed) - else -> context.stringResource(MR.strings.unknown) - } - LibraryGroup.BY_SOURCE -> if (category?.id == LocalSource.ID) { - context.stringResource(MR.strings.local_source) - } else { - categoryName - } - LibraryGroup.BY_TRACK_STATUS -> - TrackStatus.entries - .find { it.int.toLong() == category?.id } - .let { it ?: TrackStatus.OTHER } - .let { context.stringResource(it.res) } - LibraryGroup.UNGROUPED -> context.stringResource(SYMR.strings.ungrouped) - else -> categoryName - } - } - - suspend fun filterLibrary(unfiltered: List, query: String?, loggedInTrackServices: Map): List { + private suspend fun filterLibrary( + unfiltered: List, + query: String?, + loggedInTrackServices: Map, + ): List { return if (unfiltered.isNotEmpty() && !query.isNullOrBlank()) { // Prepare filter object val parsedQuery = searchEngine.parseQuery(query) @@ -957,6 +974,10 @@ class LibraryScreenModel( .associateBy { it.id } unfiltered.asFlow().cancellable().filter { item -> val mangaId = item.libraryManga.manga.id + if (query.startsWith("id:", true)) { + val id = query.substringAfter("id:").toLongOrNull() + return@filter mangaId == id + } val sourceId = item.libraryManga.manga.source if (isMetadataSource(sourceId)) { if (mangaWithMetaIds.binarySearch(mangaId) < 0) { @@ -1031,6 +1052,7 @@ class LibraryScreenModel( (searchTags?.fastAny { it.name.contains(query, true) } == true) || (searchTitles?.fastAny { it.title.contains(query, true) } == true) } + is Namespace -> { searchTags != null && searchTags.fastAny { @@ -1042,8 +1064,10 @@ class LibraryScreenModel( (tag == null && it.namespace.equals(queryComponent.namespace, true)) } } + else -> true } + true -> when (queryComponent) { is Text -> { val query = queryComponent.asQuery() @@ -1065,6 +1089,7 @@ class LibraryScreenModel( (searchTitles?.fastAny { it.title.contains(query, true) } != true) ) } + is Namespace -> { val searchedTag = queryComponent.tag?.asQuery() searchTags == null || @@ -1083,6 +1108,7 @@ class LibraryScreenModel( } } } + else -> true } } @@ -1105,19 +1131,19 @@ class LibraryScreenModel( } // SY <-- + 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) } } @@ -1126,60 +1152,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.toSet()) list.addAll(toAdd) } state.copy(selection = newSelection) @@ -1193,11 +1208,11 @@ 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. // SY --> - val categories = state.value.ogCategories.filter { it.id != 0L } + val categories = state.value.libraryData.categories.filter { it.id != 0L } // SY <-- // Get indexes of the common categories to preselect. @@ -1218,8 +1233,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() { @@ -1232,6 +1246,7 @@ class LibraryScreenModel( val manga: List, val initialSelection: ImmutableList>, ) : Dialog + data class DeleteManga(val manga: List) : Dialog // SY --> @@ -1248,15 +1263,14 @@ class LibraryScreenModel( return getNextChapters.await(manga.id).firstOrNull() } - private fun getGroupedMangaItems( + private fun List.getGroupedMangaItems( groupType: Int, - libraryManga: List, - ): LibraryMap { + ): Map> { val context = preferences.context return when (groupType) { LibraryGroup.BY_TRACK_STATUS -> { val tracks = runBlocking { getTracks.await() }.groupBy { it.mangaId } - libraryManga.groupBy { item -> + groupBy { item -> val status = tracks[item.libraryManga.manga.id]?.firstNotNullOfOrNull { track -> TrackStatus.parseTrackerStatus(trackerManager, track.trackerId, track.status) } ?: TrackStatus.OTHER @@ -1276,9 +1290,10 @@ class LibraryScreenModel( ) } } + LibraryGroup.BY_SOURCE -> { val sources: List - libraryManga.groupBy { item -> + groupBy { item -> item.libraryManga.manga.source }.also { sources = it.keys @@ -1301,8 +1316,9 @@ class LibraryScreenModel( ) } } - else -> { - libraryManga.groupBy { item -> + + LibraryGroup.BY_STATUS -> { + groupBy { item -> item.libraryManga.manga.status }.mapKeys { Category( @@ -1329,7 +1345,10 @@ class LibraryScreenModel( ) } } + + else -> emptyMap() }.toSortedMap(compareBy { it.order }) + .mapValues { (_, libraryItem) -> libraryItem.fastMap { it.id } } } fun runRecommendationSearch(selection: List) { @@ -1383,51 +1402,62 @@ class LibraryScreenModel( // SY <-- ) + @Immutable + data class LibraryData( + val isInitialized: Boolean = false, + val categories: List = emptyList(), + val favorites: List = emptyList(), + val tracksMap: Map> = emptyMap(), + val loggedInTrackerIds: Set = 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 = persistentListOf(), + val selection: Set = 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> = emptyMap(), // SY --> val showSyncExh: Boolean = false, val isSyncEnabled: Boolean = false, - val ogCategories: List = emptyList(), val groupType: Int = LibraryGroup.BY_DEFAULT, // SY <-- ) { - 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 } } + + /** + * The grouped tabs which is displayed above the library screen. + * They can be actual [Category] or [Source], [Track]... + */ + val displayedCategories = groupedFavorites.keys.toList() // SY --> val showCleanTitles: Boolean by lazy { - selection.any { - it.manga.isEhBasedManga() || - it.manga.source in nHentaiSourceIds + selectedManga.fastAny { + it.isEhBasedManga() || + it.source in nHentaiSourceIds } } val showAddToMangadex: Boolean by lazy { - selection.any { it.manga.source in mangaDexSourceIds } + selectedManga.any { it.source in mangaDexSourceIds } } val showResetInfo: Boolean by lazy { - selection.fastAny { (manga) -> + selectedManga.fastAny { manga -> manga.title != manga.ogTitle || manga.author != manga.ogAuthor || manga.artist != manga.ogArtist || @@ -1439,16 +1469,17 @@ class LibraryScreenModel( } // SY <-- - fun getLibraryItemsByCategoryId(categoryId: Long): List? { - return library.firstNotNullOfOrNull { (k, v) -> v.takeIf { k.id == categoryId } } + fun getItemsForCategoryId(categoryId: Long): List { + val category = displayedCategories.find { it.id == categoryId } ?: return emptyList() + return getItemsForCategory(category) } - fun getLibraryItemsByPage(page: Int): List { - return library.values.toTypedArray().getOrNull(page).orEmpty() + fun getItemsForCategory(category: Category): List { + 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( @@ -1456,18 +1487,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) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt index 044371ec9..f7b45506c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt @@ -142,18 +142,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.coerceAtMost(state.categories.lastIndex)]) - }, + onClickRefresh = { onClickRefresh(screenModel.activeCategory) }, onClickGlobalUpdate = { onClickRefresh(null) }, onClickOpenRandomManga = { scope.launch { @@ -180,7 +177,8 @@ data object LibraryTab : Tab { // SY <-- 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 = { @@ -189,15 +187,15 @@ 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, // SY --> onClickCleanTitles = screenModel::cleanTitles.takeIf { state.showCleanTitles }, onClickMigrate = { - val selectedMangaIds = state.selection - .filterNot { it.manga.source == MERGED_SOURCE_ID } - .map { it.manga.id } + val selectedMangaIds = state.selectedManga + .filterNot { it.source == MERGED_SOURCE_ID } + .map { it.id } screenModel.clearSelection() if (selectedMangaIds.isNotEmpty()) { PreMigrationScreen.navigateToMigration( @@ -218,7 +216,10 @@ data object LibraryTab : Tab { 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( @@ -233,9 +234,10 @@ data object LibraryTab : Tab { ), ) } + else -> { LibraryContent( - categories = state.categories, + categories = state.displayedCategories, searchQuery = state.searchQuery, selection = state.selection, contentPadding = contentPadding, @@ -243,7 +245,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) @@ -258,18 +260,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) }, + ) } } } @@ -277,20 +280,16 @@ 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, // SY --> - hasCategories = state.categories.fastAny { !it.isSystemCategory }, + hasCategories = state.libraryData.categories.fastAny { !it.isSystemCategory }, // SY <-- ) } + is LibraryScreenModel.Dialog.ChangeCategory -> { ChangeCategoryDialog( initialSelection = dialog.initialSelection, @@ -305,6 +304,7 @@ data object LibraryTab : Tab { }, ) } + is LibraryScreenModel.Dialog.DeleteManga -> { DeleteLibraryMangaDialog( containsLocalManga = dialog.manga.any(Manga::isLocal), @@ -325,6 +325,7 @@ data object LibraryTab : Tab { }, ) } + LibraryScreenModel.Dialog.SyncFavoritesConfirm -> { SyncFavoritesConfirmDialog( onDismissRequest = onDismissRequest, @@ -334,6 +335,7 @@ data object LibraryTab : Tab { }, ) } + is LibraryScreenModel.Dialog.RecommendationSearchSheet -> { RecommendationSearchBottomSheetDialog( onDismissRequest = onDismissRequest, @@ -390,14 +392,17 @@ data object LibraryTab : Tab { screenModel.recommendationSearch.status.value = SearchStatus.Idle } + is SearchStatus.Finished.WithoutResults -> { context.toast(SYMR.strings.rec_no_results) screenModel.recommendationSearch.status.value = SearchStatus.Idle } + is SearchStatus.Cancelling -> { screenModel.cancelRecommendationSearch() screenModel.recommendationSearch.status.value = SearchStatus.Idle } + else -> {} } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt index f5726e82f..8dd9c23f4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/stats/StatsScreenModel.kt @@ -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 @@ -108,26 +106,15 @@ class StatsScreenModel( } private fun getGlobalUpdateItemCount(libraryManga: List): 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 includedCategories = preferences.updateCategories().get().map { it.toLong() }.toSet() + val excludedCategories = preferences.updateCategoriesExclude().get().map { it.toLong() }.toSet() 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) || diff --git a/core/common/src/main/kotlin/mihon/core/common/utils/Set.kt b/core/common/src/main/kotlin/mihon/core/common/utils/Set.kt new file mode 100644 index 000000000..daf31fd44 --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/utils/Set.kt @@ -0,0 +1,5 @@ +package mihon.core.common.utils + +fun Set.mutate(action: (MutableSet) -> Unit): Set { + return toMutableSet().apply(action) +} diff --git a/data/src/main/java/tachiyomi/data/LibraryQuery.kt b/data/src/main/java/tachiyomi/data/LibraryQuery.kt index 9e7dce101..bc0cd9516 100644 --- a/data/src/main/java/tachiyomi/data/LibraryQuery.kt +++ b/data/src/main/java/tachiyomi/data/LibraryQuery.kt @@ -41,7 +41,7 @@ private val mapper = { cursor: SqlCursor -> chapterFetchedAt = cursor.getLong(29)!!, lastRead = cursor.getLong(30)!!, bookmarkCount = cursor.getDouble(31)!!, - category = cursor.getLong(32)!!, + categories = cursor.getString(32)!!, ) } diff --git a/data/src/main/sqldelight/tachiyomi/migrations/35.sqm b/data/src/main/sqldelight/tachiyomi/migrations/35.sqm new file mode 100644 index 000000000..fd82b7302 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/35.sqm @@ -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; diff --git a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq index 1021c544f..8ce19ae1d 100644 --- a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq @@ -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,10 +28,14 @@ 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; library: SELECT * -FROM libraryView; \ No newline at end of file +FROM libraryView; diff --git a/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt b/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt index 65e06c195..2fda140aa 100644 --- a/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt +++ b/domain/src/main/java/tachiyomi/domain/library/model/LibraryManga.kt @@ -4,7 +4,7 @@ import tachiyomi.domain.manga.model.Manga data class LibraryManga( val manga: Manga, - val category: Long, + val categories: List, val totalChapters: Long, val readCount: Long, val bookmarkCount: Long,