mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Optimize and cleanup library code (#2329)
This commit is contained in:
		| @@ -23,6 +23,9 @@ ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 | ||||
| ktlint_code_style = intellij_idea | ||||
| ktlint_function_naming_ignore_when_annotated_with = Composable | ||||
| ktlint_standard_class-signature = disabled | ||||
| ktlint_standard_comment-wrapping = disabled | ||||
| ktlint_standard_discouraged-comment-location = disabled | ||||
| ktlint_standard_function-expression-body = disabled | ||||
| ktlint_standard_function-signature = disabled | ||||
| ktlint_standard_type-argument-comment = disabled | ||||
| ktlint_standard_type-parameter-comment = disabled | ||||
|   | ||||
| @@ -34,6 +34,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co | ||||
| - Added autofill support to tracker login dialog ([@AntsyLich](https://github.com/AntsyLich)) ([#2069](https://github.com/mihonapp/mihon/pull/2069)) | ||||
| - Added option to hide missing chapter count ([@User826](https://github.com/User826), [@AntsyLich](https://github.com/AntsyLich)) ([#2108](https://github.com/mihonapp/mihon/pull/2108)) | ||||
| - Use median to determine smart update interval, making it more resilient to long hiatuses ([@Kladki](https://github.com/Kladki)) ([#2251](https://github.com/mihonapp/mihon/pull/2251)) | ||||
| - Optimize library code to potentially better handle big user libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#2329](https://github.com/mihonapp/mihon/pull/2329)) | ||||
|  | ||||
| ### Changed | ||||
| - Display all similarly named duplicates in duplicate manga dialogue ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#1861](https://github.com/mihonapp/mihon/pull/1861)) | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.lazy.grid.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -15,7 +14,7 @@ internal fun LibraryComfortableGrid( | ||||
|     items: List<LibraryItem>, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     selection: Set<Long>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -35,7 +34,7 @@ internal fun LibraryComfortableGrid( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaComfortableGridItem( | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 isSelected = manga.id in selection, | ||||
|                 title = manga.title, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.fillMaxSize | ||||
| import androidx.compose.foundation.lazy.grid.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -16,7 +15,7 @@ internal fun LibraryCompactGrid( | ||||
|     showTitle: Boolean, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     selection: Set<Long>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -36,7 +35,7 @@ internal fun LibraryCompactGrid( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaCompactGridItem( | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 isSelected = manga.id in selection, | ||||
|                 title = manga.title.takeIf { showTitle }, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -29,22 +29,22 @@ import kotlin.time.Duration.Companion.seconds | ||||
| fun LibraryContent( | ||||
|     categories: List<Category>, | ||||
|     searchQuery: String?, | ||||
|     selection: List<LibraryManga>, | ||||
|     selection: Set<Long>, | ||||
|     contentPadding: PaddingValues, | ||||
|     currentPage: () -> Int, | ||||
|     hasActiveFilters: Boolean, | ||||
|     showPageTabs: Boolean, | ||||
|     onChangeCurrentPage: (Int) -> Unit, | ||||
|     onMangaClicked: (Long) -> Unit, | ||||
|     onClickManga: (Long) -> Unit, | ||||
|     onContinueReadingClicked: ((LibraryManga) -> Unit)?, | ||||
|     onToggleSelection: (LibraryManga) -> Unit, | ||||
|     onToggleRangeSelection: (LibraryManga) -> Unit, | ||||
|     onRefresh: (Category?) -> Boolean, | ||||
|     onToggleSelection: (Category, LibraryManga) -> Unit, | ||||
|     onToggleRangeSelection: (Category, LibraryManga) -> Unit, | ||||
|     onRefresh: () -> Boolean, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     getItemCountForCategory: (Category) -> Int?, | ||||
|     getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
|     getItemsForCategory: (Category) -> List<LibraryItem>, | ||||
| ) { | ||||
|     Column( | ||||
|         modifier = Modifier.padding( | ||||
| @@ -59,7 +59,7 @@ fun LibraryContent( | ||||
|         val scope = rememberCoroutineScope() | ||||
|         var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } | ||||
|  | ||||
|         if (showPageTabs && categories.size > 1) { | ||||
|         if (showPageTabs && categories.isNotEmpty()) { | ||||
|             LaunchedEffect(categories) { | ||||
|                 if (categories.size <= pagerState.currentPage) { | ||||
|                     pagerState.scrollToPage(categories.size - 1) | ||||
| @@ -68,23 +68,20 @@ fun LibraryContent( | ||||
|             LibraryTabs( | ||||
|                 categories = categories, | ||||
|                 pagerState = pagerState, | ||||
|                 getNumberOfMangaForCategory = getNumberOfMangaForCategory, | ||||
|             ) { scope.launch { pagerState.animateScrollToPage(it) } } | ||||
|         } | ||||
|  | ||||
|         val notSelectionMode = selection.isEmpty() | ||||
|         val onClickManga = { manga: LibraryManga -> | ||||
|             if (notSelectionMode) { | ||||
|                 onMangaClicked(manga.manga.id) | ||||
|             } else { | ||||
|                 onToggleSelection(manga) | ||||
|             } | ||||
|                 getItemCountForCategory = getItemCountForCategory, | ||||
|                 onTabItemClick = { | ||||
|                     scope.launch { | ||||
|                         pagerState.animateScrollToPage(it) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         PullRefresh( | ||||
|             refreshing = isRefreshing, | ||||
|             enabled = selection.isEmpty(), | ||||
|             onRefresh = { | ||||
|                 val started = onRefresh(categories[currentPage()]) | ||||
|                 val started = onRefresh() | ||||
|                 if (!started) return@PullRefresh | ||||
|                 scope.launch { | ||||
|                     // Fake refresh status but hide it after a second as it's a long running task | ||||
| @@ -93,19 +90,25 @@ fun LibraryContent( | ||||
|                     isRefreshing = false | ||||
|                 } | ||||
|             }, | ||||
|             enabled = notSelectionMode, | ||||
|         ) { | ||||
|             LibraryPager( | ||||
|                 state = pagerState, | ||||
|                 contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()), | ||||
|                 hasActiveFilters = hasActiveFilters, | ||||
|                 selectedManga = selection, | ||||
|                 selection = selection, | ||||
|                 searchQuery = searchQuery, | ||||
|                 onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|                 getCategoryForPage = { page -> categories[page] }, | ||||
|                 getDisplayMode = getDisplayMode, | ||||
|                 getColumnsForOrientation = getColumnsForOrientation, | ||||
|                 getLibraryForPage = getLibraryForPage, | ||||
|                 onClickManga = onClickManga, | ||||
|                 getItemsForCategory = getItemsForCategory, | ||||
|                 onClickManga = { category, manga -> | ||||
|                     if (selection.isNotEmpty()) { | ||||
|                         onToggleSelection(category, manga) | ||||
|                     } else { | ||||
|                         onClickManga(manga.manga.id) | ||||
|                     } | ||||
|                 }, | ||||
|                 onLongClickManga = onToggleRangeSelection, | ||||
|                 onClickContinueReading = onContinueReadingClicked, | ||||
|             ) | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.domain.manga.model.MangaCover | ||||
| @@ -18,7 +17,7 @@ import tachiyomi.presentation.core.util.plus | ||||
| internal fun LibraryList( | ||||
|     items: List<LibraryItem>, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     selection: Set<Long>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| @@ -45,7 +44,7 @@ internal fun LibraryList( | ||||
|         ) { libraryItem -> | ||||
|             val manga = libraryItem.libraryManga.manga | ||||
|             MangaListItem( | ||||
|                 isSelected = selection.fastAny { it.id == libraryItem.libraryManga.id }, | ||||
|                 isSelected = manga.id in selection, | ||||
|                 title = manga.title, | ||||
|                 coverData = MangaCover( | ||||
|                     mangaId = manga.id, | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalConfiguration | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.core.preference.PreferenceMutableState | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import tachiyomi.domain.category.model.Category | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.library.model.LibraryManga | ||||
| import tachiyomi.i18n.MR | ||||
| @@ -31,14 +32,15 @@ fun LibraryPager( | ||||
|     state: PagerState, | ||||
|     contentPadding: PaddingValues, | ||||
|     hasActiveFilters: Boolean, | ||||
|     selectedManga: List<LibraryManga>, | ||||
|     selection: Set<Long>, | ||||
|     searchQuery: String?, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getCategoryForPage: (Int) -> Category, | ||||
|     getDisplayMode: (Int) -> PreferenceMutableState<LibraryDisplayMode>, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
|     onClickManga: (LibraryManga) -> Unit, | ||||
|     onLongClickManga: (LibraryManga) -> Unit, | ||||
|     getItemsForCategory: (Category) -> List<LibraryItem>, | ||||
|     onClickManga: (Category, LibraryManga) -> Unit, | ||||
|     onLongClickManga: (Category, LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| ) { | ||||
|     HorizontalPager( | ||||
| @@ -50,9 +52,10 @@ fun LibraryPager( | ||||
|             // To make sure only one offscreen page is being composed | ||||
|             return@HorizontalPager | ||||
|         } | ||||
|         val library = getLibraryForPage(page) | ||||
|         val category = getCategoryForPage(page) | ||||
|         val items = getItemsForCategory(category) | ||||
|  | ||||
|         if (library.isEmpty()) { | ||||
|         if (items.isEmpty()) { | ||||
|             LibraryPagerEmptyScreen( | ||||
|                 searchQuery = searchQuery, | ||||
|                 hasActiveFilters = hasActiveFilters, | ||||
| @@ -72,12 +75,15 @@ fun LibraryPager( | ||||
|             remember { mutableIntStateOf(0) } | ||||
|         } | ||||
|  | ||||
|         val onClickManga: (LibraryManga) -> Unit = { onClickManga(category, it) } | ||||
|         val onLongClickManga: (LibraryManga) -> Unit = { onLongClickManga(category, it) } | ||||
|  | ||||
|         when (displayMode) { | ||||
|             LibraryDisplayMode.List -> { | ||||
|                 LibraryList( | ||||
|                     items = library, | ||||
|                     items = items, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
|                     selection = selection, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
| @@ -87,11 +93,11 @@ fun LibraryPager( | ||||
|             } | ||||
|             LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { | ||||
|                 LibraryCompactGrid( | ||||
|                     items = library, | ||||
|                     items = items, | ||||
|                     showTitle = displayMode is LibraryDisplayMode.CompactGrid, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
|                     selection = selection, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
| @@ -101,10 +107,10 @@ fun LibraryPager( | ||||
|             } | ||||
|             LibraryDisplayMode.ComfortableGrid -> { | ||||
|                 LibraryComfortableGrid( | ||||
|                     items = library, | ||||
|                     items = items, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
|                     selection = selection, | ||||
|                     onClick = onClickManga, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
|   | ||||
| @@ -18,13 +18,11 @@ import tachiyomi.presentation.core.components.material.TabText | ||||
| internal fun LibraryTabs( | ||||
|     categories: List<Category>, | ||||
|     pagerState: PagerState, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     getItemCountForCategory: (Category) -> Int?, | ||||
|     onTabItemClick: (Int) -> Unit, | ||||
| ) { | ||||
|     val currentPageIndex = pagerState.currentPage.coerceAtMost(categories.lastIndex) | ||||
|     Column( | ||||
|         modifier = Modifier.zIndex(1f), | ||||
|     ) { | ||||
|     Column(modifier = Modifier.zIndex(2f)) { | ||||
|         PrimaryScrollableTabRow( | ||||
|             selectedTabIndex = currentPageIndex, | ||||
|             edgePadding = 0.dp, | ||||
| @@ -39,7 +37,7 @@ internal fun LibraryTabs( | ||||
|                     text = { | ||||
|                         TabText( | ||||
|                             text = category.visualName, | ||||
|                             badgeCount = getNumberOfMangaForCategory(category), | ||||
|                             badgeCount = getItemCountForCategory(category), | ||||
|                         ) | ||||
|                     }, | ||||
|                     unselectedContentColor = MaterialTheme.colorScheme.onSurface, | ||||
|   | ||||
| @@ -158,25 +158,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet | ||||
|         val libraryManga = getLibraryManga.await() | ||||
|  | ||||
|         val listToUpdate = if (categoryId != -1L) { | ||||
|             libraryManga.filter { it.category == categoryId } | ||||
|             libraryManga.filter { categoryId in it.categories } | ||||
|         } else { | ||||
|             val categoriesToUpdate = libraryPreferences.updateCategories().get().map { it.toLong() } | ||||
|             val includedManga = if (categoriesToUpdate.isNotEmpty()) { | ||||
|                 libraryManga.filter { it.category in categoriesToUpdate } | ||||
|             } else { | ||||
|                 libraryManga | ||||
|             } | ||||
|             val includedCategories = libraryPreferences.updateCategories().get().map { it.toLong() } | ||||
|             val excludedCategories = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() } | ||||
|  | ||||
|             val categoriesToExclude = libraryPreferences.updateCategoriesExclude().get().map { it.toLong() } | ||||
|             val excludedMangaIds = if (categoriesToExclude.isNotEmpty()) { | ||||
|                 libraryManga.filter { it.category in categoriesToExclude }.map { it.manga.id } | ||||
|             } else { | ||||
|                 emptyList() | ||||
|             libraryManga.filter { | ||||
|                 val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty() | ||||
|                 val excluded = it.categories.intersect(excludedCategories).isNotEmpty() | ||||
|                 included && !excluded | ||||
|             } | ||||
|  | ||||
|             includedManga | ||||
|                 .filterNot { it.manga.id in excludedMangaIds } | ||||
|                 .distinctBy { it.manga.id } | ||||
|         } | ||||
|  | ||||
|         val restrictions = libraryPreferences.autoUpdateMangaRestrictions().get() | ||||
|   | ||||
| @@ -14,6 +14,8 @@ data class LibraryItem( | ||||
|     val sourceLanguage: String = "", | ||||
|     private val sourceManager: SourceManager = Injekt.get(), | ||||
| ) { | ||||
|     val id: Long = libraryManga.id | ||||
|  | ||||
|     /** | ||||
|      * Checks if a query matches the manga | ||||
|      * | ||||
| @@ -23,8 +25,7 @@ data class LibraryItem( | ||||
|     fun matches(constraint: String): Boolean { | ||||
|         val sourceName by lazy { sourceManager.getOrStub(libraryManga.manga.source).getNameForMangaInfo() } | ||||
|         if (constraint.startsWith("id:", true)) { | ||||
|             val id = constraint.substringAfter("id:").toLongOrNull() | ||||
|             return libraryManga.id == id | ||||
|             return id == constraint.substringAfter("id:").toLongOrNull() | ||||
|         } | ||||
|         return libraryManga.manga.title.contains(constraint, true) || | ||||
|             (libraryManga.manga.author?.contains(constraint, true) ?: false) || | ||||
|   | ||||
| @@ -4,16 +4,13 @@ import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import androidx.compose.ui.util.fastDistinctBy | ||||
| import androidx.compose.ui.util.fastFilter | ||||
| import androidx.compose.ui.util.fastMap | ||||
| import androidx.compose.ui.util.fastMapNotNull | ||||
| import cafe.adriel.voyager.core.model.StateScreenModel | ||||
| import cafe.adriel.voyager.core.model.screenModelScope | ||||
| import eu.kanade.core.preference.PreferenceMutableState | ||||
| import eu.kanade.core.preference.asState | ||||
| import eu.kanade.core.util.fastFilterNot | ||||
| import eu.kanade.core.util.fastPartition | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.chapter.interactor.SetReadStatus | ||||
| import eu.kanade.domain.manga.interactor.UpdateManga | ||||
| @@ -29,28 +26,25 @@ import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.util.chapter.getNextUnread | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import kotlinx.collections.immutable.ImmutableList | ||||
| import kotlinx.collections.immutable.PersistentList | ||||
| import kotlinx.collections.immutable.mutate | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.collections.immutable.toImmutableList | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.debounce | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.first | ||||
| import kotlinx.coroutines.flow.dropWhile | ||||
| import kotlinx.coroutines.flow.flatMapLatest | ||||
| import kotlinx.coroutines.flow.flowOf | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.update | ||||
| import mihon.core.common.utils.mutate | ||||
| import tachiyomi.core.common.preference.CheckboxState | ||||
| import tachiyomi.core.common.preference.TriState | ||||
| import tachiyomi.core.common.util.lang.compareToWithCollator | ||||
| import tachiyomi.core.common.util.lang.launchIO | ||||
| import tachiyomi.core.common.util.lang.launchNonCancellable | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.SetMangaCategories | ||||
| import tachiyomi.domain.category.model.Category | ||||
| @@ -74,11 +68,6 @@ import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import kotlin.random.Random | ||||
|  | ||||
| /** | ||||
|  * Typealias for the library manga, using the category as keys, and list of manga as values. | ||||
|  */ | ||||
| typealias LibraryMap = Map<Category, List<LibraryItem>> | ||||
|  | ||||
| class LibraryScreenModel( | ||||
|     private val getLibraryManga: GetLibraryManga = Injekt.get(), | ||||
|     private val getCategories: GetCategories = Injekt.get(), | ||||
| @@ -98,32 +87,52 @@ class LibraryScreenModel( | ||||
| ) : StateScreenModel<LibraryScreenModel.State>(State()) { | ||||
|  | ||||
|     var activeCategoryIndex: Int by libraryPreferences.lastUsedCategory().asState(screenModelScope) | ||||
|     val activeCategory: Category get() = state.value.displayedCategories[activeCategoryIndex] | ||||
|  | ||||
|     init { | ||||
|         screenModelScope.launchIO { | ||||
|             combine( | ||||
|                 state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), | ||||
|                 getLibraryFlow(), | ||||
|                 getTracksPerManga.subscribe(), | ||||
|                 getTrackingFilterFlow(), | ||||
|                 downloadCache.changes, | ||||
|             ) { searchQuery, library, tracks, trackingFilter, _ -> | ||||
|                 library | ||||
|                     .applyFilters(tracks, trackingFilter) | ||||
|                     .applySort(tracks, trackingFilter.keys) | ||||
|                     .mapValues { (_, value) -> | ||||
|                         if (searchQuery != null) { | ||||
|                             value.filter { it.matches(searchQuery) } | ||||
|                         } else { | ||||
|                             value | ||||
|                         } | ||||
|                     } | ||||
|                 getCategories.subscribe(), | ||||
|                 getFavoritesFlow(), | ||||
|                 combine(getTracksPerManga.subscribe(), getTrackingFiltersFlow(), ::Pair), | ||||
|                 getLibraryItemPreferencesFlow(), | ||||
|             ) { searchQuery, categories, favorites, (tracksMap, trackingFilters), itemPreferences -> | ||||
|                 val filteredFavorites = favorites | ||||
|                     .applyFilters(tracksMap, trackingFilters, itemPreferences) | ||||
|                     .let { if (searchQuery == null) it else it.filter { m -> m.matches(searchQuery) } } | ||||
|  | ||||
|                 LibraryData( | ||||
|                     isInitialized = true, | ||||
|                     categories = categories, | ||||
|                     favorites = filteredFavorites, | ||||
|                     tracksMap = tracksMap, | ||||
|                     loggedInTrackerIds = trackingFilters.keys, | ||||
|                 ) | ||||
|             } | ||||
|                 .distinctUntilChanged() | ||||
|                 .collectLatest { libraryData -> | ||||
|                     mutableState.update { state -> | ||||
|                         state.copy(libraryData = libraryData) | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         screenModelScope.launchIO { | ||||
|             state | ||||
|                 .dropWhile { !it.libraryData.isInitialized } | ||||
|                 .map { it.libraryData } | ||||
|                 .distinctUntilChanged() | ||||
|                 .map { data -> | ||||
|                     data.favorites | ||||
|                         .applyGrouping(data.categories) | ||||
|                         .applySort(data.favoritesById, data.tracksMap, data.loggedInTrackerIds) | ||||
|                 } | ||||
|                 .collectLatest { | ||||
|                     mutableState.update { state -> | ||||
|                         state.copy( | ||||
|                             isLoading = false, | ||||
|                             library = it, | ||||
|                             groupedFavorites = it, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
| @@ -147,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) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -112,16 +112,15 @@ data object LibraryTab : Tab { | ||||
|                     defaultCategoryTitle = stringResource(MR.strings.label_default), | ||||
|                     page = screenModel.activeCategoryIndex, | ||||
|                 ) | ||||
|                 val tabVisible = state.showCategoryTabs && state.categories.size > 1 | ||||
|                 LibraryToolbar( | ||||
|                     hasActiveFilters = state.hasActiveFilters, | ||||
|                     selectedCount = state.selection.size, | ||||
|                     title = title, | ||||
|                     onClickUnselectAll = screenModel::clearSelection, | ||||
|                     onClickSelectAll = { screenModel.selectAll(screenModel.activeCategoryIndex) }, | ||||
|                     onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategoryIndex) }, | ||||
|                     onClickSelectAll = screenModel::selectAll, | ||||
|                     onClickInvertSelection = screenModel::invertSelection, | ||||
|                     onClickFilter = screenModel::showSettingsDialog, | ||||
|                     onClickRefresh = { onClickRefresh(state.categories[screenModel.activeCategoryIndex]) }, | ||||
|                     onClickRefresh = { onClickRefresh(screenModel.activeCategory) }, | ||||
|                     onClickGlobalUpdate = { onClickRefresh(null) }, | ||||
|                     onClickOpenRandomManga = { | ||||
|                         scope.launch { | ||||
| @@ -137,7 +136,8 @@ data object LibraryTab : Tab { | ||||
|                     }, | ||||
|                     searchQuery = state.searchQuery, | ||||
|                     onSearchQueryChange = screenModel::search, | ||||
|                     scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab | ||||
|                     // For scroll overlay when no tab | ||||
|                     scrollBehavior = scrollBehavior.takeIf { !state.showCategoryTabs }, | ||||
|                 ) | ||||
|             }, | ||||
|             bottomBar = { | ||||
| @@ -146,15 +146,17 @@ data object LibraryTab : Tab { | ||||
|                     onChangeCategoryClicked = screenModel::openChangeCategoryDialog, | ||||
|                     onMarkAsReadClicked = { screenModel.markReadSelection(true) }, | ||||
|                     onMarkAsUnreadClicked = { screenModel.markReadSelection(false) }, | ||||
|                     onDownloadClicked = screenModel::runDownloadActionSelection | ||||
|                         .takeIf { state.selection.fastAll { !it.manga.isLocal() } }, | ||||
|                     onDownloadClicked = screenModel::performDownloadAction | ||||
|                         .takeIf { state.selectedManga.fastAll { !it.isLocal() } }, | ||||
|                     onDeleteClicked = screenModel::openDeleteMangaDialog, | ||||
|                 ) | ||||
|             }, | ||||
|             snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|         ) { contentPadding -> | ||||
|             when { | ||||
|                 state.isLoading -> LoadingScreen(Modifier.padding(contentPadding)) | ||||
|                 state.isLoading -> { | ||||
|                     LoadingScreen(Modifier.padding(contentPadding)) | ||||
|                 } | ||||
|                 state.searchQuery.isNullOrEmpty() && !state.hasActiveFilters && state.isLibraryEmpty -> { | ||||
|                     val handler = LocalUriHandler.current | ||||
|                     EmptyScreen( | ||||
| @@ -171,7 +173,7 @@ data object LibraryTab : Tab { | ||||
|                 } | ||||
|                 else -> { | ||||
|                     LibraryContent( | ||||
|                         categories = state.categories, | ||||
|                         categories = state.displayedCategories, | ||||
|                         searchQuery = state.searchQuery, | ||||
|                         selection = state.selection, | ||||
|                         contentPadding = contentPadding, | ||||
| @@ -179,7 +181,7 @@ data object LibraryTab : Tab { | ||||
|                         hasActiveFilters = state.hasActiveFilters, | ||||
|                         showPageTabs = state.showCategoryTabs || !state.searchQuery.isNullOrEmpty(), | ||||
|                         onChangeCurrentPage = { screenModel.activeCategoryIndex = it }, | ||||
|                         onMangaClicked = { navigator.push(MangaScreen(it)) }, | ||||
|                         onClickManga = { navigator.push(MangaScreen(it)) }, | ||||
|                         onContinueReadingClicked = { it: LibraryManga -> | ||||
|                             scope.launchIO { | ||||
|                                 val chapter = screenModel.getNextUnreadChapter(it.manga) | ||||
| @@ -194,18 +196,19 @@ data object LibraryTab : Tab { | ||||
|                             Unit | ||||
|                         }.takeIf { state.showMangaContinueButton }, | ||||
|                         onToggleSelection = screenModel::toggleSelection, | ||||
|                         onToggleRangeSelection = { | ||||
|                             screenModel.toggleRangeSelection(it) | ||||
|                         onToggleRangeSelection = { category, manga -> | ||||
|                             screenModel.toggleRangeSelection(category, manga) | ||||
|                             haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|                         }, | ||||
|                         onRefresh = onClickRefresh, | ||||
|                         onRefresh = { onClickRefresh(screenModel.activeCategory) }, | ||||
|                         onGlobalSearchClicked = { | ||||
|                             navigator.push(GlobalSearchScreen(screenModel.state.value.searchQuery ?: "")) | ||||
|                         }, | ||||
|                         getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) }, | ||||
|                         getItemCountForCategory = { state.getItemCountForCategory(it) }, | ||||
|                         getDisplayMode = { screenModel.getDisplayMode() }, | ||||
|                         getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) }, | ||||
|                     ) { state.getLibraryItemsByPage(it) } | ||||
|                         getColumnsForOrientation = { screenModel.getColumnsForOrientation(it) }, | ||||
|                         getItemsForCategory = { state.getItemsForCategory(it) }, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @@ -213,15 +216,10 @@ data object LibraryTab : Tab { | ||||
|         val onDismissRequest = screenModel::closeDialog | ||||
|         when (val dialog = state.dialog) { | ||||
|             is LibraryScreenModel.Dialog.SettingsSheet -> run { | ||||
|                 val category = state.categories.getOrNull(screenModel.activeCategoryIndex) | ||||
|                 if (category == null) { | ||||
|                     onDismissRequest() | ||||
|                     return@run | ||||
|                 } | ||||
|                 LibrarySettingsDialog( | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     screenModel = settingsScreenModel, | ||||
|                     category = category, | ||||
|                     category = screenModel.activeCategory, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryScreenModel.Dialog.ChangeCategory -> { | ||||
|   | ||||
| @@ -2,11 +2,9 @@ package eu.kanade.tachiyomi.ui.stats | ||||
|  | ||||
| import androidx.compose.ui.util.fastDistinctBy | ||||
| import androidx.compose.ui.util.fastFilter | ||||
| import androidx.compose.ui.util.fastMapNotNull | ||||
| import cafe.adriel.voyager.core.model.StateScreenModel | ||||
| import cafe.adriel.voyager.core.model.screenModelScope | ||||
| import eu.kanade.core.util.fastCountNot | ||||
| import eu.kanade.core.util.fastFilterNot | ||||
| import eu.kanade.presentation.more.stats.StatsScreenState | ||||
| import eu.kanade.presentation.more.stats.data.StatsData | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| @@ -88,25 +86,14 @@ class StatsScreenModel( | ||||
|  | ||||
|     private fun getGlobalUpdateItemCount(libraryManga: List<LibraryManga>): Int { | ||||
|         val includedCategories = preferences.updateCategories().get().map { it.toLong() } | ||||
|         val includedManga = if (includedCategories.isNotEmpty()) { | ||||
|             libraryManga.filter { it.category in includedCategories } | ||||
|         } else { | ||||
|             libraryManga | ||||
|         } | ||||
|  | ||||
|         val excludedCategories = preferences.updateCategoriesExclude().get().map { it.toLong() } | ||||
|         val excludedMangaIds = if (excludedCategories.isNotEmpty()) { | ||||
|             libraryManga.fastMapNotNull { manga -> | ||||
|                 manga.id.takeIf { manga.category in excludedCategories } | ||||
|             } | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         val updateRestrictions = preferences.autoUpdateMangaRestrictions().get() | ||||
|         return includedManga | ||||
|             .fastFilterNot { it.manga.id in excludedMangaIds } | ||||
|             .fastDistinctBy { it.manga.id } | ||||
|  | ||||
|         return libraryManga.filter { | ||||
|             val included = includedCategories.isEmpty() || it.categories.intersect(includedCategories).isNotEmpty() | ||||
|             val excluded = it.categories.intersect(excludedCategories).isNotEmpty() | ||||
|             included && !excluded | ||||
|         } | ||||
|             .fastCountNot { | ||||
|                 (MANGA_NON_COMPLETED in updateRestrictions && it.manga.status.toInt() == SManga.COMPLETED) || | ||||
|                     (MANGA_HAS_UNREAD in updateRestrictions && it.unreadCount != 0L) || | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| package mihon.core.common.utils | ||||
|  | ||||
| fun <T> Set<T>.mutate(action: (MutableSet<T>) -> Unit): Set<T> { | ||||
|     return toMutableSet().apply(action) | ||||
| } | ||||
| @@ -92,7 +92,7 @@ object MangaMapper { | ||||
|         chapterFetchedAt: Long, | ||||
|         lastRead: Long, | ||||
|         bookmarkCount: Double, | ||||
|         category: Long, | ||||
|         categories: String, | ||||
|     ): LibraryManga = LibraryManga( | ||||
|         manga = mapManga( | ||||
|             id, | ||||
| @@ -121,7 +121,7 @@ object MangaMapper { | ||||
|             isSyncing, | ||||
|             notes, | ||||
|         ), | ||||
|         category = category, | ||||
|         categories = categories.split(",").map { it.toLong() }, | ||||
|         totalChapters = totalCount, | ||||
|         readCount = readCount.toLong(), | ||||
|         bookmarkCount = bookmarkCount.toLong(), | ||||
|   | ||||
							
								
								
									
										39
									
								
								data/src/main/sqldelight/tachiyomi/migrations/6.sqm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								data/src/main/sqldelight/tachiyomi/migrations/6.sqm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| DROP VIEW IF EXISTS libraryView; | ||||
|  | ||||
| CREATE VIEW libraryView AS | ||||
| SELECT | ||||
|     M.*, | ||||
|     coalesce(C.total, 0) AS totalCount, | ||||
|     coalesce(C.readCount, 0) AS readCount, | ||||
|     coalesce(C.latestUpload, 0) AS latestUpload, | ||||
|     coalesce(C.fetchedAt, 0) AS chapterFetchedAt, | ||||
|     coalesce(C.lastRead, 0) AS lastRead, | ||||
|     coalesce(C.bookmarkCount, 0) AS bookmarkCount, | ||||
|     coalesce(MC.categories, '0') AS categories | ||||
| FROM mangas M | ||||
| LEFT JOIN ( | ||||
|     SELECT | ||||
|         chapters.manga_id, | ||||
|         count(*) AS total, | ||||
|         sum(read) AS readCount, | ||||
|         coalesce(max(chapters.date_upload), 0) AS latestUpload, | ||||
|         coalesce(max(history.last_read), 0) AS lastRead, | ||||
|         coalesce(max(chapters.date_fetch), 0) AS fetchedAt, | ||||
|         sum(chapters.bookmark) AS bookmarkCount | ||||
|     FROM chapters | ||||
|     LEFT JOIN excluded_scanlators | ||||
|     ON chapters.manga_id = excluded_scanlators.manga_id | ||||
|     AND chapters.scanlator = excluded_scanlators.scanlator | ||||
|     LEFT JOIN history | ||||
|     ON chapters._id = history.chapter_id | ||||
|     WHERE excluded_scanlators.scanlator IS NULL | ||||
|     GROUP BY chapters.manga_id | ||||
| ) AS C | ||||
| ON M._id = C.manga_id | ||||
| LEFT JOIN ( | ||||
|     SELECT manga_id, group_concat(category_id) AS categories | ||||
|     FROM mangas_categories | ||||
|     GROUP BY manga_id | ||||
| ) AS MC | ||||
| ON MC.manga_id = M._id | ||||
| WHERE M.favorite = 1; | ||||
| @@ -7,9 +7,9 @@ SELECT | ||||
|     coalesce(C.fetchedAt, 0) AS chapterFetchedAt, | ||||
|     coalesce(C.lastRead, 0) AS lastRead, | ||||
|     coalesce(C.bookmarkCount, 0) AS bookmarkCount, | ||||
|     coalesce(MC.category_id, 0) AS category | ||||
|     coalesce(MC.categories, '0') AS categories | ||||
| FROM mangas M | ||||
| LEFT JOIN( | ||||
| LEFT JOIN ( | ||||
|     SELECT | ||||
|         chapters.manga_id, | ||||
|         count(*) AS total, | ||||
| @@ -28,7 +28,11 @@ LEFT JOIN( | ||||
|     GROUP BY chapters.manga_id | ||||
| ) AS C | ||||
| ON M._id = C.manga_id | ||||
| LEFT JOIN mangas_categories AS MC | ||||
| LEFT JOIN ( | ||||
|     SELECT manga_id, group_concat(category_id) AS categories | ||||
|     FROM mangas_categories | ||||
|     GROUP BY manga_id | ||||
| ) AS MC | ||||
| ON MC.manga_id = M._id | ||||
| WHERE M.favorite = 1; | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import tachiyomi.domain.manga.model.Manga | ||||
|  | ||||
| data class LibraryManga( | ||||
|     val manga: Manga, | ||||
|     val category: Long, | ||||
|     val categories: List<Long>, | ||||
|     val totalChapters: Long, | ||||
|     val readCount: Long, | ||||
|     val bookmarkCount: Long, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user