From 2b8d1bcc02994e75e1479b037ef50eb84ae674d7 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 23 Jul 2022 01:05:50 +0200 Subject: [PATCH] Use Compose for Library screen (#7557) - Move Pager to Compose - Move AppBar to Compose - Use Stable interface for state - Use pills for no. of manga in category instead of (x) --- app/build.gradle.kts | 3 + .../kanade/presentation/components/Badges.kt | 4 +- .../components/MangaBottomActionMenu.kt | 88 ++++ .../eu/kanade/presentation/components/Pill.kt | 38 ++ .../presentation/library/LibraryScreen.kt | 71 +++ .../presentation/library/LibraryState.kt | 32 ++ .../components/LibraryComfortableGrid.kt | 17 + .../library/components/LibraryCompactGrid.kt | 18 + .../library/components/LibraryContent.kt | 126 ++++++ .../components/LibraryCoverOnlyGrid.kt | 18 + .../library/components/LibraryList.kt | 15 + .../library/components/LibraryPager.kt | 96 ++++ .../library/components/LibraryTabs.kt | 77 ++++ .../library/components/LibraryToolbar.kt | 188 ++++++++ .../eu/kanade/presentation/theme/Color.kt | 12 + .../tachiyomi/ui/library/LibraryAdapter.kt | 208 --------- .../tachiyomi/ui/library/LibraryController.kt | 423 +++--------------- .../tachiyomi/ui/library/LibraryPresenter.kt | 180 +++++--- .../kanade/tachiyomi/ui/main/MainActivity.kt | 6 +- .../main/res/layout/library_controller.xml | 36 -- gradle/compose.versions.toml | 4 +- 21 files changed, 973 insertions(+), 687 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/Pill.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/LibraryState.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt create mode 100644 app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt create mode 100644 app/src/main/java/eu/kanade/presentation/theme/Color.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt delete mode 100644 app/src/main/res/layout/library_controller.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ed60cb6604..ffb9e62210 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -161,6 +161,8 @@ dependencies { implementation(compose.accompanist.webview) implementation(compose.accompanist.swiperefresh) implementation(compose.accompanist.flowlayout) + implementation(compose.accompanist.pager.core) + implementation(compose.accompanist.pager.indicators) implementation(androidx.paging.runtime) implementation(androidx.paging.compose) @@ -302,6 +304,7 @@ tasks { "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi", + "-opt-in=com.google.accompanist.pager.ExperimentalPagerApi" ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/Badges.kt b/app/src/main/java/eu/kanade/presentation/components/Badges.kt index 1ecbb6c8fe..d15a06640c 100644 --- a/app/src/main/java/eu/kanade/presentation/components/Badges.kt +++ b/app/src/main/java/eu/kanade/presentation/components/Badges.kt @@ -38,8 +38,8 @@ fun Badge( ) { Box( modifier = Modifier - .background(color) - .clip(shape), + .clip(shape) + .background(color), ) { Text( text = text, diff --git a/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt b/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt index b5166a6895..3e7dc940d3 100644 --- a/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt +++ b/app/src/main/java/eu/kanade/presentation/components/MangaBottomActionMenu.kt @@ -195,3 +195,91 @@ private fun RowScope.Button( } } } + +@Composable +fun LibraryBottomActionMenu( + visible: Boolean, + modifier: Modifier = Modifier, + onChangeCategoryClicked: (() -> Unit)?, + onMarkAsReadClicked: (() -> Unit)?, + onMarkAsUnreadClicked: (() -> Unit)?, + onDownloadClicked: (() -> Unit)?, + onDeleteClicked: (() -> Unit)?, +) { + AnimatedVisibility( + visible = visible, + enter = expandVertically(expandFrom = Alignment.Bottom), + exit = shrinkVertically(shrinkTowards = Alignment.Bottom), + ) { + val scope = rememberCoroutineScope() + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.large, + tonalElevation = 3.dp, + ) { + val haptic = LocalHapticFeedback.current + val confirm = remember { mutableStateListOf(false, false, false, false, false) } + var resetJob: Job? = remember { null } + val onLongClickItem: (Int) -> Unit = { toConfirmIndex -> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + (0 until 5).forEach { i -> confirm[i] = i == toConfirmIndex } + resetJob?.cancel() + resetJob = scope.launch { + delay(1000) + if (isActive) confirm[toConfirmIndex] = false + } + } + Row( + modifier = Modifier + .navigationBarsPadding() + .padding(horizontal = 8.dp, vertical = 12.dp), + ) { + if (onChangeCategoryClicked != null) { + Button( + title = stringResource(R.string.action_move_category), + icon = Icons.Default.BookmarkAdd, + toConfirm = confirm[0], + onLongClick = { onLongClickItem(0) }, + onClick = onChangeCategoryClicked, + ) + } + if (onMarkAsReadClicked != null) { + Button( + title = stringResource(R.string.action_mark_as_read), + icon = Icons.Default.DoneAll, + toConfirm = confirm[1], + onLongClick = { onLongClickItem(1) }, + onClick = onMarkAsReadClicked, + ) + } + if (onMarkAsUnreadClicked != null) { + Button( + title = stringResource(R.string.action_mark_as_unread), + icon = Icons.Default.RemoveDone, + toConfirm = confirm[2], + onLongClick = { onLongClickItem(2) }, + onClick = onMarkAsUnreadClicked, + ) + } + if (onDownloadClicked != null) { + Button( + title = stringResource(R.string.action_download), + icon = Icons.Outlined.Download, + toConfirm = confirm[3], + onLongClick = { onLongClickItem(3) }, + onClick = onDownloadClicked, + ) + } + if (onDeleteClicked != null) { + Button( + title = stringResource(R.string.action_delete), + icon = Icons.Outlined.Delete, + toConfirm = confirm[4], + onLongClick = { onLongClickItem(4) }, + onClick = onDeleteClicked, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/Pill.kt b/app/src/main/java/eu/kanade/presentation/components/Pill.kt new file mode 100644 index 0000000000..2cbcdee29b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/Pill.kt @@ -0,0 +1,38 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp + +@Composable +fun Pill( + text: String, + modifier: Modifier = Modifier, + color: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.background, + contentColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onBackground, + elevation: Dp = 1.dp, + fontSize: TextUnit = LocalTextStyle.current.fontSize, +) { + androidx.compose.material3.Surface( + modifier = modifier + .padding(start = 4.dp) + .clip(RoundedCornerShape(100)), + color = color, + contentColor = contentColor, + tonalElevation = elevation, + ) { + Text( + text = text, + modifier = Modifier.padding(6.dp, 1.dp), + fontSize = fontSize, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt new file mode 100644 index 0000000000..f76dd149db --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt @@ -0,0 +1,71 @@ +package eu.kanade.presentation.library + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import eu.kanade.presentation.components.LibraryBottomActionMenu +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.library.components.LibraryContent +import eu.kanade.presentation.library.components.LibraryToolbar +import eu.kanade.tachiyomi.ui.library.LibraryPresenter + +@Composable +fun LibraryScreen( + presenter: LibraryPresenter, + onMangaClicked: (Long) -> Unit, + onGlobalSearchClicked: () -> Unit, + onChangeCategoryClicked: () -> Unit, + onMarkAsReadClicked: () -> Unit, + onMarkAsUnreadClicked: () -> Unit, + onDownloadClicked: () -> Unit, + onDeleteClicked: () -> Unit, + onClickUnselectAll: () -> Unit, + onClickSelectAll: () -> Unit, + onClickInvertSelection: () -> Unit, + onClickFilter: () -> Unit, + onClickRefresh: () -> Unit, +) { + Scaffold( + topBar = { + val title by presenter.getToolbarTitle() + LibraryToolbar( + state = presenter, + title = title, + onClickUnselectAll = onClickUnselectAll, + onClickSelectAll = onClickSelectAll, + onClickInvertSelection = onClickInvertSelection, + onClickFilter = onClickFilter, + onClickRefresh = onClickRefresh, + ) + }, + bottomBar = { + LibraryBottomActionMenu( + visible = presenter.selectionMode, + onChangeCategoryClicked = onChangeCategoryClicked, + onMarkAsReadClicked = onMarkAsReadClicked, + onMarkAsUnreadClicked = onMarkAsUnreadClicked, + onDownloadClicked = onDownloadClicked, + onDeleteClicked = onDeleteClicked, + ) + }, + ) { paddingValues -> + LibraryContent( + state = presenter, + contentPadding = paddingValues, + currentPage = presenter.activeCategory, + isLibraryEmpty = presenter.loadedManga.isEmpty(), + showPageTabs = presenter.tabVisibility, + showMangaCount = presenter.mangaCountVisibility, + onChangeCurrentPage = { presenter.activeCategory = it }, + onMangaClicked = onMangaClicked, + onToggleSelection = { presenter.toggleSelection(it) }, + onRefresh = onClickRefresh, + onGlobalSearchClicked = onGlobalSearchClicked, + getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) }, + getDisplayModeForPage = { presenter.getDisplayMode(index = it) }, + getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) }, + getLibraryForPage = { presenter.getMangaForCategory(page = it) }, + isIncognitoMode = presenter.isIncognitoMode, + isDownloadOnly = presenter.isDownloadOnly, + ) + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt b/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt new file mode 100644 index 0000000000..306509d64b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/LibraryState.kt @@ -0,0 +1,32 @@ +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.tachiyomi.data.database.models.LibraryManga + +@Stable +interface LibraryState { + val isLoading: Boolean + val categories: List + var searchQuery: String? + val selection: List + val selectionMode: Boolean + var hasActiveFilters: Boolean +} + +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) +} 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 bf1f774561..d8fb824a50 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 @@ -3,14 +3,19 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import eu.kanade.domain.manga.model.MangaCover +import eu.kanade.presentation.components.TextButton +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.ui.library.LibraryItem @@ -21,10 +26,22 @@ fun LibraryComfortableGrid( selection: List, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, + searchQuery: String?, + onGlobalSearchClicked: () -> Unit, ) { LazyLibraryGrid( columns = columns, ) { + item(span = { GridItemSpan(maxLineSpan) }) { + if (searchQuery.isNullOrEmpty().not()) { + TextButton(onClick = onGlobalSearchClicked) { + Text( + text = stringResource(R.string.action_global_search_query, searchQuery!!), + modifier = Modifier.zIndex(99f), + ) + } + } + } items( items = items, key = { 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 4fc8ace80a..302f61976a 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalTextStyle @@ -17,8 +18,12 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import eu.kanade.presentation.components.TextButton +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.ui.library.LibraryItem @@ -29,10 +34,23 @@ fun LibraryCompactGrid( selection: List, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, + searchQuery: String?, + onGlobalSearchClicked: () -> Unit, ) { LazyLibraryGrid( columns = columns, ) { + item(span = { GridItemSpan(maxLineSpan) }) { + if (searchQuery.isNullOrEmpty().not()) { + TextButton(onClick = onGlobalSearchClicked) { + Text( + text = stringResource(R.string.action_global_search_query, searchQuery!!), + modifier = Modifier.zIndex(99f), + ) + } + } + } + items( items = items, key = { 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 new file mode 100644 index 0000000000..1be3ea0fa0 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -0,0 +1,126 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import com.google.accompanist.pager.rememberPagerState +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import eu.kanade.core.prefs.PreferenceMutableState +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.LoadingScreen +import eu.kanade.presentation.components.SwipeRefreshIndicator +import eu.kanade.presentation.library.LibraryState +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryItem +import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting +import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.widget.EmptyView + +@Composable +fun LibraryContent( + state: LibraryState, + contentPadding: PaddingValues, + currentPage: Int, + isLibraryEmpty: Boolean, + isDownloadOnly: Boolean, + isIncognitoMode: Boolean, + showPageTabs: Boolean, + showMangaCount: Boolean, + onChangeCurrentPage: (Int) -> Unit, + onMangaClicked: (Long) -> Unit, + onToggleSelection: (LibraryManga) -> Unit, + onRefresh: () -> Unit, + onGlobalSearchClicked: () -> Unit, + getNumberOfMangaForCategory: @Composable (Long) -> State, + getDisplayModeForPage: @Composable (Int) -> State, + getColumnsForOrientation: (Boolean) -> PreferenceMutableState, + getLibraryForPage: @Composable (Int) -> State>, +) { + val nestedScrollInterop = rememberNestedScrollInteropConnection() + + val pagerState = rememberPagerState(currentPage) + + val categories = state.categories + + if (categories.isEmpty()) { + LoadingScreen() + return + } + + Column( + modifier = Modifier.padding(contentPadding), + ) { + if (showPageTabs && categories.size > 1) { + LibraryTabs( + state = pagerState, + categories = state.categories, + showMangaCount = showMangaCount, + getNumberOfMangaForCategory = getNumberOfMangaForCategory, + isDownloadOnly = isDownloadOnly, + isIncognitoMode = isIncognitoMode, + ) + } + + val onClickManga = { manga: LibraryManga -> + if (state.selectionMode.not()) { + onMangaClicked(manga.id!!) + } else { + onToggleSelection(manga) + } + } + val onLongClickManga = { manga: LibraryManga -> + onToggleSelection(manga) + } + + SwipeRefresh( + state = rememberSwipeRefreshState(isRefreshing = false), + modifier = Modifier.nestedScroll(nestedScrollInterop), + onRefresh = onRefresh, + indicator = { s, trigger -> + SwipeRefreshIndicator( + state = s, + refreshTriggerDistance = trigger, + ) + }, + ) { + if (state.searchQuery.isNullOrEmpty() && isLibraryEmpty) { + val context = LocalContext.current + EmptyScreen( + R.string.information_empty_library, + listOf( + EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) { + context.openInBrowser("https://tachiyomi.org/help/guides/getting-started") + }, + ), + ) + return@SwipeRefresh + } + + LibraryPager( + state = pagerState, + pageCount = categories.size, + selectedManga = state.selection, + getDisplayModeForPage = getDisplayModeForPage, + getColumnsForOrientation = getColumnsForOrientation, + getLibraryForPage = getLibraryForPage, + onClickManga = onClickManga, + onLongClickManga = onLongClickManga, + onGlobalSearchClicked = onGlobalSearchClicked, + searchQuery = state.searchQuery, + ) + } + + LaunchedEffect(pagerState.currentPage) { + onChangeCurrentPage(pagerState.currentPage) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt index 8f6336f556..391be95adb 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryCoverOnlyGrid.kt @@ -1,9 +1,15 @@ package eu.kanade.presentation.library.components import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.zIndex +import eu.kanade.presentation.components.TextButton +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.ui.library.LibraryItem @@ -14,10 +20,22 @@ fun LibraryCoverOnlyGrid( selection: List, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, + searchQuery: String?, + onGlobalSearchClicked: () -> Unit, ) { LazyLibraryGrid( columns = columns, ) { + item(span = { GridItemSpan(maxLineSpan) }) { + if (searchQuery.isNullOrEmpty().not()) { + TextButton(onClick = onGlobalSearchClicked) { + Text( + text = stringResource(R.string.action_global_search_query, searchQuery!!), + modifier = Modifier.zIndex(99f), + ) + } + } + } items( items = items, key = { 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 9543335a1f..9e7ebf425e 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 @@ -17,9 +17,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import eu.kanade.domain.manga.model.MangaCover import eu.kanade.presentation.components.Badge import eu.kanade.presentation.components.BadgeGroup +import eu.kanade.presentation.components.TextButton import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.selectedBackground import eu.kanade.presentation.util.verticalPadding @@ -33,10 +35,23 @@ fun LibraryList( selection: List, onClick: (LibraryManga) -> Unit, onLongClick: (LibraryManga) -> Unit, + searchQuery: String?, + onGlobalSearchClicked: () -> Unit, ) { LazyColumn( contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { + item { + if (searchQuery.isNullOrEmpty().not()) { + TextButton(onClick = onGlobalSearchClicked) { + Text( + text = stringResource(R.string.action_global_search_query, searchQuery!!), + modifier = Modifier.zIndex(99f), + ) + } + } + } + items( items = items, key = { 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 new file mode 100644 index 0000000000..b2934eed6a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt @@ -0,0 +1,96 @@ +package eu.kanade.presentation.library.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import eu.kanade.core.prefs.PreferenceMutableState +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import eu.kanade.tachiyomi.ui.library.LibraryItem +import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting + +@Composable +fun LibraryPager( + state: PagerState, + pageCount: Int, + selectedManga: List, + searchQuery: String?, + onGlobalSearchClicked: () -> Unit, + getDisplayModeForPage: @Composable (Int) -> State, + getColumnsForOrientation: (Boolean) -> PreferenceMutableState, + getLibraryForPage: @Composable (Int) -> State>, + onClickManga: (LibraryManga) -> Unit, + onLongClickManga: (LibraryManga) -> Unit, +) { + HorizontalPager( + count = pageCount, + modifier = Modifier.fillMaxSize(), + state = state, + verticalAlignment = Alignment.Top, + ) { page -> + val library by getLibraryForPage(page) + val displayMode by getDisplayModeForPage(page) + val columns by if (displayMode != DisplayModeSetting.LIST) { + val configuration = LocalConfiguration.current + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + remember(isLandscape) { getColumnsForOrientation(isLandscape) } + } else { + remember { mutableStateOf(0) } + } + + when (displayMode) { + DisplayModeSetting.LIST -> { + LibraryList( + items = library, + selection = selectedManga, + onClick = onClickManga, + onLongClick = onLongClickManga, + searchQuery = searchQuery, + onGlobalSearchClicked = onGlobalSearchClicked, + ) + } + DisplayModeSetting.COMPACT_GRID -> { + LibraryCompactGrid( + items = library, + columns = columns, + selection = selectedManga, + onClick = onClickManga, + onLongClick = onLongClickManga, + searchQuery = searchQuery, + onGlobalSearchClicked = onGlobalSearchClicked, + ) + } + DisplayModeSetting.COMFORTABLE_GRID -> { + LibraryComfortableGrid( + items = library, + columns = columns, + selection = selectedManga, + onClick = onClickManga, + onLongClick = onLongClickManga, + searchQuery = searchQuery, + onGlobalSearchClicked = onGlobalSearchClicked, + ) + } + DisplayModeSetting.COVER_ONLY_GRID -> { + LibraryCoverOnlyGrid( + items = library, + columns = columns, + selection = selectedManga, + onClick = onClickManga, + onLongClick = onLongClickManga, + 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 new file mode 100644 index 0000000000..b479cbdb2a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -0,0 +1,77 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.ui.Alignment +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.pager.PagerState +import eu.kanade.domain.category.model.Category +import eu.kanade.presentation.components.DownloadedOnlyModeBanner +import eu.kanade.presentation.components.IncognitoModeBanner +import eu.kanade.presentation.components.Pill +import kotlinx.coroutines.launch + +@Composable +fun LibraryTabs( + state: PagerState, + categories: List, + showMangaCount: Boolean, + isDownloadOnly: Boolean, + isIncognitoMode: Boolean, + getNumberOfMangaForCategory: @Composable (Long) -> State, +) { + val scope = rememberCoroutineScope() + + val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f + + Column { + ScrollableTabRow( + selectedTabIndex = state.currentPage, + edgePadding = 0.dp, + ) { + categories.forEachIndexed { index, category -> + val count by if (showMangaCount) { + getNumberOfMangaForCategory(category.id) + } else { + remember { mutableStateOf(null) } + } + Tab( + selected = state.currentPage == index, + onClick = { scope.launch { state.animateScrollToPage(index) } }, + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = category.name) + if (count != null) { + Pill( + text = "$count", + color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha), + fontSize = 10.sp, + ) + } + } + }, + ) + } + } + if (isDownloadOnly) { + DownloadedOnlyModeBanner() + } + if (isIncognitoMode) { + IncognitoModeBanner() + } + } +} 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 new file mode 100644 index 0000000000..650981a16c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryToolbar.kt @@ -0,0 +1,188 @@ +package eu.kanade.presentation.library.components + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.FlipToBack +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.SelectAll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp +import eu.kanade.presentation.components.Pill +import eu.kanade.presentation.library.LibraryState +import eu.kanade.presentation.theme.active +import kotlinx.coroutines.delay + +@Composable +fun LibraryToolbar( + state: LibraryState, + title: LibraryToolbarTitle, + onClickUnselectAll: () -> Unit, + onClickSelectAll: () -> Unit, + onClickInvertSelection: () -> Unit, + onClickFilter: () -> Unit, + onClickRefresh: () -> Unit, +) = when { + state.searchQuery != null -> LibrarySearchToolbar( + searchQuery = state.searchQuery!!, + onChangeSearchQuery = { state.searchQuery = it }, + onClickCloseSearch = { state.searchQuery = null }, + ) + state.selectionMode -> LibrarySelectionToolbar( + state = state, + onClickUnselectAll = onClickUnselectAll, + onClickSelectAll = onClickSelectAll, + onClickInvertSelection = onClickInvertSelection, + ) + else -> LibraryRegularToolbar( + title = title, + hasFilters = state.hasActiveFilters, + onClickSearch = { state.searchQuery = "" }, + onClickFilter = onClickFilter, + onClickRefresh = onClickRefresh, + ) +} + +@Composable +fun LibraryRegularToolbar( + title: LibraryToolbarTitle, + hasFilters: Boolean, + onClickSearch: () -> Unit, + onClickFilter: () -> Unit, + onClickRefresh: () -> Unit, +) { + val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f + val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current + SmallTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = title.text, + maxLines = 1, + modifier = Modifier.weight(1f, false), + overflow = TextOverflow.Ellipsis, + ) + if (title.numberOfManga != null) { + Pill( + text = "${title.numberOfManga}", + color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha), + fontSize = 14.sp, + ) + } + } + }, + actions = { + IconButton(onClick = onClickSearch) { + Icon(Icons.Outlined.Search, contentDescription = "search") + } + IconButton(onClick = onClickFilter) { + Icon(Icons.Outlined.FilterList, contentDescription = "search", tint = filterTint) + } + IconButton(onClick = onClickRefresh) { + Icon(Icons.Outlined.Refresh, contentDescription = "search") + } + }, + ) +} + +@Composable +fun LibrarySelectionToolbar( + state: LibraryState, + onClickUnselectAll: () -> Unit, + onClickSelectAll: () -> Unit, + onClickInvertSelection: () -> Unit, +) { + val backgroundColor by TopAppBarDefaults.smallTopAppBarColors().containerColor(1f) + SmallTopAppBar( + modifier = Modifier + .drawBehind { + drawRect(backgroundColor.copy(alpha = 1f)) + } + .statusBarsPadding(), + navigationIcon = { + IconButton(onClick = onClickUnselectAll) { + Icon(Icons.Outlined.Close, contentDescription = "close") + } + }, + title = { + Text(text = "${state.selection.size}") + }, + actions = { + IconButton(onClick = onClickSelectAll) { + Icon(Icons.Outlined.SelectAll, contentDescription = "search") + } + IconButton(onClick = onClickInvertSelection) { + Icon(Icons.Outlined.FlipToBack, contentDescription = "invert") + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent, + ), + ) +} + +@Composable +fun LibrarySearchToolbar( + searchQuery: String, + onChangeSearchQuery: (String) -> Unit, + onClickCloseSearch: () -> Unit, +) { + val focusRequester = remember { FocusRequester.Default } + SmallTopAppBar( + modifier = Modifier.statusBarsPadding(), + navigationIcon = { + IconButton(onClick = onClickCloseSearch) { + Icon(Icons.Outlined.ArrowBack, contentDescription = "back") + } + }, + title = { + BasicTextField( + value = searchQuery, + onValueChange = onChangeSearchQuery, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground), + singleLine = true, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + ) + LaunchedEffect(focusRequester) { + // TODO: https://issuetracker.google.com/issues/204502668 + delay(100) + focusRequester.requestFocus() + } + }, + ) +} + +data class LibraryToolbarTitle( + val text: String, + val numberOfManga: Int? = null, +) diff --git a/app/src/main/java/eu/kanade/presentation/theme/Color.kt b/app/src/main/java/eu/kanade/presentation/theme/Color.kt new file mode 100644 index 0000000000..ea12448dfb --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/theme/Color.kt @@ -0,0 +1,12 @@ +package eu.kanade.presentation.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val ColorScheme.active: Color + @Composable + get() { + return if (isSystemInDarkTheme()) Color(255, 235, 59) else Color(255, 193, 7) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt deleted file mode 100644 index cb50142c39..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryAdapter.kt +++ /dev/null @@ -1,208 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.rememberNestedScrollInteropConnection -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState -import eu.kanade.domain.category.model.Category -import eu.kanade.presentation.components.SwipeRefreshIndicator -import eu.kanade.presentation.library.components.LibraryComfortableGrid -import eu.kanade.presentation.library.components.LibraryCompactGrid -import eu.kanade.presentation.library.components.LibraryCoverOnlyGrid -import eu.kanade.presentation.library.components.LibraryList -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.ComposeControllerBinding -import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.setComposeContent -import eu.kanade.tachiyomi.widget.RecyclerViewPagerAdapter -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * This adapter stores the categories from the library, used with a ViewPager. - * - * @constructor creates an instance of the adapter. - */ -class LibraryAdapter( - private val presenter: LibraryPresenter, - private val onClickManga: (LibraryManga) -> Unit, - private val preferences: PreferencesHelper = Injekt.get(), -) : RecyclerViewPagerAdapter() { - - /** - * The categories to bind in the adapter. - */ - var categories: List = mutableStateListOf() - private set - - /** - * The number of manga in each category. - * List order must be the same as [categories] - */ - private var itemsPerCategory: List = emptyList() - - private var boundViews = arrayListOf() - - /** - * Pair of category and size of category - */ - fun updateCategories(new: List>) { - var updated = false - - val newCategories = new.map { it.first } - if (categories != newCategories) { - categories = newCategories - updated = true - } - - val newItemsPerCategory = new.map { it.second } - if (itemsPerCategory !== newItemsPerCategory) { - itemsPerCategory = newItemsPerCategory - updated = true - } - - if (updated) { - notifyDataSetChanged() - } - } - - /** - * Creates a new view for this adapter. - * - * @return a new view. - */ - override fun inflateView(container: ViewGroup, viewType: Int): View { - val binding = ComposeControllerBinding.inflate(LayoutInflater.from(container.context), container, false) - return binding.root - } - - /** - * Binds a view with a position. - * - * @param view the view to bind. - * @param position the position in the adapter. - */ - override fun bindView(view: View, position: Int) { - (view as ComposeView).apply { - setComposeContent { - val nestedScrollInterop = rememberNestedScrollInteropConnection() - - val category = presenter.categories[position] - val displayMode = presenter.getDisplayMode(index = position) - val mangaList by presenter.getMangaForCategory(categoryId = category.id) - - val onClickManga = { manga: LibraryManga -> - if (presenter.hasSelection().not()) { - onClickManga(manga) - } else { - presenter.toggleSelection(manga) - } - } - val onLongClickManga = { manga: LibraryManga -> - presenter.toggleSelection(manga) - } - - SwipeRefresh( - modifier = Modifier.nestedScroll(nestedScrollInterop), - state = rememberSwipeRefreshState(isRefreshing = false), - onRefresh = { - if (LibraryUpdateService.start(context, category)) { - context.toast(R.string.updating_category) - } - }, - indicator = { s, trigger -> - SwipeRefreshIndicator( - state = s, - refreshTriggerDistance = trigger, - ) - }, - ) { - when (displayMode) { - DisplayModeSetting.LIST -> { - LibraryList( - items = mangaList, - selection = presenter.selection, - onClick = onClickManga, - onLongClick = onLongClickManga, - ) - } - DisplayModeSetting.COMPACT_GRID -> { - LibraryCompactGrid( - items = mangaList, - columns = presenter.columns, - selection = presenter.selection, - onClick = onClickManga, - onLongClick = onLongClickManga, - ) - } - DisplayModeSetting.COMFORTABLE_GRID -> { - LibraryComfortableGrid( - items = mangaList, - columns = presenter.columns, - selection = presenter.selection, - onClick = onClickManga, - onLongClick = onLongClickManga, - ) - } - DisplayModeSetting.COVER_ONLY_GRID -> { - LibraryCoverOnlyGrid( - items = mangaList, - columns = presenter.columns, - selection = presenter.selection, - onClick = onClickManga, - onLongClick = onLongClickManga, - ) - } - } - } - } - } - boundViews.add(view) - } - - /** - * Recycles a view. - * - * @param view the view to recycle. - * @param position the position in the adapter. - */ - override fun recycleView(view: View, position: Int) { - boundViews.remove(view) - } - - /** - * Returns the number of categories. - * - * @return the number of categories or 0 if the list is null. - */ - override fun getCount(): Int { - return categories.size - } - - /** - * Returns the title to display for a category. - * - * @param position the position of the element. - * @return the title to display. - */ - override fun getPageTitle(position: Int): CharSequence { - return if (!preferences.categoryNumberOfItems().get()) { - categories[position].name - } else { - categories[position].let { "${it.name} (${itemsPerCategory[position]})" } - } - } - - override fun getViewType(position: Int): Int = -1 -} 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 005ef4bff8..19e521e8ef 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,240 +1,119 @@ package eu.kanade.tachiyomi.ui.library -import android.content.res.Configuration import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem import android.view.View -import androidx.appcompat.view.ActionMode -import androidx.core.view.isVisible +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.fredporciuncula.flow.preferences.Preference -import com.google.android.material.tabs.TabLayout -import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.toDbCategory import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.presentation.library.LibraryScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.library.LibraryUpdateService -import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.databinding.LibraryControllerBinding +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController import eu.kanade.tachiyomi.ui.base.controller.RootController -import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController -import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI -import eu.kanade.tachiyomi.util.preference.asHotFlow -import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.widget.ActionModeWithToolbar -import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.clicks -import reactivecircus.flowbinding.viewpager.pageSelections -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.concurrent.TimeUnit class LibraryController( bundle: Bundle? = null, - private val preferences: PreferencesHelper = Injekt.get(), -) : SearchableNucleusController(bundle), +) : FullComposeController(bundle), RootController, - TabbedController, - ActionModeWithToolbar.Callback, ChangeMangaCategoriesDialog.Listener, DeleteLibraryMangasDialog.Listener { - /** - * Position of the active category. - */ - private var activeCategory: Int = preferences.lastUsedCategory().get() - - /** - * Action mode for selections. - */ - private var actionMode: ActionModeWithToolbar? = null - - private var mangaMap: LibraryMap = emptyMap() - - private var adapter: LibraryAdapter? = null - /** * Sheet containing filter/sort/display items. */ private var settingsSheet: LibrarySettingsSheet? = null - private var tabsVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) - - private var mangaCountVisibilityRelay: BehaviorRelay = BehaviorRelay.create(false) - - private var tabsVisibilitySubscription: Subscription? = null - - private var mangaCountVisibilitySubscription: Subscription? = null - init { - setHasOptionsMenu(true) retainViewMode = RetainViewMode.RETAIN_DETACH } - private var currentTitle: String? = null - set(value) { - if (field != value) { - field = value - setTitle() - } - } + override fun createPresenter(): LibraryPresenter = LibraryPresenter() - override fun getTitle(): String? { - return currentTitle ?: resources?.getString(R.string.label_library) + @Composable + override fun ComposeContent() { + val context = LocalContext.current + LibraryScreen( + presenter = presenter, + onMangaClicked = ::openManga, + onGlobalSearchClicked = { + router.pushController(GlobalSearchController(presenter.query)) + }, + onChangeCategoryClicked = ::showMangaCategoriesDialog, + onMarkAsReadClicked = { markReadStatus(true) }, + onMarkAsUnreadClicked = { markReadStatus(false) }, + onDownloadClicked = ::downloadUnreadChapters, + onDeleteClicked = ::showDeleteMangaDialog, + onClickFilter = ::showSettingsSheet, + onClickRefresh = { + if (LibraryUpdateService.start(context)) { + context.toast(R.string.updating_library) + } + }, + onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) }, + onClickSelectAll = { presenter.selectAll(presenter.activeCategory) }, + onClickUnselectAll = ::clearSelection, + ) + LaunchedEffect(presenter.selectionMode) { + val activity = (activity as? MainActivity) ?: return@LaunchedEffect + activity.showBottomNav(presenter.selectionMode.not()) + } } - private fun updateTitle() { - val showCategoryTabs = preferences.categoryTabs().get() - val currentCategory = adapter?.categories?.getOrNull(binding.libraryPager.currentItem) - - var title = if (showCategoryTabs) { - resources?.getString(R.string.label_library) - } else { - currentCategory?.name + override fun handleBack(): Boolean { + if (presenter.selection.isNotEmpty()) { + presenter.clearSelection() + return true } - - if (preferences.categoryNumberOfItems().get()) { - if (!showCategoryTabs || adapter?.categories?.size == 1) { - title += " (${mangaMap[currentCategory?.id]?.size ?: 0})" - } - } - - currentTitle = title + return false } - override fun createPresenter(): LibraryPresenter { - return LibraryPresenter() - } - - override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater) - override fun onViewCreated(view: View) { super.onViewCreated(view) - adapter = LibraryAdapter( - presenter = presenter, - onClickManga = { - openManga(it.id!!) - }, - ) - - getColumnsPreferenceForCurrentOrientation() - .asHotFlow { presenter.columns = it } - .launchIn(viewScope) - - binding.libraryPager.adapter = adapter - binding.libraryPager.pageSelections() - .drop(1) - .onEach { - preferences.lastUsedCategory().set(it) - activeCategory = it - updateTitle() - } - .launchIn(viewScope) - - if (adapter!!.categories.isNotEmpty()) { - createActionModeIfNeeded() - } - settingsSheet = LibrarySettingsSheet(router) { group -> when (group) { is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged() is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged() - is LibrarySettingsSheet.Display.DisplayGroup -> { - val delay = if (preferences.categorizedDisplaySettings().get()) 125L else 0L - - Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) - .subscribe { - reattachAdapter() - } - } + is LibrarySettingsSheet.Display.DisplayGroup -> {} is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged() - is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged() + is LibrarySettingsSheet.Display.TabsGroup -> {} // onTabsSettingsChanged() } } - - binding.btnGlobalSearch.clicks() - .onEach { - router.pushController(GlobalSearchController(presenter.query)) - } - .launchIn(viewScope) - } - - private fun getColumnsPreferenceForCurrentOrientation(): Preference { - return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) { - preferences.portraitColumns() - } else { - preferences.landscapeColumns() - } } override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (type.isEnter) { - (activity as? MainActivity)?.binding?.tabs?.setupWithViewPager(binding.libraryPager) presenter.subscribeLibrary() } } override fun onDestroyView(view: View) { - destroyActionModeIfNeeded() - adapter = null settingsSheet?.sheetScope?.cancel() settingsSheet = null - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null super.onDestroyView(view) } - override fun configureTabs(tabs: TabLayout): Boolean { - with(tabs) { - isVisible = false - tabGravity = TabLayout.GRAVITY_START - tabMode = TabLayout.MODE_SCROLLABLE - } - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = tabsVisibilityRelay.subscribe { visible -> - tabs.isVisible = visible - } - mangaCountVisibilitySubscription?.unsubscribe() - mangaCountVisibilitySubscription = mangaCountVisibilityRelay.subscribe { - adapter?.notifyDataSetChanged() - } - - return false - } - - override fun cleanupTabs(tabs: TabLayout) { - tabsVisibilitySubscription?.unsubscribe() - tabsVisibilitySubscription = null - } - fun showSettingsSheet() { - if (adapter?.categories?.isNotEmpty() == true) { - adapter?.categories?.get(binding.libraryPager.currentItem)?.let { category -> + if (presenter.categories.isNotEmpty()) { + presenter.categories[presenter.activeCategory].let { category -> settingsSheet?.show(category.toDbCategory()) } } else { @@ -242,61 +121,6 @@ class LibraryController( } } - fun onNextLibraryUpdate(categories: List, mangaMap: LibraryMap) { - val view = view ?: return - val adapter = adapter ?: return - - // Show empty view if needed - if (mangaMap.isNotEmpty()) { - binding.emptyView.hide() - } else { - binding.emptyView.show( - R.string.information_empty_library, - listOf( - EmptyView.Action(R.string.getting_started_guide, R.drawable.ic_help_24dp) { - activity?.openInBrowser("https://tachiyomi.org/help/guides/getting-started") - }, - ), - ) - (activity as? MainActivity)?.ready = true - } - - // Get the current active category. - val activeCat = if (adapter.categories.isNotEmpty()) { - binding.libraryPager.currentItem - } else { - activeCategory - } - - // Set the categories - adapter.updateCategories(categories.map { it to (mangaMap[it.id]?.size ?: 0) }) - - // Restore active category. - binding.libraryPager.setCurrentItem(activeCat, false) - - // Trigger display of tabs - onTabsSettingsChanged(firstLaunch = true) - - // Delay the scroll position to allow the view to be properly measured. - view.post { - if (isAttached) { - (activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true) - } - } - - presenter.loadedManga.clear() - mangaMap.forEach { - presenter.loadedManga[it.key] = it.value - } - presenter.loadedMangaFlow.value = presenter.loadedManga - - // Send the manga map to child fragments after the adapter is updated. - this.mangaMap = mangaMap - - // Finally update the title - updateTitle() - } - private fun onFilterChanged() { presenter.requestFilterUpdate() activity?.invalidateOptionsMenu() @@ -306,146 +130,17 @@ class LibraryController( presenter.requestBadgesUpdate() } - private fun onTabsSettingsChanged(firstLaunch: Boolean = false) { - if (!firstLaunch) { - mangaCountVisibilityRelay.call(preferences.categoryNumberOfItems().get()) - } - tabsVisibilityRelay.call(preferences.categoryTabs().get() && (adapter?.categories?.size ?: 0) > 1) - updateTitle() - } - private fun onSortChanged() { presenter.requestSortUpdate() } - /** - * Reattaches the adapter to the view pager to recreate fragments - */ - private fun reattachAdapter() { - val adapter = adapter ?: return - - val position = binding.libraryPager.currentItem - - adapter.recycle = false - binding.libraryPager.adapter = adapter - binding.libraryPager.currentItem = position - adapter.recycle = true - } - - fun createActionModeIfNeeded() { - val activity = activity - if (actionMode == null && activity is MainActivity) { - actionMode = activity.startActionModeAndToolbar(this) - activity.showBottomNav(false) - } - } - - private fun destroyActionModeIfNeeded() { - actionMode?.finish() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search) - // Mutate the filter icon because it needs to be tinted and the resource is shared. - menu.findItem(R.id.action_filter).icon?.mutate() - } - fun search(query: String) { - presenter.query = query - } - - private fun performSearch() { - if (presenter.query.isNotEmpty()) { - binding.btnGlobalSearch.isVisible = true - binding.btnGlobalSearch.text = - resources?.getString(R.string.action_global_search_query, presenter.query) - } else { - binding.btnGlobalSearch.isVisible = false - } + presenter.searchQuery = query } override fun onPrepareOptionsMenu(menu: Menu) { val settingsSheet = settingsSheet ?: return - - val filterItem = menu.findItem(R.id.action_filter) - - // Tint icon if there's a filter active - if (settingsSheet.filters.hasActiveFilters()) { - val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) - filterItem.icon?.setTint(filterColor) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_search -> expandActionViewFromInteraction = true - R.id.action_filter -> showSettingsSheet() - R.id.action_update_library -> { - activity?.let { - if (LibraryUpdateService.start(it)) { - it.toast(R.string.updating_library) - } - } - } - } - - return super.onOptionsItemSelected(item) - } - - /** - * Invalidates the action mode, forcing it to refresh its content. - */ - fun invalidateActionMode() { - actionMode?.invalidate() - } - - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - mode.menuInflater.inflate(R.menu.generic_selection, menu) - return true - } - - override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) { - menuInflater.inflate(R.menu.library_selection, menu) - } - - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - val count = presenter.selection.size - if (count == 0) { - // Destroy action mode if there are no items selected. - destroyActionModeIfNeeded() - } else { - mode.title = count.toString() - } - return true - } - - override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) { - if (presenter.hasSelection().not()) return - toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible = - presenter.selection.any { presenter.loadedManga.values.any { it.any { it.isLocal } } } - } - - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_move_to_category -> showMangaCategoriesDialog() - R.id.action_download_unread -> downloadUnreadChapters() - R.id.action_mark_as_read -> markReadStatus(true) - R.id.action_mark_as_unread -> markReadStatus(false) - R.id.action_delete -> showDeleteMangaDialog() - R.id.action_select_all -> selectAllCategoryManga() - R.id.action_select_inverse -> selectInverseCategoryManga() - else -> return false - } - return true - } - - override fun onDestroyActionMode(mode: ActionMode) { - // Clear all the manga selections and notify child views. - presenter.clearSelection() - - (activity as? MainActivity)?.showBottomNav(true) - - actionMode = null + presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters() } private fun openManga(mangaId: Long) { @@ -461,7 +156,6 @@ class LibraryController( */ fun clearSelection() { presenter.clearSelection() - invalidateActionMode() } /** @@ -496,13 +190,13 @@ class LibraryController( private fun downloadUnreadChapters() { val mangas = presenter.selection.toList() presenter.downloadUnreadChapters(mangas.mapNotNull { it.toDomainManga() }) - destroyActionModeIfNeeded() + presenter.clearSelection() } private fun markReadStatus(read: Boolean) { val mangas = presenter.selection.toList() presenter.markReadStatus(mangas.mapNotNull { it.toDomainManga() }, read) - destroyActionModeIfNeeded() + presenter.clearSelection() } private fun showDeleteMangaDialog() { @@ -512,28 +206,11 @@ class LibraryController( override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { presenter.setMangaCategories(mangas, addCategories, removeCategories) - destroyActionModeIfNeeded() + presenter.clearSelection() } override fun deleteMangas(mangas: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { presenter.removeMangas(mangas.map { it.toDbManga() }, deleteFromLibrary, deleteChapters) - destroyActionModeIfNeeded() - } - - private fun selectAllCategoryManga() { - presenter.selectAll(binding.libraryPager.currentItem) - } - - private fun selectInverseCategoryManga() { - presenter.invertSelection(binding.libraryPager.currentItem) - } - - override fun onSearchViewQueryTextChange(newText: String?) { - // Ignore events if this controller isn't at the top to avoid query being reset - if (router.backstack.lastOrNull()?.controller == this) { - presenter.query = newText ?: "" - presenter.searchQuery = newText ?: "" - performSearch() - } + presenter.clearSelection() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 4e319aa279..8ba8d325d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -4,13 +4,15 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource import androidx.compose.ui.util.fastAny import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.core.prefs.PreferenceMutableState +import eu.kanade.core.util.asFlow import eu.kanade.core.util.asObservable import eu.kanade.data.DatabaseHandler import eu.kanade.domain.category.interactor.GetCategories @@ -25,6 +27,10 @@ import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.MangaUpdate import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.track.interactor.GetTracks +import eu.kanade.presentation.library.LibraryState +import eu.kanade.presentation.library.LibraryStateImpl +import eu.kanade.presentation.library.components.LibraryToolbarTitle +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.toDomainManga @@ -39,14 +45,16 @@ import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.util.lang.combineLatest -import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import rx.Observable -import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt @@ -70,6 +78,7 @@ typealias LibraryMap = Map> * Presenter of [LibraryController]. */ class LibraryPresenter( + private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl, private val handler: DatabaseHandler = Injekt.get(), private val getLibraryManga: GetLibraryManga = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), @@ -83,31 +92,27 @@ class LibraryPresenter( private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), -) : BasePresenter() { +) : BasePresenter(), LibraryState by state { private val context = preferences.context - /** - * Categories of the library. - */ - var categories: List = mutableStateListOf() + var loadedManga by mutableStateOf(emptyMap>()) private set - var loadedManga = mutableStateMapOf>() - private set - - val loadedMangaFlow = MutableStateFlow(loadedManga) - - var searchQuery by mutableStateOf(query) - - val selection: MutableList = mutableStateListOf() - val isPerCategory by preferences.categorizedDisplaySettings().asState() - var columns by mutableStateOf(0) - var currentDisplayMode by preferences.libraryDisplayMode().asState() + val tabVisibility by preferences.categoryTabs().asState() + + val mangaCountVisibility by preferences.categoryNumberOfItems().asState() + + var activeCategory: Int by preferences.lastUsedCategory().asState() + + val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() + + val isIncognitoMode: Boolean by preferences.incognitoMode().asState() + /** * Relay used to apply the UI filters to the last emission of the library. */ @@ -123,7 +128,7 @@ class LibraryPresenter( */ private val sortTriggerRelay = BehaviorRelay.create(Unit) - private var librarySubscription: Subscription? = null + private var librarySubscription: Job? = null override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -135,22 +140,31 @@ class LibraryPresenter( * Subscribes to library if needed. */ fun subscribeLibrary() { - // TODO: Move this to a coroutine world - if (librarySubscription.isNullOrUnsubscribed()) { - librarySubscription = getLibraryObservable() - .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> - lib.apply { setBadges(mangaMap) } - } - .combineLatest(getFilterObservable()) { lib, tracks -> - lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks)) - } - .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> - lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap)) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache({ view, (categories, mangaMap) -> - view.onNextLibraryUpdate(categories, mangaMap) - },) + /** + * TODO: Move this to a coroutine world + * - 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 retrive as needed instead of fetching all of them at once + */ + if (librarySubscription == null || librarySubscription!!.isCancelled) { + librarySubscription = presenterScope.launchIO { + getLibraryObservable() + .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> + lib.apply { setBadges(mangaMap) } + } + .combineLatest(getFilterObservable()) { lib, tracks -> + lib.copy(mangaMap = applyFilters(lib.mangaMap, tracks)) + } + .combineLatest(sortTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> + lib.copy(mangaMap = applySort(lib.categories, lib.mangaMap)) + } + .observeOn(AndroidSchedulers.mainThread()) + .asFlow() + .collectLatest { + state.isLoading = false + loadedManga = it.mangaMap + } + } } } @@ -397,7 +411,7 @@ class LibraryPresenter( * @return an observable of the categories and its manga. */ private fun getLibraryObservable(): Observable { - return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga -> + return combine(getCategoriesObservable(), getLibraryMangasObservable()) { dbCategories, libraryManga -> val categories = if (libraryManga.containsKey(0)) { arrayListOf(Category.default(context)) + dbCategories } else { @@ -411,9 +425,9 @@ class LibraryPresenter( } } - this.categories = categories + state.categories = categories Library(categories, libraryManga) - } + }.asObservable() } /** @@ -421,8 +435,8 @@ class LibraryPresenter( * * @return an observable of the categories. */ - private fun getCategoriesObservable(): Observable> { - return getCategories.subscribe().asObservable() + private fun getCategoriesObservable(): Flow> { + return getCategories.subscribe() } /** @@ -431,8 +445,8 @@ class LibraryPresenter( * @return an observable containing a map with the category id as key and a list of manga as the * value. */ - private fun getLibraryMangasObservable(): Observable { - return getLibraryManga.subscribe().asObservable() + private fun getLibraryMangasObservable(): Flow { + return getLibraryManga.subscribe() .map { list -> list.map { libraryManga -> // Display mode based on user preference: take it from global library setting or category @@ -447,7 +461,8 @@ class LibraryPresenter( * @return an observable of tracked manga. */ private fun getFilterObservable(): Observable>> { - return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks } + return filterTriggerRelay.observeOn(Schedulers.io()) + .combineLatest(getTracksObservable()) { _, tracks -> tracks } } /** @@ -458,7 +473,7 @@ class LibraryPresenter( private fun getTracksObservable(): Observable>> { // TODO: Move this to domain/data layer return getTracks.subscribe() - .asObservable().map { tracks -> + .map { tracks -> tracks .groupBy { it.mangaId } .mapValues { tracksForMangaId -> @@ -468,6 +483,7 @@ class LibraryPresenter( } } } + .asObservable() .observeOn(Schedulers.io()) } @@ -497,7 +513,7 @@ class LibraryPresenter( */ fun onOpenManga() { // Avoid further db updates for the library when it's not needed - librarySubscription?.let { remove(it) } + librarySubscription?.cancel() } /** @@ -610,14 +626,50 @@ class LibraryPresenter( } @Composable - fun getMangaForCategory(categoryId: Long): androidx.compose.runtime.State> { + 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) preferences.landscapeColumns() else preferences.portraitColumns()).asState() + } + + // 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 category = categories.getOrNull(activeCategory) + + val defaultTitle = stringResource(id = R.string.label_library) + val default = remember { LibraryToolbarTitle(defaultTitle) } + + return produceState(initialValue = default, category, mangaCountVisibility, tabVisibility) { + val title = if (tabVisibility.not()) category?.name ?: defaultTitle else defaultTitle + + value = when { + category == null -> default + (tabVisibility.not() && mangaCountVisibility.not()) -> LibraryToolbarTitle(title) + tabVisibility.not() && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size) + (tabVisibility && categories.size > 1) && mangaCountVisibility -> LibraryToolbarTitle(title) + tabVisibility && mangaCountVisibility -> LibraryToolbarTitle(title, loadedManga[category.id]?.size) + else -> default + } + } + } + + @Composable + fun getMangaForCategory(page: Int): androidx.compose.runtime.State> { + val categoryId = remember(categories) { + categories.getOrNull(page)?.id ?: -1 + } val unfiltered = loadedManga[categoryId] ?: emptyList() return derivedStateOf { val query = searchQuery - if (query.isNotBlank()) { + if (query.isNullOrBlank().not()) { unfiltered.filter { - it.filter(query) + it.filter(query!!) } } else { unfiltered @@ -626,9 +678,9 @@ class LibraryPresenter( } @Composable - fun getDisplayMode(index: Int): DisplayModeSetting { + fun getDisplayMode(index: Int): androidx.compose.runtime.State { val category = categories[index] - return remember { + return derivedStateOf { if (isPerCategory.not() || category.id == 0L) { currentDisplayMode } else { @@ -642,34 +694,30 @@ class LibraryPresenter( } fun clearSelection() { - selection.clear() + state.selection = emptyList() } fun toggleSelection(manga: LibraryManga) { + val mutableList = state.selection.toMutableList() if (selection.fastAny { it.id == manga.id }) { - selection.remove(manga) + mutableList.remove(manga) } else { - selection.add(manga) + mutableList.add(manga) } - view?.invalidateActionMode() - view?.createActionModeIfNeeded() + state.selection = mutableList } fun selectAll(index: Int) { val category = categories[index] val items = loadedManga[category.id] ?: emptyList() - selection.addAll(items.filterNot { it.manga in selection }.map { it.manga }) - view?.createActionModeIfNeeded() - view?.invalidateActionMode() + state.selection = state.selection.toMutableList().apply { + addAll(items.filterNot { it.manga in selection }.map { it.manga }) + } } fun invertSelection(index: Int) { val category = categories[index] val items = (loadedManga[category.id] ?: emptyList()).map { it.manga } - val invert = items.filterNot { it in selection } - selection.removeAll(items) - selection.addAll(invert) - view?.createActionModeIfNeeded() - view?.invalidateActionMode() + state.selection = items.filterNot { it in selection } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index a25adc8455..57e901a012 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -488,9 +488,13 @@ class MainActivity : BaseActivity() { return } val backstackSize = router.backstackSize - if (backstackSize == 1 && router.getControllerWithTag("$startScreenId") == null) { + val startScreen = router.getControllerWithTag("$startScreenId") + if (backstackSize == 1 && startScreen == null) { // Return to start screen moveToStartScreen() + setSelectedNavItem(startScreenId) + } else if (startScreen != null && router.handleBack()) { + // Clear selection for Library screen } else if (shouldHandleExitConfirmation()) { // Exit confirmation (resets after 2 seconds) lifecycleScope.launchUI { resetExitConfirmation() } diff --git a/app/src/main/res/layout/library_controller.xml b/app/src/main/res/layout/library_controller.xml deleted file mode 100644 index b1038fbb09..0000000000 --- a/app/src/main/res/layout/library_controller.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -