diff --git a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt index c0824722c..30793fbf8 100644 --- a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt @@ -66,6 +66,7 @@ import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.repository.FeedSavedSearchRepository import eu.kanade.domain.source.repository.SavedSearchRepository import eu.kanade.tachiyomi.source.online.MetadataSource +import exh.search.SearchEngine import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addFactory @@ -99,6 +100,7 @@ class SYDomainModule : InjektModule { addFactory { DeleteSortTag(get(), get()) } addFactory { ReorderSortTag(get(), get()) } addFactory { GetPagePreviews(get(), get()) } + addFactory { SearchEngine() } // Required for [MetadataSource] addFactory { GetManga(get()) } diff --git a/app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt b/app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt index 65d2465bb..ff6645547 100644 --- a/app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt +++ b/app/src/main/java/eu/kanade/presentation/components/CommonMangaItem.kt @@ -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), diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt deleted file mode 100644 index 088224a78..000000000 --- a/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt +++ /dev/null @@ -1,148 +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.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, - // SY --> - onClickCleanTitles: () -> Unit, - onClickMigrate: () -> Unit, - onClickAddToMangaDex: () -> Unit, - onClickSyncExh: () -> Unit, - // SY <-- -) { - 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, - // SY --> - onClickSyncExh = onClickSyncExh, - // SY <-- - 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, - // SY --> - onClickCleanTitles = onClickCleanTitles.takeIf { presenter.showCleanTitles }, - onClickMigrate = onClickMigrate, - onClickAddToMangaDex = onClickAddToMangaDex.takeIf { presenter.showAddToMangadex }, - // SY <-- - ) - }, - ) { 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) }, - // SY --> - getDisplayModeForPage = { presenter.getDisplayMode(index = it) }, - // SY <-- - 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, - // SY --> - getCategoryName = presenter::getCategoryName, - // SY <-- - ) - } -} diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt deleted file mode 100644 index 96c450b12..000000000 --- a/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt +++ /dev/null @@ -1,64 +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.LibraryGroup -import eu.kanade.domain.library.model.LibraryManga -import eu.kanade.tachiyomi.ui.library.LibraryPresenter -import exh.source.isEhBasedManga -import exh.source.mangaDexSourceIds -import exh.source.nHentaiSourceIds - -@Stable -interface LibraryState { - val isLoading: Boolean - val categories: List - var searchQuery: String? - val selection: List - val selectionMode: Boolean - var hasActiveFilters: Boolean - var dialog: LibraryPresenter.Dialog? - - // SY --> - val ogCategories: List - val groupType: Int - val showSyncExh: Boolean - val showCleanTitles: Boolean - val showAddToMangadex: Boolean - // SY <-- -} - -fun LibraryState(): LibraryState { - return LibraryStateImpl() -} - -class LibraryStateImpl : LibraryState { - override var isLoading: Boolean by mutableStateOf(true) - override var categories: List by mutableStateOf(emptyList()) - override var searchQuery: String? by mutableStateOf(null) - override var selection: List 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) - - // SY --> - override var groupType: Int by mutableStateOf(LibraryGroup.BY_DEFAULT) - - override var ogCategories: List by mutableStateOf(emptyList()) - - override var showSyncExh: Boolean by mutableStateOf(true) - override val showCleanTitles: Boolean by derivedStateOf { - selection.any { - it.manga.isEhBasedManga() || - it.manga.source in nHentaiSourceIds - } - } - override val showAddToMangadex: Boolean by derivedStateOf { - selection.any { it.manga.source in mangaDexSourceIds } - } - // SY <-- -} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt index 878f78d67..2446646e6 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt @@ -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, ) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt index beb9ba51a..ee3950146 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt @@ -14,17 +14,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem @Composable fun LibraryComfortableGrid( items: List, - showDownloadBadges: Boolean, - showUnreadBadges: Boolean, - showLocalBadges: Boolean, - showLanguageBadges: Boolean, - showContinueReadingButton: Boolean, columns: Int, contentPadding: PaddingValues, selection: List, 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 + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt index 6dd62fda3..de3309325 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt @@ -15,17 +15,12 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem fun LibraryCompactGrid( items: List, showTitle: Boolean, - showDownloadBadges: Boolean, - showUnreadBadges: Boolean, - showLocalBadges: Boolean, - showLanguageBadges: Boolean, - showContinueReadingButton: Boolean, columns: Int, contentPadding: PaddingValues, selection: List, 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 + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index 8b8a00526..051a2bb01 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.library.components -import android.content.Context import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding @@ -8,14 +7,12 @@ 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 import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import eu.kanade.core.prefs.PreferenceMutableState import eu.kanade.domain.category.model.Category @@ -23,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 @@ -31,33 +27,26 @@ import kotlin.time.Duration.Companion.seconds @Composable fun LibraryContent( - state: LibraryState, + categories: List, + searchQuery: String?, + selection: List, 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, + getNumberOfMangaForCategory: (Category) -> Int?, getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode, getColumnsForOrientation: (Boolean) -> PreferenceMutableState, - getLibraryForPage: @Composable (Int) -> List, - showDownloadBadges: Boolean, - showUnreadBadges: Boolean, - showLocalBadges: Boolean, - showLanguageBadges: Boolean, - showContinueReadingButton: Boolean, + getLibraryForPage: (Int) -> List, isDownloadOnly: Boolean, isIncognitoMode: Boolean, - // SY --> - getCategoryName: (Context, Category, Int, String) -> String, - // SY <-- ) { Column( modifier = Modifier.padding( @@ -66,46 +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.coerceAtMost(categories.lastIndex), - showMangaCount = showMangaCount, - getNumberOfMangaForCategory = getNumberOfMangaForCategory, + currentPageIndex = pagerState.currentPage, isDownloadOnly = isDownloadOnly, isIncognitoMode = isIncognitoMode, - onTabItemClick = { scope.launch { pagerState.animateScrollToPage(it) } }, - // SY --> - getCategoryName = { category, name -> - val context = LocalContext.current - remember(context, category, state.groupType, name) { - getCategoryName(context, category, state.groupType, name) - } - }, - // SY <-- - ) + 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, @@ -119,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, ) } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt index d4f04d453..be8015020 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt @@ -23,16 +23,11 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem @Composable fun LibraryList( items: List, - showDownloadBadges: Boolean, - showUnreadBadges: Boolean, - showLocalBadges: Boolean, - showLanguageBadges: Boolean, - showContinueReadingButton: Boolean, contentPadding: PaddingValues, selection: List, 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 + }, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt index 40618a689..7c5be66ac 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt @@ -27,15 +27,10 @@ fun LibraryPager( onGlobalSearchClicked: () -> Unit, getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode, getColumnsForOrientation: (Boolean) -> PreferenceMutableState, - getLibraryForPage: @Composable (Int) -> List, - showDownloadBadges: Boolean, - showUnreadBadges: Boolean, - showLocalBadges: Boolean, - showLanguageBadges: Boolean, - showContinueReadingButton: Boolean, + getLibraryForPage: (Int) -> List, 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, ) diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index d9da48bd8..d2344d0d2 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -5,7 +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.ui.unit.dp import eu.kanade.domain.category.model.Category import eu.kanade.presentation.category.visualName @@ -18,14 +17,10 @@ import eu.kanade.presentation.components.TabText fun LibraryTabs( categories: List, currentPageIndex: Int, - showMangaCount: Boolean, isDownloadOnly: Boolean, isIncognitoMode: Boolean, - getNumberOfMangaForCategory: @Composable (Long) -> State, + getNumberOfMangaForCategory: (Category) -> Int?, onTabItemClick: (Int) -> Unit, - // SY --> - getCategoryName: @Composable (Category, String) -> String, - // SY <-- ) { Column { ScrollableTabRow( @@ -42,14 +37,8 @@ fun LibraryTabs( onClick = { onTabItemClick(index) }, text = { TabText( - // SY --> - text = getCategoryName(category, category.visualName), - // SY <--, - badgeCount = if (showMangaCount) { - getNumberOfMangaForCategory(category.id) - } else { - null - }?.value, + text = category.visualName, + badgeCount = getNumberOfMangaForCategory(category), ) }, unselectedContentColor = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt index 403adfc8a..8aa827f29 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt @@ -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, @@ -40,12 +41,14 @@ fun LibraryToolbar( onClickRefresh: () -> Unit, onClickOpenRandomManga: () -> Unit, // SY --> - onClickSyncExh: () -> Unit, + onClickSyncExh: (() -> Unit)?, // SY <-- + searchQuery: String?, + onSearchQueryChange: (String?) -> Unit, scrollBehavior: TopAppBarScrollBehavior?, ) = when { - state.selectionMode -> LibrarySelectionToolbar( - state = state, + selectedCount > 0 -> LibrarySelectionToolbar( + selectedCount = selectedCount, incognitoMode = incognitoMode, downloadedOnlyMode = downloadedOnlyMode, onClickUnselectAll = onClickUnselectAll, @@ -54,16 +57,16 @@ 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, // SY --> - onClickSyncExh = onClickSyncExh.takeIf { state.showSyncExh }, + onClickSyncExh = onClickSyncExh, // SY <-- scrollBehavior = scrollBehavior, ) @@ -76,7 +79,7 @@ fun LibraryRegularToolbar( incognitoMode: Boolean, downloadedOnlyMode: Boolean, searchQuery: String?, - onChangeSearchQuery: (String?) -> Unit, + onSearchQueryChange: (String?) -> Unit, onClickFilter: () -> Unit, onClickRefresh: () -> Unit, onClickOpenRandomManga: () -> Unit, @@ -105,7 +108,7 @@ fun LibraryRegularToolbar( } }, searchQuery = searchQuery, - onChangeSearchQuery = onChangeSearchQuery, + onChangeSearchQuery = onSearchQueryChange, actions = { val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current IconButton(onClick = onClickFilter) { @@ -148,7 +151,7 @@ fun LibraryRegularToolbar( @Composable fun LibrarySelectionToolbar( - state: LibraryState, + selectedCount: Int, incognitoMode: Boolean, downloadedOnlyMode: Boolean, onClickUnselectAll: () -> Unit, @@ -156,7 +159,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)) @@ -172,6 +175,7 @@ fun LibrarySelectionToolbar( ) } +@Immutable data class LibraryToolbarTitle( val text: String, val numberOfManga: Int? = null, diff --git a/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesConfirmDialog.kt b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesConfirmDialog.kt new file mode 100644 index 000000000..6329a5ab7 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesConfirmDialog.kt @@ -0,0 +1,36 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import eu.kanade.tachiyomi.R + +@Composable +fun SyncFavoritesConfirmDialog( + onDismissRequest: () -> Unit, + onAccept: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onAccept) { + Text(text = stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + title = { + Text(stringResource(R.string.favorites_sync)) + }, + text = { + Text(text = stringResource(R.string.favorites_sync_conformation_message)) + }, + properties = DialogProperties(dismissOnClickOutside = false), + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesProgressDialog.kt b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesProgressDialog.kt new file mode 100644 index 000000000..57269d6b2 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesProgressDialog.kt @@ -0,0 +1,118 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.window.DialogProperties +import eu.kanade.domain.manga.model.Manga +import eu.kanade.tachiyomi.R +import exh.favorites.FavoritesSyncStatus +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds + +data class SyncFavoritesProgressProperties( + val title: String, + val text: String, + val canDismiss: Boolean, + val positiveButtonText: String? = null, + val positiveButton: (() -> Unit)? = null, + val negativeButtonText: String? = null, + val negativeButton: (() -> Unit)? = null, +) + +@Composable +fun SyncFavoritesProgressDialog( + status: FavoritesSyncStatus, + setStatusIdle: () -> Unit, + openManga: (Manga) -> Unit, +) { + val context = LocalContext.current + val properties by produceState(initialValue = null, status) { + when (status) { + is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> value = SyncFavoritesProgressProperties( + title = context.getString(R.string.favorites_sync_error), + text = context.getString(R.string.favorites_sync_bad_library_state, status.message), + canDismiss = false, + positiveButtonText = context.getString(R.string.show_gallery), + positiveButton = { + openManga(status.manga) + setStatusIdle() + }, + negativeButtonText = context.getString(android.R.string.ok), + negativeButton = setStatusIdle, + ) + is FavoritesSyncStatus.CompleteWithErrors -> value = SyncFavoritesProgressProperties( + title = context.getString(R.string.favorites_sync_done_errors), + text = context.getString(R.string.favorites_sync_done_errors_message, status.message), + canDismiss = false, + positiveButtonText = context.getString(android.R.string.ok), + positiveButton = setStatusIdle, + ) + is FavoritesSyncStatus.Error -> value = SyncFavoritesProgressProperties( + title = context.getString(R.string.favorites_sync_error), + text = context.getString(R.string.favorites_sync_error_string, status.message), + canDismiss = false, + positiveButtonText = context.getString(android.R.string.ok), + positiveButton = setStatusIdle, + ) + is FavoritesSyncStatus.Idle -> value = null + is FavoritesSyncStatus.Initializing, is FavoritesSyncStatus.Processing -> { + value = SyncFavoritesProgressProperties( + title = context.getString(R.string.favorites_syncing), + text = status.message, + canDismiss = false, + ) + if (status is FavoritesSyncStatus.Processing && status.title != null) { + delay(5.seconds) + value = SyncFavoritesProgressProperties( + title = context.getString(R.string.favorites_syncing), + text = status.delayedMessage ?: status.message, + canDismiss = false, + ) + } + } + } + } + val dialog = properties + if (dialog != null) { + AlertDialog( + onDismissRequest = {}, + confirmButton = { + if (dialog.positiveButton != null && dialog.positiveButtonText != null) { + TextButton(onClick = dialog.positiveButton) { + Text(text = dialog.positiveButtonText) + } + } + }, + dismissButton = { + if (dialog.negativeButton != null && dialog.negativeButtonText != null) { + TextButton(onClick = dialog.negativeButton) { + Text(text = dialog.negativeButtonText) + } + } + }, + title = { + Text(text = dialog.title) + }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()), + ) { + Text(text = dialog.text) + } + }, + properties = DialogProperties( + dismissOnClickOutside = dialog.canDismiss, + dismissOnBackPress = dialog.canDismiss, + ), + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesWarningDialog.kt b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesWarningDialog.kt new file mode 100644 index 000000000..c72af24d1 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/SyncFavoritesWarningDialog.kt @@ -0,0 +1,47 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.window.DialogProperties +import androidx.core.text.HtmlCompat +import eu.kanade.tachiyomi.R +import exh.util.toAnnotatedString + +@Composable +fun SyncFavoritesWarningDialog( + onDismissRequest: () -> Unit, + onAccept: () -> Unit, +) { + val context = LocalContext.current + val text = remember { + HtmlCompat.fromHtml(context.getString(R.string.favorites_sync_notes_message), HtmlCompat.FROM_HTML_MODE_LEGACY).toAnnotatedString() + } + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onAccept) { + Text(text = stringResource(android.R.string.ok)) + } + }, + title = { + Text(stringResource(R.string.favorites_sync_notes)) + }, + text = { + Column( + Modifier.verticalScroll(rememberScrollState()), + ) { + Text(text = text) + } + }, + properties = DialogProperties(dismissOnClickOutside = false), + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index b2cf6bb1d..6209a1a0b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -1,246 +1,37 @@ package eu.kanade.tachiyomi.ui.library import android.os.Bundle -import android.view.Menu import android.view.View -import android.view.WindowManager -import androidx.appcompat.app.AlertDialog 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 com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.core.prefs.CheckboxState -import eu.kanade.domain.UnsortedPreferences -import eu.kanade.domain.chapter.model.Chapter -import eu.kanade.domain.library.model.LibraryGroup -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.migration.advanced.design.PreMigrationController -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 exh.favorites.FavoritesIntroDialog -import exh.favorites.FavoritesSyncStatus -import exh.source.MERGED_SOURCE_ID -import exh.source.isEhBasedManga -import exh.source.mangaDexSourceIds -import exh.source.nHentaiSourceIds -import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.sample -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch class LibraryController( bundle: Bundle? = null, -) : FullComposeController(bundle), RootController { +) : BasicFullComposeController(bundle), RootController { /** * Sheet containing filter/sort/display items. */ private var settingsSheet: LibrarySettingsSheet? = null - // --> EH - // Sync dialog - private var favSyncDialog: AlertDialog? = null - - // Old sync status - private var oldSyncStatus: FavoritesSyncStatus? = null - - // Favorites - private var favoritesSyncJob: Job? = null - // <-- EH - - 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 = { - // SY --> - val groupType = presenter.groupType - // SY --> - val started = LibraryUpdateService.start( - context = context, - category = if (groupType == LibraryGroup.BY_DEFAULT) it else null, - group = groupType, - groupExtra = when (groupType) { - LibraryGroup.BY_DEFAULT -> null - LibraryGroup.BY_SOURCE, LibraryGroup.BY_STATUS, LibraryGroup.BY_TRACK_STATUS -> it?.id?.toString() - else -> null - }, - ) - // SY <-- - 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, - // SY --> - onClickCleanTitles = ::cleanTitles, - onClickMigrate = { - val selectedMangaIds = presenter.selection - .filterNot { it.manga.source == MERGED_SOURCE_ID } - .map { it.manga.id } - presenter.clearSelection() - if (selectedMangaIds.isNotEmpty()) { - PreMigrationController.navigateToMigration( - Injekt.get().skipPreMigration().get(), - router, - selectedMangaIds, - ) - } else { - activity?.toast(R.string.no_valid_manga) - } - }, - onClickAddToMangaDex = ::pushToMdList, - onClickSyncExh = { - // TODO - if (Injekt.get().exhShowSyncIntro().get()) { - activity?.let { FavoritesIntroDialog().show(it) } - } else { - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.favorites_sync) - .setMessage(R.string.favorites_sync_conformation_message) - .setPositiveButton(android.R.string.ok) { _, _ -> - presenter.runSync() - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - }, - // SY <-- - ) - - 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) } } @@ -250,265 +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.ogCategories.filter { it.id != 0L } // SY <-- - - // 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) - } - - // SY --> - private fun cleanTitles() { - val mangas = presenter.selection.filter { - it.manga.isEhBasedManga() || - it.manga.source in nHentaiSourceIds - } - presenter.cleanTitles(mangas) - presenter.clearSelection() - } - - private fun pushToMdList() { - val mangas = presenter.selection.filter { - it.manga.source in mangaDexSourceIds - } - presenter.syncMangaToDex(mangas) - presenter.clearSelection() - } - - override fun onAttach(view: View) { - super.onAttach(view) - - // --> EXH - cleanupSyncState() - favoritesSyncJob = - presenter.favoritesSync.status - .sample(100.milliseconds) - .mapLatest { - updateSyncStatus(it) - } - .launchIn(viewScope) - // <-- EXH - } - - override fun onDetach(view: View) { - super.onDetach(view) - - // EXH - cleanupSyncState() - } - // SY <-- - - // --> EXH - private fun cleanupSyncState() { - favoritesSyncJob?.cancel() - favoritesSyncJob = null - // Close sync status - favSyncDialog?.dismiss() - favSyncDialog = null - oldSyncStatus = null - // Clear flags - releaseSyncLocks() - } - - private fun buildDialog() = activity?.let { - MaterialAlertDialogBuilder(it) - } - - private fun showSyncProgressDialog() { - favSyncDialog?.dismiss() - favSyncDialog = buildDialog() - ?.setTitle(R.string.favorites_syncing) - ?.setMessage("") - ?.setCancelable(false) - ?.create() - favSyncDialog?.show() - } - - private fun takeSyncLocks() { - activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - - private fun releaseSyncLocks() { - activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - - private suspend fun updateSyncStatus(status: FavoritesSyncStatus) { - when (status) { - is FavoritesSyncStatus.Idle -> { - releaseSyncLocks() - - favSyncDialog?.dismiss() - favSyncDialog = null - } - is FavoritesSyncStatus.BadLibraryState.MangaInMultipleCategories -> { - releaseSyncLocks() - - favSyncDialog?.dismiss() - favSyncDialog = buildDialog() - ?.setTitle(R.string.favorites_sync_error) - ?.setMessage(activity!!.getString(R.string.favorites_sync_bad_library_state, status.message)) - ?.setCancelable(false) - ?.setPositiveButton(R.string.show_gallery) { _, _ -> - openManga(status.manga.id) - presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) - } - ?.setNegativeButton(android.R.string.ok) { _, _ -> - presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) - } - ?.create() - favSyncDialog?.show() - } - is FavoritesSyncStatus.Error -> { - releaseSyncLocks() - - favSyncDialog?.dismiss() - favSyncDialog = buildDialog() - ?.setTitle(R.string.favorites_sync_error) - ?.setMessage(activity!!.getString(R.string.favorites_sync_error_string, status.message)) - ?.setCancelable(false) - ?.setPositiveButton(android.R.string.ok) { _, _ -> - presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) - } - ?.create() - favSyncDialog?.show() - } - is FavoritesSyncStatus.CompleteWithErrors -> { - releaseSyncLocks() - - favSyncDialog?.dismiss() - favSyncDialog = buildDialog() - ?.setTitle(R.string.favorites_sync_done_errors) - ?.setMessage(activity!!.getString(R.string.favorites_sync_done_errors_message, status.message)) - ?.setCancelable(false) - ?.setPositiveButton(android.R.string.ok) { _, _ -> - presenter.favoritesSync.status.value = FavoritesSyncStatus.Idle(activity!!) - } - ?.create() - favSyncDialog?.show() - } - is FavoritesSyncStatus.Processing, - is FavoritesSyncStatus.Initializing, - -> { - takeSyncLocks() - - if (favSyncDialog == null || ( - oldSyncStatus != null && - oldSyncStatus !is FavoritesSyncStatus.Initializing && - oldSyncStatus !is FavoritesSyncStatus.Processing - ) - ) { - showSyncProgressDialog() - } - - favSyncDialog?.setMessage(status.message) - } - } - oldSyncStatus = status - if (status is FavoritesSyncStatus.Processing && status.delayedMessage != null) { - delay(5.seconds) - favSyncDialog?.setMessage(status.delayedMessage) - } - } - // <-- EXH + fun search(query: String) = LibraryScreen.search(query) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt new file mode 100644 index 000000000..74004c5df --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt @@ -0,0 +1,341 @@ +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.UnsortedPreferences +import eu.kanade.domain.category.model.Category +import eu.kanade.domain.library.model.LibraryGroup +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.library.components.SyncFavoritesConfirmDialog +import eu.kanade.presentation.library.components.SyncFavoritesProgressDialog +import eu.kanade.presentation.library.components.SyncFavoritesWarningDialog +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.migration.advanced.design.PreMigrationController +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.system.toast +import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView +import exh.favorites.FavoritesSyncStatus +import exh.source.MERGED_SOURCE_ID +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +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 = { + // SY --> + val started = LibraryUpdateService.start( + context = context, + category = if (state.groupType == LibraryGroup.BY_DEFAULT) it else null, + group = state.groupType, + groupExtra = when (state.groupType) { + LibraryGroup.BY_DEFAULT -> null + LibraryGroup.BY_SOURCE, LibraryGroup.BY_TRACK_STATUS -> it?.id?.toString() + LibraryGroup.BY_STATUS -> it?.id?.minus(1)?.toString() + else -> null + }, + ) + // SY <-- + 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)) + } + } + }, + // SY --> + onClickSyncExh = screenModel::openFavoritesSyncDialog.takeIf { state.showSyncExh }, + // SY <-- + 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, + // SY --> + onClickCleanTitles = screenModel::cleanTitles.takeIf { state.showCleanTitles }, + onClickMigrate = { + val selectedMangaIds = state.selection + .filterNot { it.manga.source == MERGED_SOURCE_ID } + .map { it.manga.id } + screenModel.clearSelection() + if (selectedMangaIds.isNotEmpty()) { + PreMigrationController.navigateToMigration( + Injekt.get().skipPreMigration().get(), + router, + selectedMangaIds, + ) + } else { + context.toast(R.string.no_valid_manga) + } + }, + onClickAddToMangaDex = screenModel::syncMangaToDex.takeIf { state.showAddToMangadex }, + // SY <-- + ) + }, + 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() + }, + ) + } + LibraryScreenModel.Dialog.SyncFavoritesWarning -> { + SyncFavoritesWarningDialog( + onDismissRequest = onDismissRequest, + onAccept = { + onDismissRequest() + screenModel.onAcceptSyncWarning() + }, + ) + } + LibraryScreenModel.Dialog.SyncFavoritesConfirm -> { + SyncFavoritesConfirmDialog( + onDismissRequest = onDismissRequest, + onAccept = { + onDismissRequest() + screenModel.runSync() + }, + ) + } + null -> {} + } + + // SY --> + SyncFavoritesProgressDialog( + status = screenModel.favoritesSync.status.collectAsState().value, + setStatusIdle = { screenModel.favoritesSync.status.value = FavoritesSyncStatus.Idle(context) }, + openManga = { router.openManga(it.id) }, + ) + // SY <-- + + 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(replay = 1) + fun search(query: String) = queryEvent.tryEmit(query) + + // For opening settings sheet in LibraryController + private val requestSettingsSheetEvent = MutableSharedFlow() + private val openSettingsSheetEvent_ = MutableSharedFlow() + val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow() + private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category) + suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt similarity index 62% rename from app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt index 9244c1614..7ff70e9a7 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt @@ -1,21 +1,18 @@ package eu.kanade.tachiyomi.ui.library import android.content.Context -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.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach 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 @@ -26,7 +23,6 @@ import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.model.Category import eu.kanade.domain.chapter.interactor.GetChapterByMangaId -import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.model.Chapter import eu.kanade.domain.history.interactor.GetNextChapters @@ -34,7 +30,6 @@ import eu.kanade.domain.library.model.LibraryDisplayMode import eu.kanade.domain.library.model.LibraryGroup import eu.kanade.domain.library.model.LibraryManga import eu.kanade.domain.library.model.LibrarySort -import eu.kanade.domain.library.model.display import eu.kanade.domain.library.model.sort import eu.kanade.domain.library.service.LibraryPreferences import eu.kanade.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata @@ -50,10 +45,8 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.domain.track.interactor.GetTracksPerManga import eu.kanade.domain.track.model.Track -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.presentation.manga.DownloadAction import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.download.DownloadCache @@ -67,13 +60,13 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.MergedSource -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.preference.asHotFlow import eu.kanade.tachiyomi.util.removeCovers -import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup import exh.favorites.FavoritesSyncHelper import exh.md.utils.FollowStatus import exh.md.utils.MdUtil @@ -85,23 +78,26 @@ import exh.search.SearchEngine import exh.search.Text import exh.source.EH_SOURCE_ID import exh.source.MERGED_SOURCE_ID +import exh.source.isEhBasedManga import exh.source.isMetadataSource +import exh.source.mangaDexSourceIds +import exh.source.nHentaiSourceIds import exh.util.cancellable import exh.util.isLewd import exh.util.nullIfBlank -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -109,18 +105,12 @@ import java.text.Collator import java.util.Collections import java.util.Locale -/** - * Class containing library information. - */ -private data class Library(val categories: List, val mangaMap: LibraryMap) - /** * Typealias for the library manga, using the category as keys, and list of manga as values. */ -typealias LibraryMap = Map> +typealias LibraryMap = Map> -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(), @@ -139,46 +129,25 @@ class LibraryPresenter( // SY --> private val unsortedPreferences: UnsortedPreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(), - private val searchEngine: SearchEngine = SearchEngine(), - private val getTracks: GetTracks = Injekt.get(), - private val customMangaManager: CustomMangaManager = Injekt.get(), private val getMergedMangaById: GetMergedMangaById = Injekt.get(), - private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(), + private val customMangaManager: CustomMangaManager = Injekt.get(), + private val getTracks: GetTracks = Injekt.get(), private val getIdsOfFavoriteMangaWithMetadata: GetIdsOfFavoriteMangaWithMetadata = Injekt.get(), private val getSearchTags: GetSearchTags = Injekt.get(), private val getSearchTitles: GetSearchTitles = Injekt.get(), + private val searchEngine: SearchEngine = Injekt.get(), // SY <-- -) : BasePresenter(), LibraryState by state { +) : StateScreenModel(State()) { - private var loadedManga by mutableStateOf(emptyMap>()) + // This is active category INDEX NUMBER + var activeCategory: Int by libraryPreferences.lastUsedCategory().asState(coroutineScope) - val isLibraryEmpty by derivedStateOf { loadedManga.isEmpty() } - - 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 = Channel(Int.MAX_VALUE) - private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) } - - private var librarySubscription: Job? = null + val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope) + val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope) // SY --> val favoritesSync = FavoritesSyncHelper(preferences.context) - private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - private val services by lazy { trackManager.services.associate { service -> service.id to preferences.context.getString(service.nameRes()) @@ -186,8 +155,82 @@ class LibraryPresenter( } // SY <-- - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) + init { + coroutineScope.launchIO { + combine( + state.map { it.searchQuery }.distinctUntilChanged(), + getLibraryFlow(), + getTracksPerManga.subscribe(), + getTrackingFilterFlow(), + // SY --> + combine(state.map { it.groupType }.distinctUntilChanged(), libraryPreferences.libraryDisplayMode().changes()) { a, b -> a to b }, + // SY <-- + ) { searchQuery, library, tracks, loggedInTrackServices, (groupType, displayMode) -> + library + .applyFilters(tracks, loggedInTrackServices) + .applySort(/* SY --> */groupType/* SY <-- */) + // SY --> + .applyGrouping(groupType, displayMode) + // SY <-- + .mapValues { (_, value) -> + if (searchQuery != null) { + // Filter query + // SY --> + filterLibrary(value, searchQuery, loggedInTrackServices) + // SY <-- + } 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) // SY --> combine( @@ -195,84 +238,58 @@ class LibraryPresenter( sourcePreferences.disabledSources().changes(), unsortedPreferences.enableExhentai().changes(), ) { isHentaiEnabled, disabledSources, enableExhentai -> - state.showSyncExh = isHentaiEnabled && (EH_SOURCE_ID.toString() !in disabledSources || enableExhentai) - }.flowOn(Dispatchers.IO).launchIn(presenterScope) - // SY <-- - - 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, - // SY --> - libraryPreferences.groupLibraryBy().changes(), - libraryPreferences.librarySortingMode().changes(), - // SY <-- - ) { library, tracks, _, _, _ -> - library.mangaMap - // SY --> - .applyGrouping(library.categories).let { - it.copy(mangaMap = it.mangaMap.applyFilters(tracks).applySort(it.categories)) - } - // SY <-- - } - .collectLatest { - // SY --> - state.groupType = libraryPreferences.groupLibraryBy().get() - state.categories = it.categories - // SY <-- - state.isLoading = false - loadedManga = /* SY --> */ it.mangaMap /* SY <-- */ - } - } + isHentaiEnabled && (EH_SOURCE_ID.toString() !in disabledSources || enableExhentai) } + .distinctUntilChanged() + .onEach { + mutableState.update { state -> + state.copy(showSyncExh = it) + } + } + .launchIn(coroutineScope) + + libraryPreferences.groupLibraryBy().asHotFlow { + mutableState.update { state -> + state.copy(groupType = it) + } + }.launchIn(coroutineScope) + // SY <-- } /** * Applies library filters to the given map of manga. */ - private fun LibraryMap.applyFilters(trackMap: Map>): 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>, + loggedInTrackServices: Map, + ): 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() // SY --> - val filterLewd = libraryPreferences.filterLewd().get() + val filterLewd = prefs.filterLewd // SY <-- 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 @@ -280,10 +297,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 @@ -291,10 +308,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 @@ -302,11 +319,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 @@ -314,10 +331,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 @@ -346,10 +363,10 @@ class LibraryPresenter( // SY --> val filterFnLewd: (LibraryItem) -> Boolean = lewd@{ item -> - if (filterLewd == State.IGNORE.value) return@lewd true + if (filterLewd == TriStateGroup.State.IGNORE.value) return@lewd true val isLewd = item.libraryManga.manga.isLewd() - return@lewd if (filterLewd == State.INCLUDE.value) { + return@lewd if (filterLewd == TriStateGroup.State.INCLUDE.value) { isLewd } else { !isLewd @@ -377,7 +394,7 @@ class LibraryPresenter( /** * Applies library sorting to the given map of manga. */ - private fun LibraryMap.applySort(categories: List): LibraryMap { + private fun LibraryMap.applySort(/* SY --> */ groupType: Int /* SY <-- */): LibraryMap { // SY --> val listOfTags by lazy { libraryPreferences.sortTagsForLibrary().get() @@ -390,17 +407,8 @@ class LibraryPresenter( .map { it.second } .toList() } - // SY <-- - - // SY --> - val groupType = libraryPreferences.groupLibraryBy().get() val groupSort = libraryPreferences.librarySortingMode().get() // SY <-- - val sortModes = categories.associate { category -> - // SY --> - category.id to category.sort - // SY <-- - } val locale = Locale.getDefault() val collator = Collator.getInstance(locale).apply { @@ -413,7 +421,7 @@ class LibraryPresenter( val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> // SY --> val sort = when (groupType) { - LibraryGroup.BY_DEFAULT -> sortModes[i1.libraryManga.category]!! + LibraryGroup.BY_DEFAULT -> keys.find { it.id == i1.libraryManga.category }!!.sort else -> groupSort } // SY <-- @@ -453,14 +461,13 @@ class LibraryPresenter( manga1IndexOfTag.compareTo(manga2IndexOfTag) } // SY <-- - else -> throw IllegalStateException("Invalid SortModeSetting: ${sort.type}") } } return this.mapValues { entry -> // SY --> val isAscending = if (groupType == LibraryGroup.BY_DEFAULT) { - sortModes[entry.key]!!.isAscending + keys.find { it.id == entry.key.id }!!.sort.isAscending } else { groupSort.isAscending } @@ -475,24 +482,58 @@ class LibraryPresenter( } } + private fun getLibraryItemPreferencesFlow(): Flow { + 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(), + // SY --> + libraryPreferences.filterLewd().changes(), + // SY <-- + 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, + // SY --> + filterLewd = it[10] as Int, + // SY <-- + ) + }, + ) + } + /** * Get the categories and all its manga from the database. * * @return an observable of the categories and its manga. */ - private fun getLibraryFlow(): Flow { + private fun getLibraryFlow(): Flow { 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 { @@ -509,67 +550,80 @@ 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 } // SY --> - state.ogCategories = displayCategories + mutableState.update { state -> + state.copy(ogCategories = displayCategories) + } // SY <-- - Library(displayCategories, libraryManga) + displayCategories.associateWith { libraryManga[it.id] ?: emptyList() } } } // SY --> - private fun LibraryMap.applyGrouping(categories: List): Library { - val groupType = libraryPreferences.groupLibraryBy().get() - var editedCategories = categories + private fun LibraryMap.applyGrouping(groupType: Int, displayMode: LibraryDisplayMode): LibraryMap { val items = when (groupType) { LibraryGroup.BY_DEFAULT -> this LibraryGroup.UNGROUPED -> { - editedCategories = listOf(Category(0, "All", 0, 0)) mapOf( - 0L to this.values.flatten().distinctBy { it.libraryManga.manga.id }, + Category( + 0, + preferences.context.getString(R.string.ungrouped), + 0, + displayMode.flag, + ) to + values.flatten().distinctBy { it.libraryManga.manga.id }, ) } else -> { - val (items, customCategories) = getGroupedMangaItems( + getGroupedMangaItems( groupType = groupType, libraryManga = this.values.flatten().distinctBy { it.libraryManga.manga.id }, + displayMode = displayMode, ) - editedCategories = customCategories - items } } - return Library(editedCategories, items) + return items } // SY <-- /** - * 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> { + val loggedServices = trackManager.services.filter { it.isLogged } + val a = loggedServices + .map { libraryPreferences.filterTracking(it.id.toInt()).changes() } + .toTypedArray() + if (a.isEmpty()) { + return flowOf(emptyMap()) + } + return combine(*a) { + loggedServices + .mapIndexed { index, trackService -> trackService.id to it[index] } + .toMap() + } } /** @@ -577,7 +631,7 @@ class LibraryPresenter( * * @param mangas the list of manga. */ - suspend fun getCommonCategories(mangas: List): Collection { + private suspend fun getCommonCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() return mangas .map { getCategories.await(it.id).toSet() } @@ -593,13 +647,37 @@ class LibraryPresenter( * * @param mangas the list of manga. */ - suspend fun getMixCategories(mangas: List): Collection { + private suspend fun getMixCategories(mangas: List): Collection { 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. * @@ -607,7 +685,7 @@ class LibraryPresenter( * @param amount the amount to queue or null to queue all */ fun downloadUnreadChapters(mangas: List, amount: Int?) { - presenterScope.launchNonCancellable { + coroutineScope.launchNonCancellable { mangas.forEach { manga -> // SY --> if (manga.source == MERGED_SOURCE_ID) { @@ -656,8 +734,11 @@ class LibraryPresenter( } // SY --> - fun cleanTitles(mangas: List) { - mangas.forEach { (manga) -> + fun cleanTitles() { + state.value.selection.fastFilter { + it.manga.isEhBasedManga() || + it.manga.source in nHentaiSourceIds + }.fastForEach { (manga) -> val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let { if (it.contains("|")) { it.replace(".*\\|".toRegex(), "").trim() @@ -665,7 +746,7 @@ class LibraryPresenter( it } } - if (manga.title == editedTitle) return@forEach + if (manga.title == editedTitle) return@fastForEach val mangaJson = CustomMangaManager.MangaJson( id = manga.id, title = editedTitle.nullIfBlank(), @@ -678,33 +759,35 @@ class LibraryPresenter( customMangaManager.saveMangaInfo(mangaJson) } + clearSelection() } - fun syncMangaToDex(mangaList: List) { + fun syncMangaToDex() { launchIO { MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences, sourceManager)?.let { mdex -> - mangaList.forEach { (manga) -> + state.value.selection.fastFilter { it.manga.source in mangaDexSourceIds }.fastForEach { (manga) -> mdex.updateFollowStatus(MdUtil.getMangaId(manga.url), FollowStatus.READING) } } + clearSelection() } } // SY <-- /** * Marks mangas' chapters read status. - * - * @param mangas the list of manga. */ - fun markReadStatus(mangas: List, 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() } /** @@ -715,7 +798,7 @@ class LibraryPresenter( * @param deleteChapters whether to delete downloaded chapters. */ fun removeMangas(mangaList: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { - presenterScope.launchNonCancellable { + coroutineScope.launchNonCancellable { val mangaToDelete = mangaList.distinctBy { it.id } if (deleteFromLibrary) { @@ -757,7 +840,7 @@ class LibraryPresenter( * @param removeCategories the categories to remove in all mangas. */ fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { - presenterScope.launchNonCancellable { + coroutineScope.launchNonCancellable { mangaList.forEach { manga -> val categoryIds = getCategories.await(manga.id) .map { it.id } @@ -770,47 +853,20 @@ class LibraryPresenter( } } - @Composable - fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State { - return produceState(initialValue = null, loadedManga) { - value = loadedManga[categoryId]?.size - } - } - fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState { - 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 { - val context = LocalContext.current - 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, groupType, context) { - val title = if (tabVisibility.not()) { - getCategoryName(context, category, groupType, 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) - } + suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? { + return withIOContext { + state.value + .getLibraryItemsByCategoryId(activeCategory.toLong()) + .randomOrNull() } } + // SY --> + fun getCategoryName( context: Context, category: Category?, @@ -841,31 +897,12 @@ class LibraryPresenter( } } - // SY --> - @Composable - fun getMangaForCategory(page: Int): List { - val categoryId = remember(categories, page) { - categories.getOrNull(page)?.id ?: -1 - } - val unfiltered = remember(loadedManga, categoryId) { - loadedManga[categoryId] ?: emptyList() - } - - val items = produceState(initialValue = unfiltered, unfiltered, searchQuery, categoryId) { - value = withIOContext { - filterLibrary(unfiltered, searchQuery, categoryId) - } - } - - return items.value - } - - suspend fun filterLibrary(unfiltered: List, query: String?, categoryId: Long): List { + suspend fun filterLibrary(unfiltered: List, query: String?, loggedInTrackServices: Map): List { return if (unfiltered.isNotEmpty() && !query.isNullOrBlank()) { // Prepare filter object val parsedQuery = searchEngine.parseQuery(query) val mangaWithMetaIds = getIdsOfFavoriteMangaWithMetadata.await() - val tracks = if (loggedServices.isNotEmpty()) { + val tracks = if (loggedInTrackServices.isNotEmpty()) { getTracks.await().groupBy { it.mangaId } } else { emptyMap() @@ -885,6 +922,7 @@ class LibraryPresenter( libraryManga = item.libraryManga, tracks = tracks[mangaId], source = sources[sourceId], + loggedInTrackServices = loggedInTrackServices, ) } else { val tags = getSearchTags.await(mangaId) @@ -897,6 +935,7 @@ class LibraryPresenter( checkGenre = false, searchTags = tags, searchTitles = titles, + loggedInTrackServices = loggedInTrackServices, ) } } else { @@ -905,13 +944,11 @@ class LibraryPresenter( libraryManga = item.libraryManga, tracks = tracks[mangaId], source = sources[sourceId], + loggedInTrackServices = loggedInTrackServices, ) } - }.toList().also { queriedMangaMap[categoryId] = it } + }.toList() } else { - if (query.isNullOrBlank()) { - queriedMangaMap.clear() - } unfiltered } } @@ -924,6 +961,7 @@ class LibraryPresenter( checkGenre: Boolean = true, searchTags: List? = null, searchTitles: List? = null, + loggedInTrackServices: Map, ): Boolean { val manga = libraryManga.manga val sourceIdString = manga.source.takeUnless { it == LocalSource.ID }?.toString() @@ -939,7 +977,7 @@ class LibraryPresenter( (manga.description?.contains(query, true) == true) || (source?.name?.contains(query, true) == true) || (sourceIdString != null && sourceIdString == query) || - (loggedServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) || + (loggedInTrackServices.isNotEmpty() && tracks != null && filterTracks(query, tracks)) || (genre.fastAny { it.contains(query, true) }) || (searchTags?.fastAny { it.name.contains(query, true) } == true) || (searchTitles?.fastAny { it.title.contains(query, true) } == true) @@ -963,7 +1001,7 @@ class LibraryPresenter( (manga.description?.contains(query, true) != true) && (source?.name?.contains(query, true) != true) && (sourceIdString != null && sourceIdString != query) && - (loggedServices.isEmpty() || loggedServices.isNotEmpty() && tracks == null || tracks != null && !filterTracks(query, tracks)) && + (loggedInTrackServices.isEmpty() || tracks == null || !filterTracks(query, tracks)) && (!genre.fastAny { it.contains(query, true) }) && (searchTags?.fastAny { it.name.contains(query, true) } != true) && (searchTitles?.fastAny { it.title.contains(query, true) } != true) @@ -1001,65 +1039,39 @@ class LibraryPresenter( } } } - - private val libraryDisplayMode by libraryPreferences.libraryDisplayMode().asState() // SY <-- - @Composable - fun getDisplayMode(index: Int): LibraryDisplayMode { - val category = categories[index] - return remember(groupType, libraryDisplayMode, category) { - // SY --> - if (groupType != LibraryGroup.BY_DEFAULT) { - libraryDisplayMode - } else { - category.display - } - // SY <-- - } - } - 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> = 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 { - 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) { - presenterScope.launchIO { - state.selection = selection.toMutableList().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) + + val items = state.getLibraryItemsByCategoryId(manga.category) .fastMap { it.libraryManga } val lastMangaIndex = items.indexOf(lastSelected) val curMangaIndex = items.indexOf(manga) @@ -1076,42 +1088,83 @@ class LibraryPresenter( } addAll(newSelections) } + state.copy(selection = newSelection) } } fun selectAll(index: Int) { - presenterScope.launchIO { - state.selection = state.selection.toMutableList().apply { - val categoryId = categories.getOrNull(index)?.id ?: -1 + mutableState.update { state -> + val newSelection = state.selection.toMutableList().apply { + val categoryId = state.categories.getOrNull(index)?.id ?: -1 val selectedIds = fastMap { it.id } - val newSelections = getMangaForCategoryWithQuery(categoryId, searchQuery) + val newSelections = state.getLibraryItemsByCategoryId(categoryId) .fastMapNotNull { item -> item.libraryManga.takeUnless { it.id in selectedIds } } addAll(newSelections) } + state.copy(selection = newSelection) } } fun invertSelection(index: Int) { - presenterScope.launchIO { - state.selection = selection.toMutableList().apply { - val categoryId = categories[index].id - val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga } + 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, val initialSelection: List>) : Dialog() data class DeleteManga(val manga: List) : Dialog() data class DownloadCustomAmount(val manga: List, val max: Int) : Dialog() + object SyncFavoritesWarning : Dialog() + object SyncFavoritesConfirm : Dialog() } // SY --> @@ -1120,8 +1173,9 @@ class LibraryPresenter( return getNextChapters.await(manga.id).firstOrNull() } - private fun getGroupedMangaItems(groupType: Int, libraryManga: List): Pair> { - val manga = when (groupType) { + private fun getGroupedMangaItems(groupType: Int, libraryManga: List, displayMode: LibraryDisplayMode): LibraryMap { + val context = preferences.context + return when (groupType) { LibraryGroup.BY_TRACK_STATUS -> { val tracks = runBlocking { getTracks.await() }.groupBy { it.mangaId } libraryManga.groupBy { item -> @@ -1130,37 +1184,188 @@ class LibraryPresenter( } ?: TrackStatus.OTHER status.int - }.mapKeys { it.key.toLong() } + }.mapKeys { (id) -> + Category( + id = id.toLong(), + name = TrackStatus.values() + .find { it.int == id } + .let { it ?: TrackStatus.OTHER } + .let { context.getString(it.res) }, + order = TrackStatus.values().indexOfFirst { it.int == id }.takeUnless { it == -1 }?.toLong() ?: TrackStatus.OTHER.ordinal.toLong(), + flags = displayMode.flag, + ) + } } LibraryGroup.BY_SOURCE -> { + val sources: List libraryManga.groupBy { item -> item.libraryManga.manga.source + }.also { + sources = it.keys + .map { + sourceManager.getOrStub(it) + } + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + .map { it.id } + }.mapKeys { + Category( + id = it.key, + name = if (it.key == LocalSource.ID) { + context.getString(R.string.local_source) + } else { + sourceManager.getOrStub(it.key).name + }, + order = sources.indexOf(it.key).takeUnless { it == -1 }?.toLong() ?: Long.MAX_VALUE, + flags = displayMode.flag, + ) } } else -> { libraryManga.groupBy { item -> item.libraryManga.manga.status + }.mapKeys { + Category( + id = it.key + 1, + name = when (it.key) { + SManga.ONGOING.toLong() -> context.getString(R.string.ongoing) + SManga.LICENSED.toLong() -> context.getString(R.string.licensed) + SManga.CANCELLED.toLong() -> context.getString(R.string.cancelled) + SManga.ON_HIATUS.toLong() -> context.getString(R.string.on_hiatus) + SManga.PUBLISHING_FINISHED.toLong() -> context.getString(R.string.publishing_finished) + SManga.COMPLETED.toLong() -> context.getString(R.string.completed) + else -> context.getString(R.string.unknown) + }, + order = when (it.key) { + SManga.ONGOING.toLong() -> 1 + SManga.LICENSED.toLong() -> 2 + SManga.CANCELLED.toLong() -> 3 + SManga.ON_HIATUS.toLong() -> 4 + SManga.PUBLISHING_FINISHED.toLong() -> 5 + SManga.COMPLETED.toLong() -> 6 + else -> 7 + }, + flags = displayMode.flag, + ) } } - } - - val categories = when (groupType) { - LibraryGroup.BY_SOURCE -> - manga.keys - .map { Category(it, sourceManager.getOrStub(it).name, 0, 0) } - .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) - LibraryGroup.BY_TRACK_STATUS, LibraryGroup.BY_STATUS -> - manga.keys - .sorted() - .map { Category(it, "", 0, 0) } - else -> throw IllegalStateException("Invalid group type $groupType") - } - - return manga to categories + }.toSortedMap(compareBy { it.order }) } fun runSync() { - favoritesSync.runSync(presenterScope) + favoritesSync.runSync(coroutineScope) + } + + fun onAcceptSyncWarning() { + unsortedPreferences.exhShowSyncIntro().set(false) + } + + fun openFavoritesSyncDialog() { + mutableState.update { + it.copy( + dialog = if (unsortedPreferences.exhShowSyncIntro().get()) { + Dialog.SyncFavoritesWarning + } else { + Dialog.SyncFavoritesConfirm + }, + ) + } } // SY <-- + + @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, + // SY --> + val filterLewd: Int, + // SY <-- + ) + + @Immutable + data class State( + val isLoading: Boolean = true, + val library: LibraryMap = emptyMap(), + val searchQuery: String? = null, + val selection: List = emptyList(), + val hasActiveFilters: Boolean = false, + val showCategoryTabs: Boolean = false, + val showMangaCount: Boolean = false, + val showMangaContinueButton: Boolean = false, + val dialog: Dialog? = null, + // SY --> + val showSyncExh: Boolean = false, + val ogCategories: List = emptyList(), + val groupType: Int = LibraryGroup.BY_DEFAULT, + // SY <-- + ) { + val selectionMode = selection.isNotEmpty() + + val categories = library.keys.toList() + + val libraryCount by lazy { + library + .flatMap { (_, v) -> v } + .distinctBy { it.libraryManga.manga.id } + .size + } + + // SY --> + val showCleanTitles: Boolean by lazy { + selection.any { + it.manga.isEhBasedManga() || + it.manga.source in nHentaiSourceIds + } + } + + val showAddToMangadex: Boolean by lazy { + selection.any { it.manga.source in mangaDexSourceIds } + } + // SY <-- + + fun getLibraryItemsByCategoryId(categoryId: Long): List { + return library.firstNotNullOf { (k, v) -> v.takeIf { k.id == categoryId } } + } + + fun getLibraryItemsByPage(page: Int): List { + 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) + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index 0aa2e74a6..ad2535c75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -35,7 +35,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 @@ -50,17 +49,11 @@ class LibrarySettingsSheet( init { filters = Filter(router.activity!!) - filters.onGroupClicked = onGroupClickListener - sort = Sort(router.activity!!) - sort.onGroupClicked = onGroupClickListener - display = Display(router.activity!!) - display.onGroupClicked = onGroupClickListener // SY --> grouping = Grouping(router.activity!!) - grouping.onGroupClicked = onGroupClickListener // SY <-- }