mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Use Voyager on Library tab (#8620)
This commit is contained in:
		| @@ -49,6 +49,9 @@ object CommonMangaItemDefaults { | ||||
| } | ||||
|  | ||||
| private val ContinueReadingButtonSize = 32.dp | ||||
| private val ContinueReadingButtonGridPadding = 6.dp | ||||
| private val ContinueReadingButtonListSpacing = 8.dp | ||||
|  | ||||
| private const val GridSelectedCoverAlpha = 0.76f | ||||
|  | ||||
| /** | ||||
| @@ -61,9 +64,8 @@ fun MangaCompactGridItem( | ||||
|     title: String? = null, | ||||
|     coverData: eu.kanade.domain.manga.model.MangaCover, | ||||
|     coverAlpha: Float = 1f, | ||||
|     coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, | ||||
|     coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, | ||||
|     showContinueReadingButton: Boolean = false, | ||||
|     coverBadgeStart: @Composable (RowScope.() -> Unit)? = null, | ||||
|     coverBadgeEnd: @Composable (RowScope.() -> Unit)? = null, | ||||
|     onLongClick: () -> Unit, | ||||
|     onClick: () -> Unit, | ||||
|     onClickContinueReading: (() -> Unit)? = null, | ||||
| @@ -86,12 +88,17 @@ fun MangaCompactGridItem( | ||||
|             badgesEnd = coverBadgeEnd, | ||||
|             content = { | ||||
|                 if (title != null) { | ||||
|                     CoverTextOverlay(title = title, showContinueReadingButton) | ||||
|                 } | ||||
|             }, | ||||
|             continueReadingButton = { | ||||
|                 if (showContinueReadingButton && onClickContinueReading != null) { | ||||
|                     ContinueReadingButton(onClickContinueReading) | ||||
|                     CoverTextOverlay( | ||||
|                         title = title, | ||||
|                         onClickContinueReading = onClickContinueReading, | ||||
|                     ) | ||||
|                 } else if (onClickContinueReading != null) { | ||||
|                     ContinueReadingButton( | ||||
|                         modifier = Modifier | ||||
|                             .padding(ContinueReadingButtonGridPadding) | ||||
|                             .align(Alignment.BottomEnd), | ||||
|                         onClickContinueReading = onClickContinueReading, | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
| @@ -104,7 +111,7 @@ fun MangaCompactGridItem( | ||||
| @Composable | ||||
| private fun BoxScope.CoverTextOverlay( | ||||
|     title: String, | ||||
|     showContinueReadingButton: Boolean = false, | ||||
|     onClickContinueReading: (() -> Unit)? = null, | ||||
| ) { | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
| @@ -119,20 +126,33 @@ private fun BoxScope.CoverTextOverlay( | ||||
|             .fillMaxWidth() | ||||
|             .align(Alignment.BottomCenter), | ||||
|     ) | ||||
|     val endPadding = if (showContinueReadingButton) ContinueReadingButtonSize else 0.dp | ||||
|     GridItemTitle( | ||||
|         modifier = Modifier | ||||
|             .padding(start = 8.dp, top = 8.dp, end = endPadding + 8.dp, bottom = 8.dp) | ||||
|             .align(Alignment.BottomStart), | ||||
|         title = title, | ||||
|         style = MaterialTheme.typography.titleSmall.copy( | ||||
|             color = Color.White, | ||||
|             shadow = Shadow( | ||||
|                 color = Color.Black, | ||||
|                 blurRadius = 4f, | ||||
|     Row( | ||||
|         modifier = Modifier.align(Alignment.BottomStart), | ||||
|         verticalAlignment = Alignment.Bottom, | ||||
|     ) { | ||||
|         GridItemTitle( | ||||
|             modifier = Modifier | ||||
|                 .weight(1f) | ||||
|                 .padding(8.dp), | ||||
|             title = title, | ||||
|             style = MaterialTheme.typography.titleSmall.copy( | ||||
|                 color = Color.White, | ||||
|                 shadow = Shadow( | ||||
|                     color = Color.Black, | ||||
|                     blurRadius = 4f, | ||||
|                 ), | ||||
|             ), | ||||
|         ), | ||||
|     ) | ||||
|         ) | ||||
|         if (onClickContinueReading != null) { | ||||
|             ContinueReadingButton( | ||||
|                 modifier = Modifier.padding( | ||||
|                     end = ContinueReadingButtonGridPadding, | ||||
|                     bottom = ContinueReadingButtonGridPadding, | ||||
|                 ), | ||||
|                 onClickContinueReading = onClickContinueReading, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -146,7 +166,6 @@ fun MangaComfortableGridItem( | ||||
|     coverAlpha: Float = 1f, | ||||
|     coverBadgeStart: (@Composable RowScope.() -> Unit)? = null, | ||||
|     coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null, | ||||
|     showContinueReadingButton: Boolean = false, | ||||
|     onLongClick: () -> Unit, | ||||
|     onClick: () -> Unit, | ||||
|     onClickContinueReading: (() -> Unit)? = null, | ||||
| @@ -168,9 +187,14 @@ fun MangaComfortableGridItem( | ||||
|                 }, | ||||
|                 badgesStart = coverBadgeStart, | ||||
|                 badgesEnd = coverBadgeEnd, | ||||
|                 continueReadingButton = { | ||||
|                     if (showContinueReadingButton && onClickContinueReading != null) { | ||||
|                         ContinueReadingButton(onClickContinueReading) | ||||
|                 content = { | ||||
|                     if (onClickContinueReading != null) { | ||||
|                         ContinueReadingButton( | ||||
|                             modifier = Modifier | ||||
|                                 .padding(ContinueReadingButtonGridPadding) | ||||
|                                 .align(Alignment.BottomEnd), | ||||
|                             onClickContinueReading = onClickContinueReading, | ||||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
| @@ -192,7 +216,6 @@ private fun MangaGridCover( | ||||
|     cover: @Composable BoxScope.() -> Unit = {}, | ||||
|     badgesStart: (@Composable RowScope.() -> Unit)? = null, | ||||
|     badgesEnd: (@Composable RowScope.() -> Unit)? = null, | ||||
|     continueReadingButton: (@Composable BoxScope.() -> Unit)? = null, | ||||
|     content: @Composable (BoxScope.() -> Unit)? = null, | ||||
| ) { | ||||
|     Box( | ||||
| @@ -219,7 +242,6 @@ private fun MangaGridCover( | ||||
|                 content = badgesEnd, | ||||
|             ) | ||||
|         } | ||||
|         continueReadingButton?.invoke(this) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -310,8 +332,7 @@ fun MangaListItem( | ||||
|     title: String, | ||||
|     coverData: eu.kanade.domain.manga.model.MangaCover, | ||||
|     coverAlpha: Float = 1f, | ||||
|     badge: @Composable RowScope.() -> Unit, | ||||
|     showContinueReadingButton: Boolean = false, | ||||
|     badge: @Composable (RowScope.() -> Unit), | ||||
|     onLongClick: () -> Unit, | ||||
|     onClick: () -> Unit, | ||||
|     onClickContinueReading: (() -> Unit)? = null, | ||||
| @@ -343,23 +364,21 @@ fun MangaListItem( | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|         ) | ||||
|         BadgeGroup(content = badge) | ||||
|         if (showContinueReadingButton && onClickContinueReading != null) { | ||||
|             Box { | ||||
|                 ContinueReadingButton(onClickContinueReading) | ||||
|             } | ||||
|         if (onClickContinueReading != null) { | ||||
|             ContinueReadingButton( | ||||
|                 modifier = Modifier.padding(start = ContinueReadingButtonListSpacing), | ||||
|                 onClickContinueReading = onClickContinueReading, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun BoxScope.ContinueReadingButton( | ||||
| private fun ContinueReadingButton( | ||||
|     modifier: Modifier = Modifier, | ||||
|     onClickContinueReading: () -> Unit, | ||||
| ) { | ||||
|     Box( | ||||
|         modifier = Modifier | ||||
|             .align(Alignment.BottomEnd) | ||||
|             .padding(horizontal = 4.dp, vertical = 8.dp), | ||||
|     ) { | ||||
|     Box(modifier = modifier) { | ||||
|         FilledIconButton( | ||||
|             onClick = onClickContinueReading, | ||||
|             modifier = Modifier.size(ContinueReadingButtonSize), | ||||
|   | ||||
| @@ -1,130 +0,0 @@ | ||||
| package eu.kanade.presentation.library | ||||
|  | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.HelpOutline | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.util.fastAll | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.domain.library.model.LibraryManga | ||||
| import eu.kanade.domain.library.model.display | ||||
| import eu.kanade.domain.manga.model.isLocal | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.EmptyScreenAction | ||||
| import eu.kanade.presentation.components.LibraryBottomActionMenu | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.library.components.LibraryContent | ||||
| import eu.kanade.presentation.library.components.LibraryToolbar | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryPresenter | ||||
| import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView | ||||
|  | ||||
| @Composable | ||||
| fun LibraryScreen( | ||||
|     presenter: LibraryPresenter, | ||||
|     onMangaClicked: (Long) -> Unit, | ||||
|     onContinueReadingClicked: (LibraryManga) -> Unit, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     onChangeCategoryClicked: () -> Unit, | ||||
|     onMarkAsReadClicked: () -> Unit, | ||||
|     onMarkAsUnreadClicked: () -> Unit, | ||||
|     onDownloadClicked: (DownloadAction) -> Unit, | ||||
|     onDeleteClicked: () -> Unit, | ||||
|     onClickUnselectAll: () -> Unit, | ||||
|     onClickSelectAll: () -> Unit, | ||||
|     onClickInvertSelection: () -> Unit, | ||||
|     onClickFilter: () -> Unit, | ||||
|     onClickRefresh: (Category?) -> Boolean, | ||||
|     onClickOpenRandomManga: () -> Unit, | ||||
| ) { | ||||
|     val haptic = LocalHapticFeedback.current | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             val title by presenter.getToolbarTitle() | ||||
|             val tabVisible = presenter.tabVisibility && presenter.categories.size > 1 | ||||
|             LibraryToolbar( | ||||
|                 state = presenter, | ||||
|                 title = title, | ||||
|                 incognitoMode = !tabVisible && presenter.isIncognitoMode, | ||||
|                 downloadedOnlyMode = !tabVisible && presenter.isDownloadOnly, | ||||
|                 onClickUnselectAll = onClickUnselectAll, | ||||
|                 onClickSelectAll = onClickSelectAll, | ||||
|                 onClickInvertSelection = onClickInvertSelection, | ||||
|                 onClickFilter = onClickFilter, | ||||
|                 onClickRefresh = { onClickRefresh(null) }, | ||||
|                 onClickOpenRandomManga = onClickOpenRandomManga, | ||||
|                 scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab | ||||
|             ) | ||||
|         }, | ||||
|         bottomBar = { | ||||
|             LibraryBottomActionMenu( | ||||
|                 visible = presenter.selectionMode, | ||||
|                 onChangeCategoryClicked = onChangeCategoryClicked, | ||||
|                 onMarkAsReadClicked = onMarkAsReadClicked, | ||||
|                 onMarkAsUnreadClicked = onMarkAsUnreadClicked, | ||||
|                 onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.fastAll { !it.manga.isLocal() } }, | ||||
|                 onDeleteClicked = onDeleteClicked, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         if (presenter.isLoading) { | ||||
|             LoadingScreen() | ||||
|             return@Scaffold | ||||
|         } | ||||
|  | ||||
|         val contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(paddingValues) | ||||
|         if (presenter.searchQuery.isNullOrEmpty() && presenter.isLibraryEmpty) { | ||||
|             val handler = LocalUriHandler.current | ||||
|             EmptyScreen( | ||||
|                 textResource = R.string.information_empty_library, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|                 actions = listOf( | ||||
|                     EmptyScreenAction( | ||||
|                         stringResId = R.string.getting_started_guide, | ||||
|                         icon = Icons.Outlined.HelpOutline, | ||||
|                         onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") }, | ||||
|                     ), | ||||
|                 ), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|  | ||||
|         LibraryContent( | ||||
|             state = presenter, | ||||
|             contentPadding = contentPadding, | ||||
|             currentPage = { presenter.activeCategory }, | ||||
|             isLibraryEmpty = presenter.isLibraryEmpty, | ||||
|             showPageTabs = presenter.tabVisibility, | ||||
|             showMangaCount = presenter.mangaCountVisibility, | ||||
|             onChangeCurrentPage = { presenter.activeCategory = it }, | ||||
|             onMangaClicked = onMangaClicked, | ||||
|             onContinueReadingClicked = onContinueReadingClicked, | ||||
|             onToggleSelection = { presenter.toggleSelection(it) }, | ||||
|             onToggleRangeSelection = { | ||||
|                 presenter.toggleRangeSelection(it) | ||||
|                 haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|             }, | ||||
|             onRefresh = onClickRefresh, | ||||
|             onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|             getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) }, | ||||
|             getDisplayModeForPage = { presenter.categories[it].display }, | ||||
|             getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) }, | ||||
|             getLibraryForPage = { presenter.getMangaForCategory(page = it) }, | ||||
|             showDownloadBadges = presenter.showDownloadBadges, | ||||
|             showUnreadBadges = presenter.showUnreadBadges, | ||||
|             showLocalBadges = presenter.showLocalBadges, | ||||
|             showLanguageBadges = presenter.showLanguageBadges, | ||||
|             showContinueReadingButton = presenter.showContinueReadingButton, | ||||
|             isIncognitoMode = presenter.isIncognitoMode, | ||||
|             isDownloadOnly = presenter.isDownloadOnly, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.kanade.presentation.library | ||||
|  | ||||
| import androidx.compose.runtime.Stable | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.setValue | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.domain.library.model.LibraryManga | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryPresenter | ||||
|  | ||||
| @Stable | ||||
| interface LibraryState { | ||||
|     val isLoading: Boolean | ||||
|     val categories: List<Category> | ||||
|     var searchQuery: String? | ||||
|     val selection: List<LibraryManga> | ||||
|     val selectionMode: Boolean | ||||
|     var hasActiveFilters: Boolean | ||||
|     var dialog: LibraryPresenter.Dialog? | ||||
| } | ||||
|  | ||||
| fun LibraryState(): LibraryState { | ||||
|     return LibraryStateImpl() | ||||
| } | ||||
|  | ||||
| class LibraryStateImpl : LibraryState { | ||||
|     override var isLoading: Boolean by mutableStateOf(true) | ||||
|     override var categories: List<Category> by mutableStateOf(emptyList()) | ||||
|     override var searchQuery: String? by mutableStateOf(null) | ||||
|     override var selection: List<LibraryManga> by mutableStateOf(emptyList()) | ||||
|     override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() } | ||||
|     override var hasActiveFilters: Boolean by mutableStateOf(false) | ||||
|     override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null) | ||||
| } | ||||
| @@ -5,16 +5,12 @@ import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.Badge | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
|  | ||||
| @Composable | ||||
| fun DownloadsBadge( | ||||
|     enabled: Boolean, | ||||
|     item: LibraryItem, | ||||
| ) { | ||||
|     if (enabled && item.downloadCount > 0) { | ||||
| fun DownloadsBadge(count: Int) { | ||||
|     if (count > 0) { | ||||
|         Badge( | ||||
|             text = "${item.downloadCount}", | ||||
|             text = "$count", | ||||
|             color = MaterialTheme.colorScheme.tertiary, | ||||
|             textColor = MaterialTheme.colorScheme.onTertiary, | ||||
|         ) | ||||
| @@ -22,30 +18,26 @@ fun DownloadsBadge( | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun UnreadBadge( | ||||
|     enabled: Boolean, | ||||
|     item: LibraryItem, | ||||
| ) { | ||||
|     if (enabled && item.unreadCount > 0) { | ||||
|         Badge(text = "${item.unreadCount}") | ||||
| fun UnreadBadge(count: Int) { | ||||
|     if (count > 0) { | ||||
|         Badge(text = "$count") | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| fun LanguageBadge( | ||||
|     showLanguage: Boolean, | ||||
|     showLocal: Boolean, | ||||
|     item: LibraryItem, | ||||
|     isLocal: Boolean, | ||||
|     sourceLanguage: String, | ||||
| ) { | ||||
|     if (showLocal && item.isLocal) { | ||||
|     if (isLocal) { | ||||
|         Badge( | ||||
|             text = stringResource(R.string.local_source_badge), | ||||
|             color = MaterialTheme.colorScheme.tertiary, | ||||
|             textColor = MaterialTheme.colorScheme.onTertiary, | ||||
|         ) | ||||
|     } else if (showLanguage && item.sourceLanguage.isNotEmpty()) { | ||||
|     } else if (sourceLanguage.isNotEmpty()) { | ||||
|         Badge( | ||||
|             text = item.sourceLanguage.uppercase(), | ||||
|             text = sourceLanguage.uppercase(), | ||||
|             color = MaterialTheme.colorScheme.tertiary, | ||||
|             textColor = MaterialTheme.colorScheme.onTertiary, | ||||
|         ) | ||||
|   | ||||
| @@ -14,17 +14,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| @Composable | ||||
| fun LibraryComfortableGrid( | ||||
|     items: List<LibraryItem>, | ||||
|     showDownloadBadges: Boolean, | ||||
|     showUnreadBadges: Boolean, | ||||
|     showLocalBadges: Boolean, | ||||
|     showLanguageBadges: Boolean, | ||||
|     showContinueReadingButton: Boolean, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
|     searchQuery: String?, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
| ) { | ||||
| @@ -51,26 +46,22 @@ fun LibraryComfortableGrid( | ||||
|                     lastModified = manga.coverLastModified, | ||||
|                 ), | ||||
|                 coverBadgeStart = { | ||||
|                     DownloadsBadge( | ||||
|                         enabled = showDownloadBadges, | ||||
|                         item = libraryItem, | ||||
|                     ) | ||||
|                     UnreadBadge( | ||||
|                         enabled = showUnreadBadges, | ||||
|                         item = libraryItem, | ||||
|                     ) | ||||
|                     DownloadsBadge(count = libraryItem.downloadCount.toInt()) | ||||
|                     UnreadBadge(count = libraryItem.unreadCount.toInt()) | ||||
|                 }, | ||||
|                 coverBadgeEnd = { | ||||
|                     LanguageBadge( | ||||
|                         showLanguage = showLanguageBadges, | ||||
|                         showLocal = showLocalBadges, | ||||
|                         item = libraryItem, | ||||
|                         isLocal = libraryItem.isLocal, | ||||
|                         sourceLanguage = libraryItem.sourceLanguage, | ||||
|                     ) | ||||
|                 }, | ||||
|                 showContinueReadingButton = showContinueReadingButton, | ||||
|                 onLongClick = { onLongClick(libraryItem.libraryManga) }, | ||||
|                 onClick = { onClick(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = if (onClickContinueReading != null) { | ||||
|                     { onClickContinueReading(libraryItem.libraryManga) } | ||||
|                 } else { | ||||
|                     null | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -15,17 +15,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| fun LibraryCompactGrid( | ||||
|     items: List<LibraryItem>, | ||||
|     showTitle: Boolean, | ||||
|     showDownloadBadges: Boolean, | ||||
|     showUnreadBadges: Boolean, | ||||
|     showLocalBadges: Boolean, | ||||
|     showLanguageBadges: Boolean, | ||||
|     showContinueReadingButton: Boolean, | ||||
|     columns: Int, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
|     searchQuery: String?, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
| ) { | ||||
| @@ -52,26 +47,22 @@ fun LibraryCompactGrid( | ||||
|                     lastModified = manga.coverLastModified, | ||||
|                 ), | ||||
|                 coverBadgeStart = { | ||||
|                     DownloadsBadge( | ||||
|                         enabled = showDownloadBadges, | ||||
|                         item = libraryItem, | ||||
|                     ) | ||||
|                     UnreadBadge( | ||||
|                         enabled = showUnreadBadges, | ||||
|                         item = libraryItem, | ||||
|                     ) | ||||
|                     DownloadsBadge(count = libraryItem.downloadCount.toInt()) | ||||
|                     UnreadBadge(count = libraryItem.unreadCount.toInt()) | ||||
|                 }, | ||||
|                 coverBadgeEnd = { | ||||
|                     LanguageBadge( | ||||
|                         showLanguage = showLanguageBadges, | ||||
|                         showLocal = showLocalBadges, | ||||
|                         item = libraryItem, | ||||
|                         isLocal = libraryItem.isLocal, | ||||
|                         sourceLanguage = libraryItem.sourceLanguage, | ||||
|                     ) | ||||
|                 }, | ||||
|                 showContinueReadingButton = showContinueReadingButton, | ||||
|                 onLongClick = { onLongClick(libraryItem.libraryManga) }, | ||||
|                 onClick = { onClick(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = if (onClickContinueReading != null) { | ||||
|                     { onClickContinueReading(libraryItem.libraryManga) } | ||||
|                 } else { | ||||
|                     null | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.calculateStartPadding | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| @@ -21,7 +20,6 @@ import eu.kanade.domain.library.model.LibraryDisplayMode | ||||
| import eu.kanade.domain.library.model.LibraryManga | ||||
| import eu.kanade.presentation.components.SwipeRefresh | ||||
| import eu.kanade.presentation.components.rememberPagerState | ||||
| import eu.kanade.presentation.library.LibraryState | ||||
| import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.launch | ||||
| @@ -29,28 +27,24 @@ import kotlin.time.Duration.Companion.seconds | ||||
|  | ||||
| @Composable | ||||
| fun LibraryContent( | ||||
|     state: LibraryState, | ||||
|     categories: List<Category>, | ||||
|     searchQuery: String?, | ||||
|     selection: List<LibraryManga>, | ||||
|     contentPadding: PaddingValues, | ||||
|     currentPage: () -> Int, | ||||
|     isLibraryEmpty: Boolean, | ||||
|     showPageTabs: Boolean, | ||||
|     showMangaCount: Boolean, | ||||
|     onChangeCurrentPage: (Int) -> Unit, | ||||
|     onMangaClicked: (Long) -> Unit, | ||||
|     onContinueReadingClicked: (LibraryManga) -> Unit, | ||||
|     onContinueReadingClicked: ((LibraryManga) -> Unit)?, | ||||
|     onToggleSelection: (LibraryManga) -> Unit, | ||||
|     onToggleRangeSelection: (LibraryManga) -> Unit, | ||||
|     onRefresh: (Category?) -> Boolean, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getLibraryForPage: @Composable (Int) -> List<LibraryItem>, | ||||
|     showDownloadBadges: Boolean, | ||||
|     showUnreadBadges: Boolean, | ||||
|     showLocalBadges: Boolean, | ||||
|     showLanguageBadges: Boolean, | ||||
|     showContinueReadingButton: Boolean, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
|     isDownloadOnly: Boolean, | ||||
|     isIncognitoMode: Boolean, | ||||
| ) { | ||||
| @@ -61,38 +55,30 @@ fun LibraryContent( | ||||
|             end = contentPadding.calculateEndPadding(LocalLayoutDirection.current), | ||||
|         ), | ||||
|     ) { | ||||
|         val categories = state.categories | ||||
|         val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) } | ||||
|         val pagerState = rememberPagerState(coercedCurrentPage) | ||||
|  | ||||
|         val scope = rememberCoroutineScope() | ||||
|         var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) } | ||||
|  | ||||
|         if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) { | ||||
|         if (!isLibraryEmpty && showPageTabs && categories.size > 1) { | ||||
|             LibraryTabs( | ||||
|                 categories = categories, | ||||
|                 currentPageIndex = pagerState.currentPage, | ||||
|                 showMangaCount = showMangaCount, | ||||
|                 getNumberOfMangaForCategory = getNumberOfMangaForCategory, | ||||
|                 isDownloadOnly = isDownloadOnly, | ||||
|                 isIncognitoMode = isIncognitoMode, | ||||
|                 onTabItemClick = { scope.launch { pagerState.animateScrollToPage(it) } }, | ||||
|             ) | ||||
|                 getNumberOfMangaForCategory = getNumberOfMangaForCategory, | ||||
|             ) { scope.launch { pagerState.animateScrollToPage(it) } } | ||||
|         } | ||||
|  | ||||
|         val notSelectionMode = selection.isEmpty() | ||||
|         val onClickManga = { manga: LibraryManga -> | ||||
|             if (state.selectionMode.not()) { | ||||
|             if (notSelectionMode) { | ||||
|                 onMangaClicked(manga.manga.id) | ||||
|             } else { | ||||
|                 onToggleSelection(manga) | ||||
|             } | ||||
|         } | ||||
|         val onLongClickManga = { manga: LibraryManga -> | ||||
|             onToggleRangeSelection(manga) | ||||
|         } | ||||
|         val onClickContinueReading = { manga: LibraryManga -> | ||||
|             onContinueReadingClicked(manga) | ||||
|         } | ||||
|  | ||||
|         SwipeRefresh( | ||||
|             refreshing = isRefreshing, | ||||
| @@ -106,26 +92,21 @@ fun LibraryContent( | ||||
|                     isRefreshing = false | ||||
|                 } | ||||
|             }, | ||||
|             enabled = state.selectionMode.not(), | ||||
|             enabled = notSelectionMode, | ||||
|         ) { | ||||
|             LibraryPager( | ||||
|                 state = pagerState, | ||||
|                 contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()), | ||||
|                 pageCount = categories.size, | ||||
|                 selectedManga = state.selection, | ||||
|                 selectedManga = selection, | ||||
|                 searchQuery = searchQuery, | ||||
|                 onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|                 getDisplayModeForPage = getDisplayModeForPage, | ||||
|                 getColumnsForOrientation = getColumnsForOrientation, | ||||
|                 getLibraryForPage = getLibraryForPage, | ||||
|                 showDownloadBadges = showDownloadBadges, | ||||
|                 showUnreadBadges = showUnreadBadges, | ||||
|                 showLocalBadges = showLocalBadges, | ||||
|                 showLanguageBadges = showLanguageBadges, | ||||
|                 showContinueReadingButton = showContinueReadingButton, | ||||
|                 onClickManga = onClickManga, | ||||
|                 onLongClickManga = onLongClickManga, | ||||
|                 onClickContinueReading = onClickContinueReading, | ||||
|                 onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|                 searchQuery = state.searchQuery, | ||||
|                 onLongClickManga = onToggleRangeSelection, | ||||
|                 onClickContinueReading = onContinueReadingClicked, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -23,16 +23,11 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem | ||||
| @Composable | ||||
| fun LibraryList( | ||||
|     items: List<LibraryItem>, | ||||
|     showDownloadBadges: Boolean, | ||||
|     showUnreadBadges: Boolean, | ||||
|     showLocalBadges: Boolean, | ||||
|     showLanguageBadges: Boolean, | ||||
|     showContinueReadingButton: Boolean, | ||||
|     contentPadding: PaddingValues, | ||||
|     selection: List<LibraryManga>, | ||||
|     onClick: (LibraryManga) -> Unit, | ||||
|     onLongClick: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
|     searchQuery: String?, | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
| ) { | ||||
| @@ -41,13 +36,13 @@ fun LibraryList( | ||||
|         contentPadding = contentPadding + PaddingValues(vertical = 8.dp), | ||||
|     ) { | ||||
|         item { | ||||
|             if (searchQuery.isNullOrEmpty().not()) { | ||||
|             if (!searchQuery.isNullOrEmpty()) { | ||||
|                 TextButton( | ||||
|                     modifier = Modifier.fillMaxWidth(), | ||||
|                     onClick = onGlobalSearchClicked, | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = stringResource(R.string.action_global_search_query, searchQuery!!), | ||||
|                         text = stringResource(R.string.action_global_search_query, searchQuery), | ||||
|                         modifier = Modifier.zIndex(99f), | ||||
|                     ) | ||||
|                 } | ||||
| @@ -70,14 +65,20 @@ fun LibraryList( | ||||
|                     lastModified = manga.coverLastModified, | ||||
|                 ), | ||||
|                 badge = { | ||||
|                     DownloadsBadge(enabled = showDownloadBadges, item = libraryItem) | ||||
|                     UnreadBadge(enabled = showUnreadBadges, item = libraryItem) | ||||
|                     LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem) | ||||
|                     DownloadsBadge(count = libraryItem.downloadCount.toInt()) | ||||
|                     UnreadBadge(count = libraryItem.unreadCount.toInt()) | ||||
|                     LanguageBadge( | ||||
|                         isLocal = libraryItem.isLocal, | ||||
|                         sourceLanguage = libraryItem.sourceLanguage, | ||||
|                     ) | ||||
|                 }, | ||||
|                 showContinueReadingButton = showContinueReadingButton, | ||||
|                 onLongClick = { onLongClick(libraryItem.libraryManga) }, | ||||
|                 onClick = { onClick(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) }, | ||||
|                 onClickContinueReading = if (onClickContinueReading != null) { | ||||
|                     { onClickContinueReading(libraryItem.libraryManga) } | ||||
|                 } else { | ||||
|                     null | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -27,15 +27,10 @@ fun LibraryPager( | ||||
|     onGlobalSearchClicked: () -> Unit, | ||||
|     getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode, | ||||
|     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>, | ||||
|     getLibraryForPage: @Composable (Int) -> List<LibraryItem>, | ||||
|     showDownloadBadges: Boolean, | ||||
|     showUnreadBadges: Boolean, | ||||
|     showLocalBadges: Boolean, | ||||
|     showLanguageBadges: Boolean, | ||||
|     showContinueReadingButton: Boolean, | ||||
|     getLibraryForPage: (Int) -> List<LibraryItem>, | ||||
|     onClickManga: (LibraryManga) -> Unit, | ||||
|     onLongClickManga: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: (LibraryManga) -> Unit, | ||||
|     onClickContinueReading: ((LibraryManga) -> Unit)?, | ||||
| ) { | ||||
|     HorizontalPager( | ||||
|         count = pageCount, | ||||
| @@ -62,11 +57,6 @@ fun LibraryPager( | ||||
|             LibraryDisplayMode.List -> { | ||||
|                 LibraryList( | ||||
|                     items = library, | ||||
|                     showDownloadBadges = showDownloadBadges, | ||||
|                     showUnreadBadges = showUnreadBadges, | ||||
|                     showLocalBadges = showLocalBadges, | ||||
|                     showLanguageBadges = showLanguageBadges, | ||||
|                     showContinueReadingButton = showContinueReadingButton, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
|                     onClick = onClickManga, | ||||
| @@ -80,11 +70,6 @@ fun LibraryPager( | ||||
|                 LibraryCompactGrid( | ||||
|                     items = library, | ||||
|                     showTitle = displayMode is LibraryDisplayMode.CompactGrid, | ||||
|                     showDownloadBadges = showDownloadBadges, | ||||
|                     showUnreadBadges = showUnreadBadges, | ||||
|                     showLocalBadges = showLocalBadges, | ||||
|                     showLanguageBadges = showLanguageBadges, | ||||
|                     showContinueReadingButton = showContinueReadingButton, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
| @@ -98,17 +83,12 @@ fun LibraryPager( | ||||
|             LibraryDisplayMode.ComfortableGrid -> { | ||||
|                 LibraryComfortableGrid( | ||||
|                     items = library, | ||||
|                     showDownloadBadges = showDownloadBadges, | ||||
|                     showUnreadBadges = showUnreadBadges, | ||||
|                     showLocalBadges = showLocalBadges, | ||||
|                     showLanguageBadges = showLanguageBadges, | ||||
|                     showContinueReadingButton = showContinueReadingButton, | ||||
|                     columns = columns, | ||||
|                     contentPadding = contentPadding, | ||||
|                     selection = selectedManga, | ||||
|                     onClick = onClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
|                     onLongClick = onLongClickManga, | ||||
|                     onClickContinueReading = onClickContinueReading, | ||||
|                     searchQuery = searchQuery, | ||||
|                     onGlobalSearchClicked = onGlobalSearchClicked, | ||||
|                 ) | ||||
|   | ||||
| @@ -5,8 +5,6 @@ import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.ScrollableTabRow | ||||
| import androidx.compose.material3.Tab | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.presentation.category.visualName | ||||
| @@ -19,10 +17,9 @@ import eu.kanade.presentation.components.TabText | ||||
| fun LibraryTabs( | ||||
|     categories: List<Category>, | ||||
|     currentPageIndex: Int, | ||||
|     showMangaCount: Boolean, | ||||
|     isDownloadOnly: Boolean, | ||||
|     isIncognitoMode: Boolean, | ||||
|     getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>, | ||||
|     getNumberOfMangaForCategory: (Category) -> Int?, | ||||
|     onTabItemClick: (Int) -> Unit, | ||||
| ) { | ||||
|     Column { | ||||
| @@ -41,11 +38,7 @@ fun LibraryTabs( | ||||
|                     text = { | ||||
|                         TabText( | ||||
|                             text = category.visualName, | ||||
|                             badgeCount = if (showMangaCount) { | ||||
|                                 getNumberOfMangaForCategory(category.id) | ||||
|                             } else { | ||||
|                                 null | ||||
|                             }?.value, | ||||
|                             badgeCount = getNumberOfMangaForCategory(category), | ||||
|                         ) | ||||
|                     }, | ||||
|                     unselectedContentColor = MaterialTheme.colorScheme.onSurface, | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TopAppBarScrollBehavior | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.res.stringResource | ||||
| @@ -23,13 +24,13 @@ import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.OverflowMenu | ||||
| import eu.kanade.presentation.components.Pill | ||||
| import eu.kanade.presentation.components.SearchToolbar | ||||
| import eu.kanade.presentation.library.LibraryState | ||||
| import eu.kanade.presentation.theme.active | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| @Composable | ||||
| fun LibraryToolbar( | ||||
|     state: LibraryState, | ||||
|     hasActiveFilters: Boolean, | ||||
|     selectedCount: Int, | ||||
|     title: LibraryToolbarTitle, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
| @@ -39,10 +40,12 @@ fun LibraryToolbar( | ||||
|     onClickFilter: () -> Unit, | ||||
|     onClickRefresh: () -> Unit, | ||||
|     onClickOpenRandomManga: () -> Unit, | ||||
|     searchQuery: String?, | ||||
|     onSearchQueryChange: (String?) -> Unit, | ||||
|     scrollBehavior: TopAppBarScrollBehavior?, | ||||
| ) = when { | ||||
|     state.selectionMode -> LibrarySelectionToolbar( | ||||
|         state = state, | ||||
|     selectedCount > 0 -> LibrarySelectionToolbar( | ||||
|         selectedCount = selectedCount, | ||||
|         incognitoMode = incognitoMode, | ||||
|         downloadedOnlyMode = downloadedOnlyMode, | ||||
|         onClickUnselectAll = onClickUnselectAll, | ||||
| @@ -51,11 +54,11 @@ fun LibraryToolbar( | ||||
|     ) | ||||
|     else -> LibraryRegularToolbar( | ||||
|         title = title, | ||||
|         hasFilters = state.hasActiveFilters, | ||||
|         hasFilters = hasActiveFilters, | ||||
|         incognitoMode = incognitoMode, | ||||
|         downloadedOnlyMode = downloadedOnlyMode, | ||||
|         searchQuery = state.searchQuery, | ||||
|         onChangeSearchQuery = { state.searchQuery = it }, | ||||
|         searchQuery = searchQuery, | ||||
|         onSearchQueryChange = onSearchQueryChange, | ||||
|         onClickFilter = onClickFilter, | ||||
|         onClickRefresh = onClickRefresh, | ||||
|         onClickOpenRandomManga = onClickOpenRandomManga, | ||||
| @@ -70,7 +73,7 @@ fun LibraryRegularToolbar( | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
|     searchQuery: String?, | ||||
|     onChangeSearchQuery: (String?) -> Unit, | ||||
|     onSearchQueryChange: (String?) -> Unit, | ||||
|     onClickFilter: () -> Unit, | ||||
|     onClickRefresh: () -> Unit, | ||||
|     onClickOpenRandomManga: () -> Unit, | ||||
| @@ -96,7 +99,7 @@ fun LibraryRegularToolbar( | ||||
|             } | ||||
|         }, | ||||
|         searchQuery = searchQuery, | ||||
|         onChangeSearchQuery = onChangeSearchQuery, | ||||
|         onChangeSearchQuery = onSearchQueryChange, | ||||
|         actions = { | ||||
|             val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current | ||||
|             IconButton(onClick = onClickFilter) { | ||||
| @@ -128,7 +131,7 @@ fun LibraryRegularToolbar( | ||||
|  | ||||
| @Composable | ||||
| fun LibrarySelectionToolbar( | ||||
|     state: LibraryState, | ||||
|     selectedCount: Int, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
|     onClickUnselectAll: () -> Unit, | ||||
| @@ -136,7 +139,7 @@ fun LibrarySelectionToolbar( | ||||
|     onClickInvertSelection: () -> Unit, | ||||
| ) { | ||||
|     AppBar( | ||||
|         titleContent = { Text(text = "${state.selection.size}") }, | ||||
|         titleContent = { Text(text = "$selectedCount") }, | ||||
|         actions = { | ||||
|             IconButton(onClick = onClickSelectAll) { | ||||
|                 Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all)) | ||||
| @@ -152,6 +155,7 @@ fun LibrarySelectionToolbar( | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Immutable | ||||
| data class LibraryToolbarTitle( | ||||
|     val text: String, | ||||
|     val numberOfManga: Int? = null, | ||||
|   | ||||
| @@ -1,167 +1,37 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.View | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import com.bluelinelabs.conductor.ControllerChangeHandler | ||||
| import com.bluelinelabs.conductor.ControllerChangeType | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.domain.chapter.model.Chapter | ||||
| import eu.kanade.domain.library.model.LibraryManga | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.domain.manga.model.isLocal | ||||
| import eu.kanade.presentation.components.ChangeCategoryDialog | ||||
| import eu.kanade.presentation.components.DeleteLibraryMangaDialog | ||||
| import eu.kanade.presentation.library.LibraryScreen | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import cafe.adriel.voyager.navigator.Navigator | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.cancel | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| class LibraryController( | ||||
|     bundle: Bundle? = null, | ||||
| ) : FullComposeController<LibraryPresenter>(bundle), RootController { | ||||
| ) : BasicFullComposeController(bundle), RootController { | ||||
|  | ||||
|     /** | ||||
|      * Sheet containing filter/sort/display items. | ||||
|      */ | ||||
|     private var settingsSheet: LibrarySettingsSheet? = null | ||||
|  | ||||
|     override fun createPresenter(): LibraryPresenter = LibraryPresenter() | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         val context = LocalContext.current | ||||
|         val getMangaForCategory = presenter.getMangaForCategory(page = presenter.activeCategory) | ||||
|  | ||||
|         LibraryScreen( | ||||
|             presenter = presenter, | ||||
|             onMangaClicked = ::openManga, | ||||
|             onContinueReadingClicked = ::continueReading, | ||||
|             onGlobalSearchClicked = { | ||||
|                 router.pushController(GlobalSearchController(presenter.searchQuery)) | ||||
|             }, | ||||
|             onChangeCategoryClicked = ::showMangaCategoriesDialog, | ||||
|             onMarkAsReadClicked = { markReadStatus(true) }, | ||||
|             onMarkAsUnreadClicked = { markReadStatus(false) }, | ||||
|             onDownloadClicked = ::runDownloadChapterAction, | ||||
|             onDeleteClicked = ::showDeleteMangaDialog, | ||||
|             onClickFilter = ::showSettingsSheet, | ||||
|             onClickRefresh = { | ||||
|                 val started = LibraryUpdateService.start(context, it) | ||||
|                 context.toast(if (started) R.string.updating_category else R.string.update_already_running) | ||||
|                 started | ||||
|             }, | ||||
|             onClickOpenRandomManga = { | ||||
|                 val items = getMangaForCategory.map { it.libraryManga.manga.id } | ||||
|                 if (getMangaForCategory.isNotEmpty()) { | ||||
|                     openManga(items.random()) | ||||
|                 } else { | ||||
|                     context.toast(R.string.information_no_entries_found) | ||||
|                 } | ||||
|             }, | ||||
|             onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) }, | ||||
|             onClickSelectAll = { presenter.selectAll(presenter.activeCategory) }, | ||||
|             onClickUnselectAll = ::clearSelection, | ||||
|         ) | ||||
|  | ||||
|         val onDismissRequest = { presenter.dialog = null } | ||||
|         when (val dialog = presenter.dialog) { | ||||
|             is LibraryPresenter.Dialog.ChangeCategory -> { | ||||
|                 ChangeCategoryDialog( | ||||
|                     initialSelection = dialog.initialSelection, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onEditCategories = { | ||||
|                         presenter.clearSelection() | ||||
|                         router.pushController(CategoryController()) | ||||
|                     }, | ||||
|                     onConfirm = { include, exclude -> | ||||
|                         presenter.clearSelection() | ||||
|                         presenter.setMangaCategories(dialog.manga, include, exclude) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryPresenter.Dialog.DeleteManga -> { | ||||
|                 DeleteLibraryMangaDialog( | ||||
|                     containsLocalManga = dialog.manga.any(Manga::isLocal), | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onConfirm = { deleteManga, deleteChapter -> | ||||
|                         presenter.removeMangas(dialog.manga, deleteManga, deleteChapter) | ||||
|                         presenter.clearSelection() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryPresenter.Dialog.DownloadCustomAmount -> { | ||||
|                 DownloadCustomAmountDialog( | ||||
|                     maxAmount = dialog.max, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onConfirm = { amount -> | ||||
|                         presenter.downloadUnreadChapters(dialog.manga, amount) | ||||
|                         presenter.clearSelection() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             null -> {} | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(presenter.selectionMode) { | ||||
|             // Could perhaps be removed when navigation is in a Compose world | ||||
|             if (router.backstackSize == 1) { | ||||
|                 (activity as? MainActivity)?.showBottomNav(presenter.selectionMode.not()) | ||||
|             } | ||||
|         } | ||||
|         LaunchedEffect(presenter.isLoading) { | ||||
|             if (!presenter.isLoading) { | ||||
|                 (activity as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun handleBack(): Boolean { | ||||
|         return when { | ||||
|             presenter.selection.isNotEmpty() -> { | ||||
|                 presenter.clearSelection() | ||||
|                 true | ||||
|             } | ||||
|             presenter.searchQuery != null -> { | ||||
|                 presenter.searchQuery = null | ||||
|                 true | ||||
|             } | ||||
|             else -> false | ||||
|         } | ||||
|         Navigator(screen = LibraryScreen) | ||||
|     } | ||||
|  | ||||
|     override fun onViewCreated(view: View) { | ||||
|         super.onViewCreated(view) | ||||
|  | ||||
|         settingsSheet = LibrarySettingsSheet(router) { group -> | ||||
|             when (group) { | ||||
|                 is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() | ||||
|                 else -> {} // Handled via different mechanisms | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { | ||||
|         super.onChangeStarted(handler, type) | ||||
|         if (type.isEnter) { | ||||
|             presenter.subscribeLibrary() | ||||
|         settingsSheet = LibrarySettingsSheet(router) | ||||
|         viewScope.launch { | ||||
|             LibraryScreen.openSettingsSheetEvent | ||||
|                 .collectLatest(::showSettingsSheet) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -171,111 +41,13 @@ class LibraryController( | ||||
|         super.onDestroyView(view) | ||||
|     } | ||||
|  | ||||
|     fun showSettingsSheet() { | ||||
|         presenter.categories.getOrNull(presenter.activeCategory)?.let { category -> | ||||
|     fun showSettingsSheet(category: Category? = null) { | ||||
|         if (category != null) { | ||||
|             settingsSheet?.show(category) | ||||
|         } else { | ||||
|             viewScope.launch { LibraryScreen.requestOpenSettingsSheet() } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun onFilterChanged() { | ||||
|         viewScope.launchUI { | ||||
|             presenter.requestFilterUpdate() | ||||
|             activity?.invalidateOptionsMenu() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun search(query: String) { | ||||
|         presenter.searchQuery = query | ||||
|     } | ||||
|  | ||||
|     override fun onPrepareOptionsMenu(menu: Menu) { | ||||
|         val settingsSheet = settingsSheet ?: return | ||||
|         presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters() | ||||
|     } | ||||
|  | ||||
|     private fun openManga(mangaId: Long) { | ||||
|         presenter.onOpenManga() | ||||
|         router.pushController(MangaController(mangaId)) | ||||
|     } | ||||
|  | ||||
|     private fun continueReading(libraryManga: LibraryManga) { | ||||
|         viewScope.launchIO { | ||||
|             val chapter = presenter.getNextUnreadChapter(libraryManga.manga) | ||||
|             if (chapter != null) { | ||||
|                 openChapter(chapter) | ||||
|             } else { | ||||
|                 withUIContext { activity?.toast(R.string.no_next_chapter) } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun openChapter(chapter: Chapter) { | ||||
|         activity?.run { | ||||
|             startActivity(ReaderActivity.newIntent(this, chapter.mangaId, chapter.id)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Clear all of the manga currently selected, and | ||||
|      * invalidate the action mode to revert the top toolbar | ||||
|      */ | ||||
|     private fun clearSelection() { | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Move the selected manga to a list of categories. | ||||
|      */ | ||||
|     private fun showMangaCategoriesDialog() { | ||||
|         viewScope.launchIO { | ||||
|             // Create a copy of selected manga | ||||
|             val mangaList = presenter.selection.map { it.manga } | ||||
|  | ||||
|             // Hide the default category because it has a different behavior than the ones from db. | ||||
|             val categories = presenter.categories.filter { it.id != 0L } | ||||
|  | ||||
|             // Get indexes of the common categories to preselect. | ||||
|             val common = presenter.getCommonCategories(mangaList) | ||||
|             // Get indexes of the mix categories to preselect. | ||||
|             val mix = presenter.getMixCategories(mangaList) | ||||
|             val preselected = categories.map { | ||||
|                 when (it) { | ||||
|                     in common -> CheckboxState.State.Checked(it) | ||||
|                     in mix -> CheckboxState.TriState.Exclude(it) | ||||
|                     else -> CheckboxState.State.None(it) | ||||
|                 } | ||||
|             } | ||||
|             presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun runDownloadChapterAction(action: DownloadAction) { | ||||
|         val mangas = presenter.selection.map { it.manga }.toList() | ||||
|         when (action) { | ||||
|             DownloadAction.NEXT_1_CHAPTER -> presenter.downloadUnreadChapters(mangas, 1) | ||||
|             DownloadAction.NEXT_5_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 5) | ||||
|             DownloadAction.NEXT_10_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 10) | ||||
|             DownloadAction.UNREAD_CHAPTERS -> presenter.downloadUnreadChapters(mangas, null) | ||||
|             DownloadAction.CUSTOM -> { | ||||
|                 presenter.dialog = LibraryPresenter.Dialog.DownloadCustomAmount( | ||||
|                     mangas, | ||||
|                     presenter.selection.maxOf { it.unreadCount }.toInt(), | ||||
|                 ) | ||||
|                 return | ||||
|             } | ||||
|             else -> {} | ||||
|         } | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     private fun markReadStatus(read: Boolean) { | ||||
|         val mangaList = presenter.selection.toList() | ||||
|         presenter.markReadStatus(mangaList.map { it.manga }, read) | ||||
|         presenter.clearSelection() | ||||
|     } | ||||
|  | ||||
|     private fun showDeleteMangaDialog() { | ||||
|         val mangaList = presenter.selection.map { it.manga } | ||||
|         presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList) | ||||
|     } | ||||
|     fun search(query: String) = LibraryScreen.search(query) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,270 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
|  | ||||
| import androidx.activity.compose.BackHandler | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.HelpOutline | ||||
| import androidx.compose.material3.ScaffoldDefaults | ||||
| import androidx.compose.material3.SnackbarHost | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.rememberCoroutineScope | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.hapticfeedback.HapticFeedbackType | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalHapticFeedback | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.util.fastAll | ||||
| import cafe.adriel.voyager.core.model.rememberScreenModel | ||||
| import cafe.adriel.voyager.core.screen.Screen | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import com.bluelinelabs.conductor.Router | ||||
| import eu.kanade.domain.category.model.Category | ||||
| import eu.kanade.domain.library.model.LibraryManga | ||||
| import eu.kanade.domain.library.model.display | ||||
| import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.domain.manga.model.isLocal | ||||
| import eu.kanade.presentation.components.ChangeCategoryDialog | ||||
| import eu.kanade.presentation.components.DeleteLibraryMangaDialog | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.EmptyScreenAction | ||||
| import eu.kanade.presentation.components.LibraryBottomActionMenu | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.library.components.LibraryContent | ||||
| import eu.kanade.presentation.library.components.LibraryToolbar | ||||
| import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog | ||||
| import eu.kanade.presentation.util.LocalRouter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController | ||||
| import eu.kanade.tachiyomi.ui.category.CategoryController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView | ||||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||||
| import kotlinx.coroutines.flow.asSharedFlow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.launch | ||||
|  | ||||
| object LibraryScreen : Screen { | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         val router = LocalRouter.currentOrThrow | ||||
|         val context = LocalContext.current | ||||
|         val scope = rememberCoroutineScope() | ||||
|         val haptic = LocalHapticFeedback.current | ||||
|  | ||||
|         val screenModel = rememberScreenModel { LibraryScreenModel() } | ||||
|         val state by screenModel.state.collectAsState() | ||||
|  | ||||
|         val snackbarHostState = remember { SnackbarHostState() } | ||||
|  | ||||
|         val onClickRefresh: (Category?) -> Boolean = { | ||||
|             val started = LibraryUpdateService.start(context, it) | ||||
|             scope.launch { | ||||
|                 val msgRes = if (started) R.string.updating_category else R.string.update_already_running | ||||
|                 snackbarHostState.showSnackbar(context.getString(msgRes)) | ||||
|             } | ||||
|             started | ||||
|         } | ||||
|         val onClickFilter: () -> Unit = { | ||||
|             scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategory]) } | ||||
|         } | ||||
|  | ||||
|         Scaffold( | ||||
|             topBar = { scrollBehavior -> | ||||
|                 val title = state.getToolbarTitle( | ||||
|                     defaultTitle = stringResource(R.string.label_library), | ||||
|                     defaultCategoryTitle = stringResource(R.string.label_default), | ||||
|                     page = screenModel.activeCategory, | ||||
|                 ) | ||||
|                 val tabVisible = state.showCategoryTabs && state.categories.size > 1 | ||||
|                 LibraryToolbar( | ||||
|                     hasActiveFilters = state.hasActiveFilters, | ||||
|                     selectedCount = state.selection.size, | ||||
|                     title = title, | ||||
|                     incognitoMode = !tabVisible && screenModel.isIncognitoMode, | ||||
|                     downloadedOnlyMode = !tabVisible && screenModel.isDownloadOnly, | ||||
|                     onClickUnselectAll = screenModel::clearSelection, | ||||
|                     onClickSelectAll = { screenModel.selectAll(screenModel.activeCategory) }, | ||||
|                     onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategory) }, | ||||
|                     onClickFilter = onClickFilter, | ||||
|                     onClickRefresh = { onClickRefresh(null) }, | ||||
|                     onClickOpenRandomManga = { | ||||
|                         scope.launch { | ||||
|                             val randomItem = screenModel.getRandomLibraryItemForCurrentCategory() | ||||
|                             if (randomItem != null) { | ||||
|                                 router.openManga(randomItem.libraryManga.manga.id) | ||||
|                             } else { | ||||
|                                 snackbarHostState.showSnackbar(context.getString(R.string.information_no_entries_found)) | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     searchQuery = state.searchQuery, | ||||
|                     onSearchQueryChange = screenModel::search, | ||||
|                     scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab | ||||
|                 ) | ||||
|             }, | ||||
|             bottomBar = { | ||||
|                 LibraryBottomActionMenu( | ||||
|                     visible = state.selectionMode, | ||||
|                     onChangeCategoryClicked = screenModel::openChangeCategoryDialog, | ||||
|                     onMarkAsReadClicked = { screenModel.markReadSelection(true) }, | ||||
|                     onMarkAsUnreadClicked = { screenModel.markReadSelection(false) }, | ||||
|                     onDownloadClicked = screenModel::runDownloadActionSelection | ||||
|                         .takeIf { state.selection.fastAll { !it.manga.isLocal() } }, | ||||
|                     onDeleteClicked = screenModel::openDeleteMangaDialog, | ||||
|                 ) | ||||
|             }, | ||||
|             snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|             contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets), | ||||
|         ) { contentPadding -> | ||||
|             if (state.isLoading) { | ||||
|                 LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|                 return@Scaffold | ||||
|             } | ||||
|  | ||||
|             if (state.searchQuery.isNullOrEmpty() && state.library.isEmpty()) { | ||||
|                 val handler = LocalUriHandler.current | ||||
|                 EmptyScreen( | ||||
|                     textResource = R.string.information_empty_library, | ||||
|                     modifier = Modifier.padding(contentPadding), | ||||
|                     actions = listOf( | ||||
|                         EmptyScreenAction( | ||||
|                             stringResId = R.string.getting_started_guide, | ||||
|                             icon = Icons.Outlined.HelpOutline, | ||||
|                             onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") }, | ||||
|                         ), | ||||
|                     ), | ||||
|                 ) | ||||
|                 return@Scaffold | ||||
|             } | ||||
|  | ||||
|             LibraryContent( | ||||
|                 categories = state.categories, | ||||
|                 searchQuery = state.searchQuery, | ||||
|                 selection = state.selection, | ||||
|                 contentPadding = contentPadding, | ||||
|                 currentPage = { screenModel.activeCategory }, | ||||
|                 isLibraryEmpty = state.library.isEmpty(), | ||||
|                 showPageTabs = state.showCategoryTabs, | ||||
|                 onChangeCurrentPage = { screenModel.activeCategory = it }, | ||||
|                 onMangaClicked = { router.openManga(it) }, | ||||
|                 onContinueReadingClicked = { it: LibraryManga -> | ||||
|                     scope.launchIO { | ||||
|                         val chapter = screenModel.getNextUnreadChapter(it.manga) | ||||
|                         if (chapter != null) { | ||||
|                             context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id)) | ||||
|                         } else { | ||||
|                             snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter)) | ||||
|                         } | ||||
|                     } | ||||
|                     Unit | ||||
|                 }.takeIf { state.showMangaContinueButton }, | ||||
|                 onToggleSelection = { screenModel.toggleSelection(it) }, | ||||
|                 onToggleRangeSelection = { | ||||
|                     screenModel.toggleRangeSelection(it) | ||||
|                     haptic.performHapticFeedback(HapticFeedbackType.LongPress) | ||||
|                 }, | ||||
|                 onRefresh = onClickRefresh, | ||||
|                 onGlobalSearchClicked = { | ||||
|                     router.pushController(GlobalSearchController(screenModel.state.value.searchQuery)) | ||||
|                 }, | ||||
|                 getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) }, | ||||
|                 getDisplayModeForPage = { state.categories[it].display }, | ||||
|                 getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) }, | ||||
|                 getLibraryForPage = { state.getLibraryItemsByPage(it) }, | ||||
|                 isDownloadOnly = screenModel.isDownloadOnly, | ||||
|                 isIncognitoMode = screenModel.isIncognitoMode, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         val onDismissRequest = screenModel::closeDialog | ||||
|         when (val dialog = state.dialog) { | ||||
|             is LibraryScreenModel.Dialog.ChangeCategory -> { | ||||
|                 ChangeCategoryDialog( | ||||
|                     initialSelection = dialog.initialSelection, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onEditCategories = { | ||||
|                         screenModel.clearSelection() | ||||
|                         router.pushController(CategoryController()) | ||||
|                     }, | ||||
|                     onConfirm = { include, exclude -> | ||||
|                         screenModel.clearSelection() | ||||
|                         screenModel.setMangaCategories(dialog.manga, include, exclude) | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryScreenModel.Dialog.DeleteManga -> { | ||||
|                 DeleteLibraryMangaDialog( | ||||
|                     containsLocalManga = dialog.manga.any(Manga::isLocal), | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onConfirm = { deleteManga, deleteChapter -> | ||||
|                         screenModel.removeMangas(dialog.manga, deleteManga, deleteChapter) | ||||
|                         screenModel.clearSelection() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is LibraryScreenModel.Dialog.DownloadCustomAmount -> { | ||||
|                 DownloadCustomAmountDialog( | ||||
|                     maxAmount = dialog.max, | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onConfirm = { amount -> | ||||
|                         screenModel.downloadUnreadChapters(dialog.manga, amount) | ||||
|                         screenModel.clearSelection() | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             null -> {} | ||||
|         } | ||||
|  | ||||
|         BackHandler(enabled = state.selectionMode || state.searchQuery != null) { | ||||
|             when { | ||||
|                 state.selectionMode -> screenModel.clearSelection() | ||||
|                 state.searchQuery != null -> screenModel.search(null) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(state.selectionMode) { | ||||
|             // Could perhaps be removed when navigation is in a Compose world | ||||
|             if (router.backstackSize == 1) { | ||||
|                 (context as? MainActivity)?.showBottomNav(!state.selectionMode) | ||||
|             } | ||||
|         } | ||||
|         LaunchedEffect(state.isLoading) { | ||||
|             if (!state.isLoading) { | ||||
|                 (context as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(Unit) { | ||||
|             launch { queryEvent.collectLatest(screenModel::search) } | ||||
|             launch { requestSettingsSheetEvent.collectLatest { onClickFilter() } } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun Router.openManga(mangaId: Long) { | ||||
|         pushController(MangaController(mangaId)) | ||||
|     } | ||||
|  | ||||
|     // For invoking search from other screen | ||||
|     private val queryEvent = MutableSharedFlow<String>(replay = 1) | ||||
|     fun search(query: String) = queryEvent.tryEmit(query) | ||||
|  | ||||
|     // For opening settings sheet in LibraryController | ||||
|     private val requestSettingsSheetEvent = MutableSharedFlow<Unit>() | ||||
|     private val openSettingsSheetEvent_ = MutableSharedFlow<Category>() | ||||
|     val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow() | ||||
|     private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category) | ||||
|     suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit) | ||||
| } | ||||
| @@ -1,18 +1,15 @@ | ||||
| package eu.kanade.tachiyomi.ui.library | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
| import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.produceState | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import androidx.compose.ui.util.fastAny | ||||
| import androidx.compose.ui.util.fastMap | ||||
| import cafe.adriel.voyager.core.model.StateScreenModel | ||||
| import cafe.adriel.voyager.core.model.coroutineScope | ||||
| import eu.kanade.core.prefs.CheckboxState | ||||
| import eu.kanade.core.prefs.PreferenceMutableState | ||||
| import eu.kanade.core.prefs.asState | ||||
| import eu.kanade.core.util.fastFilter | ||||
| import eu.kanade.core.util.fastFilterNot | ||||
| import eu.kanade.core.util.fastMapNotNull | ||||
| @@ -35,11 +32,8 @@ import eu.kanade.domain.manga.model.Manga | ||||
| import eu.kanade.domain.manga.model.MangaUpdate | ||||
| import eu.kanade.domain.manga.model.isLocal | ||||
| import eu.kanade.domain.track.interactor.GetTracksPerManga | ||||
| import eu.kanade.presentation.category.visualName | ||||
| import eu.kanade.presentation.library.LibraryState | ||||
| import eu.kanade.presentation.library.LibraryStateImpl | ||||
| import eu.kanade.presentation.library.components.LibraryToolbarTitle | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.presentation.manga.DownloadAction | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadCache | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| @@ -47,38 +41,33 @@ import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.chapter.getNextUnread | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.launchNonCancellable | ||||
| import eu.kanade.tachiyomi.util.lang.withIOContext | ||||
| import eu.kanade.tachiyomi.util.removeCovers | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.onStart | ||||
| import kotlinx.coroutines.flow.receiveAsFlow | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.first | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.update | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.text.Collator | ||||
| import java.util.Collections | ||||
| import java.util.Locale | ||||
| 
 | ||||
| /** | ||||
|  * Class containing library information. | ||||
|  */ | ||||
| private data class Library(val categories: List<Category>, val mangaMap: LibraryMap) | ||||
| 
 | ||||
| /** | ||||
|  * Typealias for the library manga, using the category as keys, and list of manga as values. | ||||
|  */ | ||||
| typealias LibraryMap = Map<Long, List<LibraryItem>> | ||||
| typealias LibraryMap = Map<Category, List<LibraryItem>> | ||||
| 
 | ||||
| class LibraryPresenter( | ||||
|     private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl, | ||||
| class LibraryScreenModel( | ||||
|     private val getLibraryManga: GetLibraryManga = Injekt.get(), | ||||
|     private val getCategories: GetCategories = Injekt.get(), | ||||
|     private val getTracksPerManga: GetTracksPerManga = Injekt.get(), | ||||
| @@ -94,90 +83,114 @@ class LibraryPresenter( | ||||
|     private val downloadManager: DownloadManager = Injekt.get(), | ||||
|     private val downloadCache: DownloadCache = Injekt.get(), | ||||
|     private val trackManager: TrackManager = Injekt.get(), | ||||
| ) : BasePresenter<LibraryController>(), LibraryState by state { | ||||
| ) : StateScreenModel<LibraryScreenModel.State>(State()) { | ||||
| 
 | ||||
|     private var loadedManga by mutableStateOf(emptyMap<Long, List<LibraryItem>>()) | ||||
|     // This is active category INDEX NUMBER | ||||
|     var activeCategory: Int by libraryPreferences.lastUsedCategory().asState(coroutineScope) | ||||
| 
 | ||||
|     val isLibraryEmpty by derivedStateOf { loadedManga.isEmpty() } | ||||
|     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope) | ||||
|     val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope) | ||||
| 
 | ||||
|     val tabVisibility by libraryPreferences.categoryTabs().asState() | ||||
|     val mangaCountVisibility by libraryPreferences.categoryNumberOfItems().asState() | ||||
| 
 | ||||
|     val showDownloadBadges by libraryPreferences.downloadBadge().asState() | ||||
|     val showUnreadBadges by libraryPreferences.unreadBadge().asState() | ||||
|     val showLocalBadges by libraryPreferences.localBadge().asState() | ||||
|     val showLanguageBadges by libraryPreferences.languageBadge().asState() | ||||
| 
 | ||||
|     var activeCategory: Int by libraryPreferences.lastUsedCategory().asState() | ||||
| 
 | ||||
|     val showContinueReadingButton by libraryPreferences.showContinueReadingButton().asState() | ||||
| 
 | ||||
|     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() | ||||
|     val isIncognitoMode: Boolean by preferences.incognitoMode().asState() | ||||
| 
 | ||||
|     private val _filterChanges: Channel<Unit> = Channel(Int.MAX_VALUE) | ||||
|     private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) } | ||||
| 
 | ||||
|     private var librarySubscription: Job? = null | ||||
| 
 | ||||
|     override fun onCreate(savedState: Bundle?) { | ||||
|         super.onCreate(savedState) | ||||
| 
 | ||||
|         subscribeLibrary() | ||||
|     } | ||||
| 
 | ||||
|     fun subscribeLibrary() { | ||||
|         /** | ||||
|          * TODO: | ||||
|          * - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library | ||||
|          * - Create new db view and new query to just fetch the current category save as needed to instance variable | ||||
|          * - Fetch badges to maps and retrieve as needed instead of fetching all of them at once | ||||
|          */ | ||||
|         if (librarySubscription == null || librarySubscription!!.isCancelled) { | ||||
|             librarySubscription = presenterScope.launchIO { | ||||
|                 combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ -> | ||||
|                     library.mangaMap | ||||
|                         .applyFilters(tracks) | ||||
|                         .applySort(library.categories) | ||||
|                 } | ||||
|                     .collectLatest { | ||||
|                         state.isLoading = false | ||||
|                         loadedManga = it | ||||
|     init { | ||||
|         coroutineScope.launchIO { | ||||
|             combine( | ||||
|                 state.map { it.searchQuery }.distinctUntilChanged(), | ||||
|                 getLibraryFlow(), | ||||
|                 getTracksPerManga.subscribe(), | ||||
|                 getTrackingFilterFlow(), | ||||
|             ) { searchQuery, library, tracks, loggedInTrackServices -> | ||||
|                 library | ||||
|                     .applyFilters(tracks, loggedInTrackServices) | ||||
|                     .applySort() | ||||
|                     .mapValues { (_, value) -> | ||||
|                         if (searchQuery != null) { | ||||
|                             // Filter query | ||||
|                             value.filter { it.matches(searchQuery) } | ||||
|                         } else { | ||||
|                             // Don't do anything | ||||
|                             value | ||||
|                         } | ||||
|                     } | ||||
|             } | ||||
|                 .collectLatest { | ||||
|                     mutableState.update { state -> | ||||
|                         state.copy( | ||||
|                             isLoading = false, | ||||
|                             library = it, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
| 
 | ||||
|         combine( | ||||
|             libraryPreferences.categoryTabs().changes(), | ||||
|             libraryPreferences.categoryNumberOfItems().changes(), | ||||
|             libraryPreferences.showContinueReadingButton().changes(), | ||||
|         ) { a, b, c -> arrayOf(a, b, c) } | ||||
|             .onEach { (showCategoryTabs, showMangaCount, showMangaContinueButton) -> | ||||
|                 mutableState.update { state -> | ||||
|                     state.copy( | ||||
|                         showCategoryTabs = showCategoryTabs, | ||||
|                         showMangaCount = showMangaCount, | ||||
|                         showMangaContinueButton = showMangaContinueButton, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(coroutineScope) | ||||
| 
 | ||||
|         combine( | ||||
|             getLibraryItemPreferencesFlow(), | ||||
|             getTrackingFilterFlow(), | ||||
|         ) { prefs, trackFilter -> | ||||
|             val a = ( | ||||
|                 prefs.filterDownloaded or | ||||
|                     prefs.filterUnread or | ||||
|                     prefs.filterStarted or | ||||
|                     prefs.filterBookmarked or | ||||
|                     prefs.filterCompleted | ||||
|                 ) != TriStateGroup.State.IGNORE.value | ||||
|             val b = trackFilter.values.any { it != TriStateGroup.State.IGNORE.value } | ||||
|             a || b | ||||
|         } | ||||
|             .distinctUntilChanged() | ||||
|             .onEach { | ||||
|                 mutableState.update { state -> | ||||
|                     state.copy(hasActiveFilters = it) | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(coroutineScope) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Applies library filters to the given map of manga. | ||||
|      */ | ||||
|     private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap { | ||||
|         val downloadedOnly = preferences.downloadedOnly().get() | ||||
|         val filterDownloaded = libraryPreferences.filterDownloaded().get() | ||||
|         val filterUnread = libraryPreferences.filterUnread().get() | ||||
|         val filterStarted = libraryPreferences.filterStarted().get() | ||||
|         val filterBookmarked = libraryPreferences.filterBookmarked().get() | ||||
|         val filterCompleted = libraryPreferences.filterCompleted().get() | ||||
|     private suspend fun LibraryMap.applyFilters( | ||||
|         trackMap: Map<Long, List<Long>>, | ||||
|         loggedInTrackServices: Map<Long, Int>, | ||||
|     ): LibraryMap { | ||||
|         val prefs = getLibraryItemPreferencesFlow().first() | ||||
|         val downloadedOnly = prefs.globalFilterDownloaded | ||||
|         val filterDownloaded = prefs.filterDownloaded | ||||
|         val filterUnread = prefs.filterUnread | ||||
|         val filterStarted = prefs.filterStarted | ||||
|         val filterBookmarked = prefs.filterBookmarked | ||||
|         val filterCompleted = prefs.filterCompleted | ||||
| 
 | ||||
|         val loggedInTrackServices = trackManager.services.fastFilter { trackService -> trackService.isLogged } | ||||
|             .associate { trackService -> | ||||
|                 trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get() | ||||
|             } | ||||
|         val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty() | ||||
| 
 | ||||
|         val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null } | ||||
|         val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null } | ||||
|         val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.EXCLUDE.value) it.key else null } | ||||
|         val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.INCLUDE.value) it.key else null } | ||||
|         val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty() | ||||
| 
 | ||||
|         val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item -> | ||||
|             if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true | ||||
|             if (!downloadedOnly && filterDownloaded == TriStateGroup.State.IGNORE.value) return@downloaded true | ||||
|             val isDownloaded = when { | ||||
|                 item.libraryManga.manga.isLocal() -> true | ||||
|                 item.downloadCount != -1L -> item.downloadCount > 0 | ||||
|                 else -> downloadManager.getDownloadCount(item.libraryManga.manga) > 0 | ||||
|             } | ||||
| 
 | ||||
|             return@downloaded if (downloadedOnly || filterDownloaded == State.INCLUDE.value) { | ||||
|             return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.INCLUDE.value) { | ||||
|                 isDownloaded | ||||
|             } else { | ||||
|                 !isDownloaded | ||||
| @@ -185,10 +198,10 @@ class LibraryPresenter( | ||||
|         } | ||||
| 
 | ||||
|         val filterFnUnread: (LibraryItem) -> Boolean = unread@{ item -> | ||||
|             if (filterUnread == State.IGNORE.value) return@unread true | ||||
|             if (filterUnread == TriStateGroup.State.IGNORE.value) return@unread true | ||||
|             val isUnread = item.libraryManga.unreadCount > 0 | ||||
| 
 | ||||
|             return@unread if (filterUnread == State.INCLUDE.value) { | ||||
|             return@unread if (filterUnread == TriStateGroup.State.INCLUDE.value) { | ||||
|                 isUnread | ||||
|             } else { | ||||
|                 !isUnread | ||||
| @@ -196,10 +209,10 @@ class LibraryPresenter( | ||||
|         } | ||||
| 
 | ||||
|         val filterFnStarted: (LibraryItem) -> Boolean = started@{ item -> | ||||
|             if (filterStarted == State.IGNORE.value) return@started true | ||||
|             if (filterStarted == TriStateGroup.State.IGNORE.value) return@started true | ||||
|             val hasStarted = item.libraryManga.hasStarted | ||||
| 
 | ||||
|             return@started if (filterStarted == State.INCLUDE.value) { | ||||
|             return@started if (filterStarted == TriStateGroup.State.INCLUDE.value) { | ||||
|                 hasStarted | ||||
|             } else { | ||||
|                 !hasStarted | ||||
| @@ -207,11 +220,11 @@ class LibraryPresenter( | ||||
|         } | ||||
| 
 | ||||
|         val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{ item -> | ||||
|             if (filterBookmarked == State.IGNORE.value) return@bookmarked true | ||||
|             if (filterBookmarked == TriStateGroup.State.IGNORE.value) return@bookmarked true | ||||
| 
 | ||||
|             val hasBookmarks = item.libraryManga.hasBookmarks | ||||
| 
 | ||||
|             return@bookmarked if (filterBookmarked == State.INCLUDE.value) { | ||||
|             return@bookmarked if (filterBookmarked == TriStateGroup.State.INCLUDE.value) { | ||||
|                 hasBookmarks | ||||
|             } else { | ||||
|                 !hasBookmarks | ||||
| @@ -219,10 +232,10 @@ class LibraryPresenter( | ||||
|         } | ||||
| 
 | ||||
|         val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item -> | ||||
|             if (filterCompleted == State.IGNORE.value) return@completed true | ||||
|             if (filterCompleted == TriStateGroup.State.IGNORE.value) return@completed true | ||||
|             val isCompleted = item.libraryManga.manga.status.toInt() == SManga.COMPLETED | ||||
| 
 | ||||
|             return@completed if (filterCompleted == State.INCLUDE.value) { | ||||
|             return@completed if (filterCompleted == TriStateGroup.State.INCLUDE.value) { | ||||
|                 isCompleted | ||||
|             } else { | ||||
|                 !isCompleted | ||||
| @@ -266,9 +279,7 @@ class LibraryPresenter( | ||||
|     /** | ||||
|      * Applies library sorting to the given map of manga. | ||||
|      */ | ||||
|     private fun LibraryMap.applySort(categories: List<Category>): LibraryMap { | ||||
|         val sortModes = categories.associate { it.id to it.sort } | ||||
| 
 | ||||
|     private fun LibraryMap.applySort(): LibraryMap { | ||||
|         val locale = Locale.getDefault() | ||||
|         val collator = Collator.getInstance(locale).apply { | ||||
|             strength = Collator.PRIMARY | ||||
| @@ -278,7 +289,7 @@ class LibraryPresenter( | ||||
|         } | ||||
| 
 | ||||
|         val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> | ||||
|             val sort = sortModes[i1.libraryManga.category]!! | ||||
|             val sort = keys.find { it.id == i1.libraryManga.category }!!.sort | ||||
|             when (sort.type) { | ||||
|                 LibrarySort.Type.Alphabetical -> { | ||||
|                     sortAlphabetically(i1, i2) | ||||
| @@ -308,12 +319,11 @@ class LibraryPresenter( | ||||
|                 LibrarySort.Type.DateAdded -> { | ||||
|                     i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded) | ||||
|                 } | ||||
|                 else -> throw IllegalStateException("Invalid SortModeSetting: ${sort.type}") | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this.mapValues { entry -> | ||||
|             val comparator = if (sortModes[entry.key]!!.isAscending) { | ||||
|             val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) { | ||||
|                 Comparator(sortFn) | ||||
|             } else { | ||||
|                 Collections.reverseOrder(sortFn) | ||||
| @@ -323,24 +333,52 @@ class LibraryPresenter( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getLibraryItemPreferencesFlow(): Flow<ItemPreferences> { | ||||
|         return combine( | ||||
|             libraryPreferences.downloadBadge().changes(), | ||||
|             libraryPreferences.unreadBadge().changes(), | ||||
|             libraryPreferences.localBadge().changes(), | ||||
|             libraryPreferences.languageBadge().changes(), | ||||
| 
 | ||||
|             preferences.downloadedOnly().changes(), | ||||
|             libraryPreferences.filterDownloaded().changes(), | ||||
|             libraryPreferences.filterUnread().changes(), | ||||
|             libraryPreferences.filterStarted().changes(), | ||||
|             libraryPreferences.filterBookmarked().changes(), | ||||
|             libraryPreferences.filterCompleted().changes(), | ||||
|             transform = { | ||||
|                 ItemPreferences( | ||||
|                     downloadBadge = it[0] as Boolean, | ||||
|                     unreadBadge = it[1] as Boolean, | ||||
|                     localBadge = it[2] as Boolean, | ||||
|                     languageBadge = it[3] as Boolean, | ||||
|                     globalFilterDownloaded = it[4] as Boolean, | ||||
|                     filterDownloaded = it[5] as Int, | ||||
|                     filterUnread = it[6] as Int, | ||||
|                     filterStarted = it[7] as Int, | ||||
|                     filterBookmarked = it[8] as Int, | ||||
|                     filterCompleted = it[9] as Int, | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the categories and all its manga from the database. | ||||
|      * | ||||
|      * @return an observable of the categories and its manga. | ||||
|      */ | ||||
|     private fun getLibraryFlow(): Flow<Library> { | ||||
|     private fun getLibraryFlow(): Flow<LibraryMap> { | ||||
|         val libraryMangasFlow = combine( | ||||
|             getLibraryManga.subscribe(), | ||||
|             libraryPreferences.downloadBadge().changes(), | ||||
|             libraryPreferences.filterDownloaded().changes(), | ||||
|             preferences.downloadedOnly().changes(), | ||||
|             getLibraryItemPreferencesFlow(), | ||||
|             downloadCache.changes, | ||||
|         ) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ -> | ||||
|         ) { libraryMangaList, prefs, _ -> | ||||
|             libraryMangaList | ||||
|                 .map { libraryManga -> | ||||
|                     val needsDownloadCounts = downloadBadgePref || | ||||
|                         filterDownloadedPref != State.IGNORE.value || | ||||
|                         downloadedOnly | ||||
|                     val needsDownloadCounts = prefs.downloadBadge || | ||||
|                         prefs.filterDownloaded != TriStateGroup.State.IGNORE.value || | ||||
|                         prefs.globalFilterDownloaded | ||||
| 
 | ||||
|                     // Display mode based on user preference: take it from global library setting or category | ||||
|                     LibraryItem(libraryManga).apply { | ||||
| @@ -349,39 +387,44 @@ class LibraryPresenter( | ||||
|                         } else { | ||||
|                             0 | ||||
|                         } | ||||
|                         unreadCount = libraryManga.unreadCount | ||||
|                         isLocal = libraryManga.manga.isLocal() | ||||
|                         sourceLanguage = sourceManager.getOrStub(libraryManga.manga.source).lang | ||||
|                         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).not()) { | ||||
|             val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) { | ||||
|                 categories.fastFilterNot { it.isSystemCategory } | ||||
|             } else { | ||||
|                 categories | ||||
|             } | ||||
| 
 | ||||
|             state.categories = displayCategories | ||||
|             Library(categories, libraryManga) | ||||
|             displayCategories.associateWith { libraryManga[it.id] ?: emptyList() } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Requests the library to be filtered. | ||||
|      * Flow of tracking filter preferences | ||||
|      * | ||||
|      * @return map of track id with the filter value | ||||
|      */ | ||||
|     suspend fun requestFilterUpdate() = withIOContext { | ||||
|         _filterChanges.send(Unit) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called when a manga is opened. | ||||
|      */ | ||||
|     fun onOpenManga() { | ||||
|         // Avoid further db updates for the library when it's not needed | ||||
|         librarySubscription?.cancel() | ||||
|     private fun getTrackingFilterFlow(): Flow<Map<Long, Int>> { | ||||
|         val loggedServices = trackManager.services.filter { it.isLogged } | ||||
|         val a = loggedServices | ||||
|             .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } | ||||
|             .toTypedArray() | ||||
|         return combine(*a) { | ||||
|             loggedServices | ||||
|                 .mapIndexed { index, trackService -> trackService.id to it[index] } | ||||
|                 .toMap() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -389,7 +432,7 @@ class LibraryPresenter( | ||||
|      * | ||||
|      * @param mangas the list of manga. | ||||
|      */ | ||||
|     suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> { | ||||
|     private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> { | ||||
|         if (mangas.isEmpty()) return emptyList() | ||||
|         return mangas | ||||
|             .map { getCategories.await(it.id).toSet() } | ||||
| @@ -405,13 +448,37 @@ class LibraryPresenter( | ||||
|      * | ||||
|      * @param mangas the list of manga. | ||||
|      */ | ||||
|     suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> { | ||||
|     private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> { | ||||
|         if (mangas.isEmpty()) return emptyList() | ||||
|         val mangaCategories = mangas.map { getCategories.await(it.id).toSet() } | ||||
|         val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) } | ||||
|         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.UNREAD_CHAPTERS -> downloadUnreadChapters(mangas, null) | ||||
|             DownloadAction.CUSTOM -> { | ||||
|                 mutableState.update { state -> | ||||
|                     state.copy( | ||||
|                         dialog = Dialog.DownloadCustomAmount( | ||||
|                             mangas, | ||||
|                             selection.maxOf { it.unreadCount }.toInt(), | ||||
|                         ), | ||||
|                     ) | ||||
|                 } | ||||
|                 return | ||||
|             } | ||||
|             else -> {} | ||||
|         } | ||||
|         clearSelection() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Queues the amount specified of unread chapters from the list of mangas given. | ||||
|      * | ||||
| @@ -419,7 +486,7 @@ class LibraryPresenter( | ||||
|      * @param amount the amount to queue or null to queue all | ||||
|      */ | ||||
|     fun downloadUnreadChapters(mangas: List<Manga>, amount: Int?) { | ||||
|         presenterScope.launchNonCancellable { | ||||
|         coroutineScope.launchNonCancellable { | ||||
|             mangas.forEach { manga -> | ||||
|                 val chapters = getNextChapters.await(manga.id) | ||||
|                     .fastFilterNot { chapter -> | ||||
| @@ -440,18 +507,18 @@ class LibraryPresenter( | ||||
| 
 | ||||
|     /** | ||||
|      * Marks mangas' chapters read status. | ||||
|      * | ||||
|      * @param mangas the list of manga. | ||||
|      */ | ||||
|     fun markReadStatus(mangas: List<Manga>, read: Boolean) { | ||||
|         presenterScope.launchNonCancellable { | ||||
|     fun markReadSelection(read: Boolean) { | ||||
|         val mangas = state.value.selection.toList() | ||||
|         coroutineScope.launchNonCancellable { | ||||
|             mangas.forEach { manga -> | ||||
|                 setReadStatus.await( | ||||
|                     manga = manga, | ||||
|                     manga = manga.manga, | ||||
|                     read = read, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|         clearSelection() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -462,7 +529,7 @@ class LibraryPresenter( | ||||
|      * @param deleteChapters whether to delete downloaded chapters. | ||||
|      */ | ||||
|     fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) { | ||||
|         presenterScope.launchNonCancellable { | ||||
|         coroutineScope.launchNonCancellable { | ||||
|             val mangaToDelete = mangaList.distinctBy { it.id } | ||||
| 
 | ||||
|             if (deleteFromLibrary) { | ||||
| @@ -495,7 +562,7 @@ class LibraryPresenter( | ||||
|      * @param removeCategories the categories to remove in all mangas. | ||||
|      */ | ||||
|     fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) { | ||||
|         presenterScope.launchNonCancellable { | ||||
|         coroutineScope.launchNonCancellable { | ||||
|             mangaList.forEach { manga -> | ||||
|                 val categoryIds = getCategories.await(manga.id) | ||||
|                     .map { it.id } | ||||
| @@ -508,148 +575,215 @@ class LibraryPresenter( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State<Int?> { | ||||
|         return produceState<Int?>(initialValue = null, loadedManga) { | ||||
|             value = loadedManga[categoryId]?.size | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> { | ||||
|         return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState() | ||||
|         return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState(coroutineScope) | ||||
|     } | ||||
| 
 | ||||
|     // TODO: This is good but should we separate title from count or get categories with count from db | ||||
|     @Composable | ||||
|     fun getToolbarTitle(): androidx.compose.runtime.State<LibraryToolbarTitle> { | ||||
|         val category = categories.getOrNull(activeCategory) | ||||
| 
 | ||||
|         val defaultTitle = stringResource(R.string.label_library) | ||||
|         val categoryName = category?.visualName ?: defaultTitle | ||||
| 
 | ||||
|         val default = remember { LibraryToolbarTitle(defaultTitle) } | ||||
| 
 | ||||
|         return produceState(initialValue = default, category, loadedManga, mangaCountVisibility, tabVisibility) { | ||||
|             val title = if (tabVisibility.not()) categoryName else defaultTitle | ||||
|             val count = when { | ||||
|                 category == null || mangaCountVisibility.not() -> null | ||||
|                 tabVisibility.not() -> loadedManga[category.id]?.size | ||||
|                 else -> loadedManga.values.flatten().distinctBy { it.libraryManga.manga.id }.size | ||||
|             } | ||||
| 
 | ||||
|             value = when (category) { | ||||
|                 null -> default | ||||
|                 else -> LibraryToolbarTitle(title, count) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Composable | ||||
|     fun getMangaForCategory(page: Int): List<LibraryItem> { | ||||
|         val categoryId = remember(categories, page) { | ||||
|             categories.getOrNull(page)?.id ?: -1 | ||||
|         } | ||||
|         val unfiltered = remember(loadedManga, categoryId) { | ||||
|             loadedManga[categoryId] ?: emptyList() | ||||
|         } | ||||
|         return remember(unfiltered, searchQuery) { | ||||
|             if (searchQuery.isNullOrBlank()) { | ||||
|                 queriedMangaMap.clear() | ||||
|                 unfiltered | ||||
|             } else { | ||||
|                 unfiltered.fastFilter { it.matches(searchQuery!!) } | ||||
|                     .also { queriedMangaMap[categoryId] = it } | ||||
|             } | ||||
|     suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { | ||||
|         return withIOContext { | ||||
|             state.value | ||||
|                 .getLibraryItemsByCategoryId(activeCategory.toLong()) | ||||
|                 .randomOrNull() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun clearSelection() { | ||||
|         state.selection = emptyList() | ||||
|         mutableState.update { it.copy(selection = emptyList()) } | ||||
|     } | ||||
| 
 | ||||
|     fun toggleSelection(manga: LibraryManga) { | ||||
|         state.selection = selection.toMutableList().apply { | ||||
|             if (fastAny { it.id == manga.id }) { | ||||
|                 removeAll { it.id == manga.id } | ||||
|             } else { | ||||
|                 add(manga) | ||||
|         mutableState.update { state -> | ||||
|             val newSelection = state.selection.toMutableList().apply { | ||||
|                 if (fastAny { it.id == manga.id }) { | ||||
|                     removeAll { it.id == manga.id } | ||||
|                 } else { | ||||
|                     add(manga) | ||||
|                 } | ||||
|             } | ||||
|             state.copy(selection = newSelection) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Map is cleared out via [getMangaForCategory] when [searchQuery] is null or blank | ||||
|      */ | ||||
|     private val queriedMangaMap: MutableMap<Long, List<LibraryItem>> = mutableMapOf() | ||||
| 
 | ||||
|     /** | ||||
|      * Used by select all, inverse and range selection. | ||||
|      * | ||||
|      * If current query is empty then we get manga list from [loadedManga] otherwise from [queriedMangaMap] | ||||
|      */ | ||||
|     private fun getMangaForCategoryWithQuery(categoryId: Long, query: String?): List<LibraryItem> { | ||||
|         return if (query.isNullOrBlank()) loadedManga[categoryId].orEmpty() else queriedMangaMap[categoryId].orEmpty() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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) { | ||||
|         state.selection = selection.toMutableList().apply { | ||||
|             val lastSelected = lastOrNull() | ||||
|             if (lastSelected?.category != manga.category) { | ||||
|                 add(manga) | ||||
|                 return@apply | ||||
|             } | ||||
|         mutableState.update { state -> | ||||
|             val newSelection = state.selection.toMutableList().apply { | ||||
|                 val lastSelected = lastOrNull() | ||||
|                 if (lastSelected?.category != manga.category) { | ||||
|                     add(manga) | ||||
|                     return@apply | ||||
|                 } | ||||
| 
 | ||||
|             val items = getMangaForCategoryWithQuery(manga.category, searchQuery) | ||||
|                 .fastMap { it.libraryManga } | ||||
|             val lastMangaIndex = items.indexOf(lastSelected) | ||||
|             val curMangaIndex = items.indexOf(manga) | ||||
|                 val items = state.getLibraryItemsByCategoryId(manga.category) | ||||
|                     .fastMap { it.libraryManga } | ||||
|                 val lastMangaIndex = items.indexOf(lastSelected) | ||||
|                 val curMangaIndex = items.indexOf(manga) | ||||
| 
 | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val selectionRange = when { | ||||
|                 lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex) | ||||
|                 curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex) | ||||
|                 // We shouldn't reach this point | ||||
|                 else -> return@apply | ||||
|                 val selectedIds = fastMap { it.id } | ||||
|                 val selectionRange = when { | ||||
|                     lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex) | ||||
|                     curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex) | ||||
|                     // We shouldn't reach this point | ||||
|                     else -> return@apply | ||||
|                 } | ||||
|                 val newSelections = selectionRange.mapNotNull { index -> | ||||
|                     items[index].takeUnless { it.id in selectedIds } | ||||
|                 } | ||||
|                 addAll(newSelections) | ||||
|             } | ||||
|             val newSelections = selectionRange.mapNotNull { index -> | ||||
|                 items[index].takeUnless { it.id in selectedIds } | ||||
|             } | ||||
|             addAll(newSelections) | ||||
|             state.copy(selection = newSelection) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun selectAll(index: Int) { | ||||
|         state.selection = state.selection.toMutableList().apply { | ||||
|             val categoryId = categories.getOrNull(index)?.id ?: -1 | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val newSelections = getMangaForCategoryWithQuery(categoryId, searchQuery) | ||||
|                 .fastMapNotNull { item -> | ||||
|                     item.libraryManga.takeUnless { it.id in selectedIds } | ||||
|                 } | ||||
|         mutableState.update { state -> | ||||
|             val newSelection = state.selection.toMutableList().apply { | ||||
|                 val categoryId = state.categories.getOrNull(index)?.id ?: -1 | ||||
|                 val selectedIds = fastMap { it.id } | ||||
|                 val newSelections = state.getLibraryItemsByCategoryId(categoryId) | ||||
|                     .fastMapNotNull { item -> | ||||
|                         item.libraryManga.takeUnless { it.id in selectedIds } | ||||
|                     } | ||||
| 
 | ||||
|             addAll(newSelections) | ||||
|                 addAll(newSelections) | ||||
|             } | ||||
|             state.copy(selection = newSelection) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun invertSelection(index: Int) { | ||||
|         state.selection = selection.toMutableList().apply { | ||||
|             val categoryId = categories[index].id | ||||
|             val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga } | ||||
|             val selectedIds = fastMap { it.id } | ||||
|             val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds } | ||||
|             val toRemoveIds = toRemove.fastMap { it.id } | ||||
|             removeAll { it.id in toRemoveIds } | ||||
|             addAll(toAdd) | ||||
|         mutableState.update { state -> | ||||
|             val newSelection = state.selection.toMutableList().apply { | ||||
|                 val categoryId = state.categories[index].id | ||||
|                 val items = state.getLibraryItemsByCategoryId(categoryId).fastMap { it.libraryManga } | ||||
|                 val selectedIds = fastMap { it.id } | ||||
|                 val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds } | ||||
|                 val toRemoveIds = toRemove.fastMap { it.id } | ||||
|                 removeAll { it.id in toRemoveIds } | ||||
|                 addAll(toAdd) | ||||
|             } | ||||
|             state.copy(selection = newSelection) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun search(query: String?) { | ||||
|         mutableState.update { it.copy(searchQuery = query) } | ||||
|     } | ||||
| 
 | ||||
|     fun openChangeCategoryDialog() { | ||||
|         coroutineScope.launchIO { | ||||
|             // Create a copy of selected manga | ||||
|             val mangaList = state.value.selection.map { it.manga } | ||||
| 
 | ||||
|             // Hide the default category because it has a different behavior than the ones from db. | ||||
|             val categories = state.value.categories.filter { it.id != 0L } | ||||
| 
 | ||||
|             // Get indexes of the common categories to preselect. | ||||
|             val common = getCommonCategories(mangaList) | ||||
|             // Get indexes of the mix categories to preselect. | ||||
|             val mix = getMixCategories(mangaList) | ||||
|             val preselected = categories.map { | ||||
|                 when (it) { | ||||
|                     in common -> CheckboxState.State.Checked(it) | ||||
|                     in mix -> CheckboxState.TriState.Exclude(it) | ||||
|                     else -> CheckboxState.State.None(it) | ||||
|                 } | ||||
|             } | ||||
|             mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun openDeleteMangaDialog() { | ||||
|         val mangaList = state.value.selection.map { it.manga } | ||||
|         mutableState.update { it.copy(dialog = Dialog.DeleteManga(mangaList)) } | ||||
|     } | ||||
| 
 | ||||
|     fun closeDialog() { | ||||
|         mutableState.update { it.copy(dialog = null) } | ||||
|     } | ||||
| 
 | ||||
|     sealed class Dialog { | ||||
|         data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog() | ||||
|         data class DeleteManga(val manga: List<Manga>) : Dialog() | ||||
|         data class DownloadCustomAmount(val manga: List<Manga>, val max: Int) : Dialog() | ||||
|     } | ||||
| 
 | ||||
|     @Immutable | ||||
|     private data class ItemPreferences( | ||||
|         val downloadBadge: Boolean, | ||||
|         val unreadBadge: Boolean, | ||||
|         val localBadge: Boolean, | ||||
|         val languageBadge: Boolean, | ||||
| 
 | ||||
|         val globalFilterDownloaded: Boolean, | ||||
|         val filterDownloaded: Int, | ||||
|         val filterUnread: Int, | ||||
|         val filterStarted: Int, | ||||
|         val filterBookmarked: Int, | ||||
|         val filterCompleted: Int, | ||||
|     ) | ||||
| 
 | ||||
|     @Immutable | ||||
|     data class State( | ||||
|         val isLoading: Boolean = true, | ||||
|         val library: LibraryMap = emptyMap(), | ||||
|         val searchQuery: String? = null, | ||||
|         val selection: List<LibraryManga> = emptyList(), | ||||
|         val hasActiveFilters: Boolean = false, | ||||
|         val showCategoryTabs: Boolean = false, | ||||
|         val showMangaCount: Boolean = false, | ||||
|         val showMangaContinueButton: Boolean = false, | ||||
|         val dialog: Dialog? = null, | ||||
|     ) { | ||||
|         val selectionMode = selection.isNotEmpty() | ||||
| 
 | ||||
|         val categories = library.keys.toList() | ||||
| 
 | ||||
|         val libraryCount by lazy { | ||||
|             library | ||||
|                 .flatMap { (_, v) -> v } | ||||
|                 .distinctBy { it.libraryManga.manga.id } | ||||
|                 .size | ||||
|         } | ||||
| 
 | ||||
|         fun getLibraryItemsByCategoryId(categoryId: Long): List<LibraryItem> { | ||||
|             return library.firstNotNullOf { (k, v) -> v.takeIf { k.id == categoryId } } | ||||
|         } | ||||
| 
 | ||||
|         fun getLibraryItemsByPage(page: Int): List<LibraryItem> { | ||||
|             return library.values.toTypedArray().getOrNull(page) ?: emptyList() | ||||
|         } | ||||
| 
 | ||||
|         fun getMangaCountForCategory(category: Category): Int? { | ||||
|             return library[category]?.size?.takeIf { showMangaCount } | ||||
|         } | ||||
| 
 | ||||
|         fun getToolbarTitle( | ||||
|             defaultTitle: String, | ||||
|             defaultCategoryTitle: String, | ||||
|             page: Int, | ||||
|         ): LibraryToolbarTitle { | ||||
|             val category = categories.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) | ||||
|                 // Whole library count | ||||
|                 else -> libraryCount | ||||
|             } | ||||
| 
 | ||||
|             return LibraryToolbarTitle(title, count) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -32,7 +32,6 @@ class LibrarySettingsSheet( | ||||
|     private val trackManager: TrackManager = Injekt.get(), | ||||
|     private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(), | ||||
|     private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(), | ||||
|     onGroupClickListener: (ExtendedNavigationView.Group) -> Unit, | ||||
| ) : TabbedBottomSheetDialog(router.activity!!) { | ||||
|  | ||||
|     val filters: Filter | ||||
| @@ -43,13 +42,8 @@ class LibrarySettingsSheet( | ||||
|  | ||||
|     init { | ||||
|         filters = Filter(router.activity!!) | ||||
|         filters.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         sort = Sort(router.activity!!) | ||||
|         sort.onGroupClicked = onGroupClickListener | ||||
|  | ||||
|         display = Display(router.activity!!) | ||||
|         display.onGroupClicked = onGroupClickListener | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user