浏览代码

Use Voyager on Library tab (#8620)

Ivan Iskandar 2 年之前
父节点
当前提交
e14909fff4

+ 60 - 41
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),

+ 0 - 130
app/src/main/java/eu/kanade/presentation/library/LibraryScreen.kt

@@ -1,130 +0,0 @@
-package eu.kanade.presentation.library
-
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.HelpOutline
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.util.fastAll
-import eu.kanade.domain.category.model.Category
-import eu.kanade.domain.library.model.LibraryManga
-import eu.kanade.domain.library.model.display
-import eu.kanade.domain.manga.model.isLocal
-import eu.kanade.presentation.components.EmptyScreen
-import eu.kanade.presentation.components.EmptyScreenAction
-import eu.kanade.presentation.components.LibraryBottomActionMenu
-import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.components.Scaffold
-import eu.kanade.presentation.library.components.LibraryContent
-import eu.kanade.presentation.library.components.LibraryToolbar
-import eu.kanade.presentation.manga.DownloadAction
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.library.LibraryPresenter
-import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
-
-@Composable
-fun LibraryScreen(
-    presenter: LibraryPresenter,
-    onMangaClicked: (Long) -> Unit,
-    onContinueReadingClicked: (LibraryManga) -> Unit,
-    onGlobalSearchClicked: () -> Unit,
-    onChangeCategoryClicked: () -> Unit,
-    onMarkAsReadClicked: () -> Unit,
-    onMarkAsUnreadClicked: () -> Unit,
-    onDownloadClicked: (DownloadAction) -> Unit,
-    onDeleteClicked: () -> Unit,
-    onClickUnselectAll: () -> Unit,
-    onClickSelectAll: () -> Unit,
-    onClickInvertSelection: () -> Unit,
-    onClickFilter: () -> Unit,
-    onClickRefresh: (Category?) -> Boolean,
-    onClickOpenRandomManga: () -> Unit,
-) {
-    val haptic = LocalHapticFeedback.current
-
-    Scaffold(
-        topBar = { scrollBehavior ->
-            val title by presenter.getToolbarTitle()
-            val tabVisible = presenter.tabVisibility && presenter.categories.size > 1
-            LibraryToolbar(
-                state = presenter,
-                title = title,
-                incognitoMode = !tabVisible && presenter.isIncognitoMode,
-                downloadedOnlyMode = !tabVisible && presenter.isDownloadOnly,
-                onClickUnselectAll = onClickUnselectAll,
-                onClickSelectAll = onClickSelectAll,
-                onClickInvertSelection = onClickInvertSelection,
-                onClickFilter = onClickFilter,
-                onClickRefresh = { onClickRefresh(null) },
-                onClickOpenRandomManga = onClickOpenRandomManga,
-                scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
-            )
-        },
-        bottomBar = {
-            LibraryBottomActionMenu(
-                visible = presenter.selectionMode,
-                onChangeCategoryClicked = onChangeCategoryClicked,
-                onMarkAsReadClicked = onMarkAsReadClicked,
-                onMarkAsUnreadClicked = onMarkAsUnreadClicked,
-                onDownloadClicked = onDownloadClicked.takeIf { presenter.selection.fastAll { !it.manga.isLocal() } },
-                onDeleteClicked = onDeleteClicked,
-            )
-        },
-    ) { paddingValues ->
-        if (presenter.isLoading) {
-            LoadingScreen()
-            return@Scaffold
-        }
-
-        val contentPadding = TachiyomiBottomNavigationView.withBottomNavPadding(paddingValues)
-        if (presenter.searchQuery.isNullOrEmpty() && presenter.isLibraryEmpty) {
-            val handler = LocalUriHandler.current
-            EmptyScreen(
-                textResource = R.string.information_empty_library,
-                modifier = Modifier.padding(contentPadding),
-                actions = listOf(
-                    EmptyScreenAction(
-                        stringResId = R.string.getting_started_guide,
-                        icon = Icons.Outlined.HelpOutline,
-                        onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
-                    ),
-                ),
-            )
-            return@Scaffold
-        }
-
-        LibraryContent(
-            state = presenter,
-            contentPadding = contentPadding,
-            currentPage = { presenter.activeCategory },
-            isLibraryEmpty = presenter.isLibraryEmpty,
-            showPageTabs = presenter.tabVisibility,
-            showMangaCount = presenter.mangaCountVisibility,
-            onChangeCurrentPage = { presenter.activeCategory = it },
-            onMangaClicked = onMangaClicked,
-            onContinueReadingClicked = onContinueReadingClicked,
-            onToggleSelection = { presenter.toggleSelection(it) },
-            onToggleRangeSelection = {
-                presenter.toggleRangeSelection(it)
-                haptic.performHapticFeedback(HapticFeedbackType.LongPress)
-            },
-            onRefresh = onClickRefresh,
-            onGlobalSearchClicked = onGlobalSearchClicked,
-            getNumberOfMangaForCategory = { presenter.getMangaCountForCategory(it) },
-            getDisplayModeForPage = { presenter.categories[it].display },
-            getColumnsForOrientation = { presenter.getColumnsPreferenceForCurrentOrientation(it) },
-            getLibraryForPage = { presenter.getMangaForCategory(page = it) },
-            showDownloadBadges = presenter.showDownloadBadges,
-            showUnreadBadges = presenter.showUnreadBadges,
-            showLocalBadges = presenter.showLocalBadges,
-            showLanguageBadges = presenter.showLanguageBadges,
-            showContinueReadingButton = presenter.showContinueReadingButton,
-            isIncognitoMode = presenter.isIncognitoMode,
-            isDownloadOnly = presenter.isDownloadOnly,
-        )
-    }
-}

+ 0 - 35
app/src/main/java/eu/kanade/presentation/library/LibraryState.kt

@@ -1,35 +0,0 @@
-package eu.kanade.presentation.library
-
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import eu.kanade.domain.category.model.Category
-import eu.kanade.domain.library.model.LibraryManga
-import eu.kanade.tachiyomi.ui.library.LibraryPresenter
-
-@Stable
-interface LibraryState {
-    val isLoading: Boolean
-    val categories: List<Category>
-    var searchQuery: String?
-    val selection: List<LibraryManga>
-    val selectionMode: Boolean
-    var hasActiveFilters: Boolean
-    var dialog: LibraryPresenter.Dialog?
-}
-
-fun LibraryState(): LibraryState {
-    return LibraryStateImpl()
-}
-
-class LibraryStateImpl : LibraryState {
-    override var isLoading: Boolean by mutableStateOf(true)
-    override var categories: List<Category> by mutableStateOf(emptyList())
-    override var searchQuery: String? by mutableStateOf(null)
-    override var selection: List<LibraryManga> by mutableStateOf(emptyList())
-    override val selectionMode: Boolean by derivedStateOf { selection.isNotEmpty() }
-    override var hasActiveFilters: Boolean by mutableStateOf(false)
-    override var dialog: LibraryPresenter.Dialog? by mutableStateOf(null)
-}

+ 11 - 19
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,
         )

+ 10 - 19
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<LibraryItem>,
-    showDownloadBadges: Boolean,
-    showUnreadBadges: Boolean,
-    showLocalBadges: Boolean,
-    showLanguageBadges: Boolean,
-    showContinueReadingButton: Boolean,
     columns: Int,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
-    onClickContinueReading: (LibraryManga) -> Unit,
+    onClickContinueReading: ((LibraryManga) -> Unit)?,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -51,26 +46,22 @@ fun LibraryComfortableGrid(
                     lastModified = manga.coverLastModified,
                 ),
                 coverBadgeStart = {
-                    DownloadsBadge(
-                        enabled = showDownloadBadges,
-                        item = libraryItem,
-                    )
-                    UnreadBadge(
-                        enabled = showUnreadBadges,
-                        item = libraryItem,
-                    )
+                    DownloadsBadge(count = libraryItem.downloadCount.toInt())
+                    UnreadBadge(count = libraryItem.unreadCount.toInt())
                 },
                 coverBadgeEnd = {
                     LanguageBadge(
-                        showLanguage = showLanguageBadges,
-                        showLocal = showLocalBadges,
-                        item = libraryItem,
+                        isLocal = libraryItem.isLocal,
+                        sourceLanguage = libraryItem.sourceLanguage,
                     )
                 },
-                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
-                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
+                onClickContinueReading = if (onClickContinueReading != null) {
+                    { onClickContinueReading(libraryItem.libraryManga) }
+                } else {
+                    null
+                },
             )
         }
     }

+ 10 - 19
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<LibraryItem>,
     showTitle: Boolean,
-    showDownloadBadges: Boolean,
-    showUnreadBadges: Boolean,
-    showLocalBadges: Boolean,
-    showLanguageBadges: Boolean,
-    showContinueReadingButton: Boolean,
     columns: Int,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
-    onClickContinueReading: (LibraryManga) -> Unit,
+    onClickContinueReading: ((LibraryManga) -> Unit)?,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -52,26 +47,22 @@ fun LibraryCompactGrid(
                     lastModified = manga.coverLastModified,
                 ),
                 coverBadgeStart = {
-                    DownloadsBadge(
-                        enabled = showDownloadBadges,
-                        item = libraryItem,
-                    )
-                    UnreadBadge(
-                        enabled = showUnreadBadges,
-                        item = libraryItem,
-                    )
+                    DownloadsBadge(count = libraryItem.downloadCount.toInt())
+                    UnreadBadge(count = libraryItem.unreadCount.toInt())
                 },
                 coverBadgeEnd = {
                     LanguageBadge(
-                        showLanguage = showLanguageBadges,
-                        showLocal = showLocalBadges,
-                        item = libraryItem,
+                        isLocal = libraryItem.isLocal,
+                        sourceLanguage = libraryItem.sourceLanguage,
                     )
                 },
-                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
-                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
+                onClickContinueReading = if (onClickContinueReading != null) {
+                    { onClickContinueReading(libraryItem.libraryManga) }
+                } else {
+                    null
+                },
             )
         }
     }

+ 17 - 36
app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt

@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.calculateStartPadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -21,7 +20,6 @@ import eu.kanade.domain.library.model.LibraryDisplayMode
 import eu.kanade.domain.library.model.LibraryManga
 import eu.kanade.presentation.components.SwipeRefresh
 import eu.kanade.presentation.components.rememberPagerState
-import eu.kanade.presentation.library.LibraryState
 import eu.kanade.tachiyomi.ui.library.LibraryItem
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -29,28 +27,24 @@ import kotlin.time.Duration.Companion.seconds
 
 @Composable
 fun LibraryContent(
-    state: LibraryState,
+    categories: List<Category>,
+    searchQuery: String?,
+    selection: List<LibraryManga>,
     contentPadding: PaddingValues,
     currentPage: () -> Int,
     isLibraryEmpty: Boolean,
     showPageTabs: Boolean,
-    showMangaCount: Boolean,
     onChangeCurrentPage: (Int) -> Unit,
     onMangaClicked: (Long) -> Unit,
-    onContinueReadingClicked: (LibraryManga) -> Unit,
+    onContinueReadingClicked: ((LibraryManga) -> Unit)?,
     onToggleSelection: (LibraryManga) -> Unit,
     onToggleRangeSelection: (LibraryManga) -> Unit,
     onRefresh: (Category?) -> Boolean,
     onGlobalSearchClicked: () -> Unit,
-    getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
+    getNumberOfMangaForCategory: (Category) -> Int?,
     getDisplayModeForPage: @Composable (Int) -> LibraryDisplayMode,
     getColumnsForOrientation: (Boolean) -> PreferenceMutableState<Int>,
-    getLibraryForPage: @Composable (Int) -> List<LibraryItem>,
-    showDownloadBadges: Boolean,
-    showUnreadBadges: Boolean,
-    showLocalBadges: Boolean,
-    showLanguageBadges: Boolean,
-    showContinueReadingButton: Boolean,
+    getLibraryForPage: (Int) -> List<LibraryItem>,
     isDownloadOnly: Boolean,
     isIncognitoMode: Boolean,
 ) {
@@ -61,38 +55,30 @@ fun LibraryContent(
             end = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
         ),
     ) {
-        val categories = state.categories
         val coercedCurrentPage = remember { currentPage().coerceAtMost(categories.lastIndex) }
         val pagerState = rememberPagerState(coercedCurrentPage)
 
         val scope = rememberCoroutineScope()
         var isRefreshing by remember(pagerState.currentPage) { mutableStateOf(false) }
 
-        if (isLibraryEmpty.not() && showPageTabs && categories.size > 1) {
+        if (!isLibraryEmpty && showPageTabs && categories.size > 1) {
             LibraryTabs(
                 categories = categories,
                 currentPageIndex = pagerState.currentPage,
-                showMangaCount = showMangaCount,
-                getNumberOfMangaForCategory = getNumberOfMangaForCategory,
                 isDownloadOnly = isDownloadOnly,
                 isIncognitoMode = isIncognitoMode,
-                onTabItemClick = { scope.launch { pagerState.animateScrollToPage(it) } },
-            )
+                getNumberOfMangaForCategory = getNumberOfMangaForCategory,
+            ) { scope.launch { pagerState.animateScrollToPage(it) } }
         }
 
+        val notSelectionMode = selection.isEmpty()
         val onClickManga = { manga: LibraryManga ->
-            if (state.selectionMode.not()) {
+            if (notSelectionMode) {
                 onMangaClicked(manga.manga.id)
             } else {
                 onToggleSelection(manga)
             }
         }
-        val onLongClickManga = { manga: LibraryManga ->
-            onToggleRangeSelection(manga)
-        }
-        val onClickContinueReading = { manga: LibraryManga ->
-            onContinueReadingClicked(manga)
-        }
 
         SwipeRefresh(
             refreshing = isRefreshing,
@@ -106,26 +92,21 @@ fun LibraryContent(
                     isRefreshing = false
                 }
             },
-            enabled = state.selectionMode.not(),
+            enabled = notSelectionMode,
         ) {
             LibraryPager(
                 state = pagerState,
                 contentPadding = PaddingValues(bottom = contentPadding.calculateBottomPadding()),
                 pageCount = categories.size,
-                selectedManga = state.selection,
+                selectedManga = selection,
+                searchQuery = searchQuery,
+                onGlobalSearchClicked = onGlobalSearchClicked,
                 getDisplayModeForPage = getDisplayModeForPage,
                 getColumnsForOrientation = getColumnsForOrientation,
                 getLibraryForPage = getLibraryForPage,
-                showDownloadBadges = showDownloadBadges,
-                showUnreadBadges = showUnreadBadges,
-                showLocalBadges = showLocalBadges,
-                showLanguageBadges = showLanguageBadges,
-                showContinueReadingButton = showContinueReadingButton,
                 onClickManga = onClickManga,
-                onLongClickManga = onLongClickManga,
-                onClickContinueReading = onClickContinueReading,
-                onGlobalSearchClicked = onGlobalSearchClicked,
-                searchQuery = state.searchQuery,
+                onLongClickManga = onToggleRangeSelection,
+                onClickContinueReading = onContinueReadingClicked,
             )
         }
 

+ 14 - 13
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<LibraryItem>,
-    showDownloadBadges: Boolean,
-    showUnreadBadges: Boolean,
-    showLocalBadges: Boolean,
-    showLanguageBadges: Boolean,
-    showContinueReadingButton: Boolean,
     contentPadding: PaddingValues,
     selection: List<LibraryManga>,
     onClick: (LibraryManga) -> Unit,
     onLongClick: (LibraryManga) -> Unit,
-    onClickContinueReading: (LibraryManga) -> Unit,
+    onClickContinueReading: ((LibraryManga) -> Unit)?,
     searchQuery: String?,
     onGlobalSearchClicked: () -> Unit,
 ) {
@@ -41,13 +36,13 @@ fun LibraryList(
         contentPadding = contentPadding + PaddingValues(vertical = 8.dp),
     ) {
         item {
-            if (searchQuery.isNullOrEmpty().not()) {
+            if (!searchQuery.isNullOrEmpty()) {
                 TextButton(
                     modifier = Modifier.fillMaxWidth(),
                     onClick = onGlobalSearchClicked,
                 ) {
                     Text(
-                        text = stringResource(R.string.action_global_search_query, searchQuery!!),
+                        text = stringResource(R.string.action_global_search_query, searchQuery),
                         modifier = Modifier.zIndex(99f),
                     )
                 }
@@ -70,14 +65,20 @@ fun LibraryList(
                     lastModified = manga.coverLastModified,
                 ),
                 badge = {
-                    DownloadsBadge(enabled = showDownloadBadges, item = libraryItem)
-                    UnreadBadge(enabled = showUnreadBadges, item = libraryItem)
-                    LanguageBadge(showLanguage = showLanguageBadges, showLocal = showLocalBadges, item = libraryItem)
+                    DownloadsBadge(count = libraryItem.downloadCount.toInt())
+                    UnreadBadge(count = libraryItem.unreadCount.toInt())
+                    LanguageBadge(
+                        isLocal = libraryItem.isLocal,
+                        sourceLanguage = libraryItem.sourceLanguage,
+                    )
                 },
-                showContinueReadingButton = showContinueReadingButton,
                 onLongClick = { onLongClick(libraryItem.libraryManga) },
                 onClick = { onClick(libraryItem.libraryManga) },
-                onClickContinueReading = { onClickContinueReading(libraryItem.libraryManga) },
+                onClickContinueReading = if (onClickContinueReading != null) {
+                    { onClickContinueReading(libraryItem.libraryManga) }
+                } else {
+                    null
+                },
             )
         }
     }

+ 3 - 23
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<Int>,
-    getLibraryForPage: @Composable (Int) -> List<LibraryItem>,
-    showDownloadBadges: Boolean,
-    showUnreadBadges: Boolean,
-    showLocalBadges: Boolean,
-    showLanguageBadges: Boolean,
-    showContinueReadingButton: Boolean,
+    getLibraryForPage: (Int) -> List<LibraryItem>,
     onClickManga: (LibraryManga) -> Unit,
     onLongClickManga: (LibraryManga) -> Unit,
-    onClickContinueReading: (LibraryManga) -> Unit,
+    onClickContinueReading: ((LibraryManga) -> Unit)?,
 ) {
     HorizontalPager(
         count = pageCount,
@@ -62,11 +57,6 @@ fun LibraryPager(
             LibraryDisplayMode.List -> {
                 LibraryList(
                     items = library,
-                    showDownloadBadges = showDownloadBadges,
-                    showUnreadBadges = showUnreadBadges,
-                    showLocalBadges = showLocalBadges,
-                    showLanguageBadges = showLanguageBadges,
-                    showContinueReadingButton = showContinueReadingButton,
                     contentPadding = contentPadding,
                     selection = selectedManga,
                     onClick = onClickManga,
@@ -80,11 +70,6 @@ fun LibraryPager(
                 LibraryCompactGrid(
                     items = library,
                     showTitle = displayMode is LibraryDisplayMode.CompactGrid,
-                    showDownloadBadges = showDownloadBadges,
-                    showUnreadBadges = showUnreadBadges,
-                    showLocalBadges = showLocalBadges,
-                    showLanguageBadges = showLanguageBadges,
-                    showContinueReadingButton = showContinueReadingButton,
                     columns = columns,
                     contentPadding = contentPadding,
                     selection = selectedManga,
@@ -98,17 +83,12 @@ fun LibraryPager(
             LibraryDisplayMode.ComfortableGrid -> {
                 LibraryComfortableGrid(
                     items = library,
-                    showDownloadBadges = showDownloadBadges,
-                    showUnreadBadges = showUnreadBadges,
-                    showLocalBadges = showLocalBadges,
-                    showLanguageBadges = showLanguageBadges,
-                    showContinueReadingButton = showContinueReadingButton,
                     columns = columns,
                     contentPadding = contentPadding,
                     selection = selectedManga,
                     onClick = onClickManga,
-                    onClickContinueReading = onClickContinueReading,
                     onLongClick = onLongClickManga,
+                    onClickContinueReading = onClickContinueReading,
                     searchQuery = searchQuery,
                     onGlobalSearchClicked = onGlobalSearchClicked,
                 )

+ 2 - 9
app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt

@@ -5,8 +5,6 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ScrollableTabRow
 import androidx.compose.material3.Tab
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.unit.dp
 import eu.kanade.domain.category.model.Category
 import eu.kanade.presentation.category.visualName
@@ -19,10 +17,9 @@ import eu.kanade.presentation.components.TabText
 fun LibraryTabs(
     categories: List<Category>,
     currentPageIndex: Int,
-    showMangaCount: Boolean,
     isDownloadOnly: Boolean,
     isIncognitoMode: Boolean,
-    getNumberOfMangaForCategory: @Composable (Long) -> State<Int?>,
+    getNumberOfMangaForCategory: (Category) -> Int?,
     onTabItemClick: (Int) -> Unit,
 ) {
     Column {
@@ -41,11 +38,7 @@ fun LibraryTabs(
                     text = {
                         TabText(
                             text = category.visualName,
-                            badgeCount = if (showMangaCount) {
-                                getNumberOfMangaForCategory(category.id)
-                            } else {
-                                null
-                            }?.value,
+                            badgeCount = getNumberOfMangaForCategory(category),
                         )
                     },
                     unselectedContentColor = MaterialTheme.colorScheme.onSurface,

+ 15 - 11
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,
@@ -39,10 +40,12 @@ fun LibraryToolbar(
     onClickFilter: () -> Unit,
     onClickRefresh: () -> Unit,
     onClickOpenRandomManga: () -> Unit,
+    searchQuery: String?,
+    onSearchQueryChange: (String?) -> Unit,
     scrollBehavior: TopAppBarScrollBehavior?,
 ) = when {
-    state.selectionMode -> LibrarySelectionToolbar(
-        state = state,
+    selectedCount > 0 -> LibrarySelectionToolbar(
+        selectedCount = selectedCount,
         incognitoMode = incognitoMode,
         downloadedOnlyMode = downloadedOnlyMode,
         onClickUnselectAll = onClickUnselectAll,
@@ -51,11 +54,11 @@ fun LibraryToolbar(
     )
     else -> LibraryRegularToolbar(
         title = title,
-        hasFilters = state.hasActiveFilters,
+        hasFilters = hasActiveFilters,
         incognitoMode = incognitoMode,
         downloadedOnlyMode = downloadedOnlyMode,
-        searchQuery = state.searchQuery,
-        onChangeSearchQuery = { state.searchQuery = it },
+        searchQuery = searchQuery,
+        onSearchQueryChange = onSearchQueryChange,
         onClickFilter = onClickFilter,
         onClickRefresh = onClickRefresh,
         onClickOpenRandomManga = onClickOpenRandomManga,
@@ -70,7 +73,7 @@ fun LibraryRegularToolbar(
     incognitoMode: Boolean,
     downloadedOnlyMode: Boolean,
     searchQuery: String?,
-    onChangeSearchQuery: (String?) -> Unit,
+    onSearchQueryChange: (String?) -> Unit,
     onClickFilter: () -> Unit,
     onClickRefresh: () -> Unit,
     onClickOpenRandomManga: () -> Unit,
@@ -96,7 +99,7 @@ fun LibraryRegularToolbar(
             }
         },
         searchQuery = searchQuery,
-        onChangeSearchQuery = onChangeSearchQuery,
+        onChangeSearchQuery = onSearchQueryChange,
         actions = {
             val filterTint = if (hasFilters) MaterialTheme.colorScheme.active else LocalContentColor.current
             IconButton(onClick = onClickFilter) {
@@ -128,7 +131,7 @@ fun LibraryRegularToolbar(
 
 @Composable
 fun LibrarySelectionToolbar(
-    state: LibraryState,
+    selectedCount: Int,
     incognitoMode: Boolean,
     downloadedOnlyMode: Boolean,
     onClickUnselectAll: () -> Unit,
@@ -136,7 +139,7 @@ fun LibrarySelectionToolbar(
     onClickInvertSelection: () -> Unit,
 ) {
     AppBar(
-        titleContent = { Text(text = "${state.selection.size}") },
+        titleContent = { Text(text = "$selectedCount") },
         actions = {
             IconButton(onClick = onClickSelectAll) {
                 Icon(Icons.Outlined.SelectAll, contentDescription = stringResource(R.string.action_select_all))
@@ -152,6 +155,7 @@ fun LibrarySelectionToolbar(
     )
 }
 
+@Immutable
 data class LibraryToolbarTitle(
     val text: String,
     val numberOfManga: Int? = null,

+ 16 - 244
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -1,167 +1,37 @@
 package eu.kanade.tachiyomi.ui.library
 
 import android.os.Bundle
-import android.view.Menu
 import android.view.View
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.platform.LocalContext
-import com.bluelinelabs.conductor.ControllerChangeHandler
-import com.bluelinelabs.conductor.ControllerChangeType
-import eu.kanade.core.prefs.CheckboxState
-import eu.kanade.domain.chapter.model.Chapter
-import eu.kanade.domain.library.model.LibraryManga
-import eu.kanade.domain.manga.model.Manga
-import eu.kanade.domain.manga.model.isLocal
-import eu.kanade.presentation.components.ChangeCategoryDialog
-import eu.kanade.presentation.components.DeleteLibraryMangaDialog
-import eu.kanade.presentation.library.LibraryScreen
-import eu.kanade.presentation.manga.DownloadAction
-import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.library.LibraryUpdateService
-import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
+import cafe.adriel.voyager.navigator.Navigator
+import eu.kanade.domain.category.model.Category
+import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
-import eu.kanade.tachiyomi.ui.base.controller.pushController
-import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
-import eu.kanade.tachiyomi.ui.category.CategoryController
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.manga.MangaController
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.launchUI
-import eu.kanade.tachiyomi.util.lang.withUIContext
-import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
 
 class LibraryController(
     bundle: Bundle? = null,
-) : FullComposeController<LibraryPresenter>(bundle), RootController {
+) : BasicFullComposeController(bundle), RootController {
 
     /**
      * Sheet containing filter/sort/display items.
      */
     private var settingsSheet: LibrarySettingsSheet? = null
 
-    override fun createPresenter(): LibraryPresenter = LibraryPresenter()
-
     @Composable
     override fun ComposeContent() {
-        val context = LocalContext.current
-        val getMangaForCategory = presenter.getMangaForCategory(page = presenter.activeCategory)
-
-        LibraryScreen(
-            presenter = presenter,
-            onMangaClicked = ::openManga,
-            onContinueReadingClicked = ::continueReading,
-            onGlobalSearchClicked = {
-                router.pushController(GlobalSearchController(presenter.searchQuery))
-            },
-            onChangeCategoryClicked = ::showMangaCategoriesDialog,
-            onMarkAsReadClicked = { markReadStatus(true) },
-            onMarkAsUnreadClicked = { markReadStatus(false) },
-            onDownloadClicked = ::runDownloadChapterAction,
-            onDeleteClicked = ::showDeleteMangaDialog,
-            onClickFilter = ::showSettingsSheet,
-            onClickRefresh = {
-                val started = LibraryUpdateService.start(context, it)
-                context.toast(if (started) R.string.updating_category else R.string.update_already_running)
-                started
-            },
-            onClickOpenRandomManga = {
-                val items = getMangaForCategory.map { it.libraryManga.manga.id }
-                if (getMangaForCategory.isNotEmpty()) {
-                    openManga(items.random())
-                } else {
-                    context.toast(R.string.information_no_entries_found)
-                }
-            },
-            onClickInvertSelection = { presenter.invertSelection(presenter.activeCategory) },
-            onClickSelectAll = { presenter.selectAll(presenter.activeCategory) },
-            onClickUnselectAll = ::clearSelection,
-        )
-
-        val onDismissRequest = { presenter.dialog = null }
-        when (val dialog = presenter.dialog) {
-            is LibraryPresenter.Dialog.ChangeCategory -> {
-                ChangeCategoryDialog(
-                    initialSelection = dialog.initialSelection,
-                    onDismissRequest = onDismissRequest,
-                    onEditCategories = {
-                        presenter.clearSelection()
-                        router.pushController(CategoryController())
-                    },
-                    onConfirm = { include, exclude ->
-                        presenter.clearSelection()
-                        presenter.setMangaCategories(dialog.manga, include, exclude)
-                    },
-                )
-            }
-            is LibraryPresenter.Dialog.DeleteManga -> {
-                DeleteLibraryMangaDialog(
-                    containsLocalManga = dialog.manga.any(Manga::isLocal),
-                    onDismissRequest = onDismissRequest,
-                    onConfirm = { deleteManga, deleteChapter ->
-                        presenter.removeMangas(dialog.manga, deleteManga, deleteChapter)
-                        presenter.clearSelection()
-                    },
-                )
-            }
-            is LibraryPresenter.Dialog.DownloadCustomAmount -> {
-                DownloadCustomAmountDialog(
-                    maxAmount = dialog.max,
-                    onDismissRequest = onDismissRequest,
-                    onConfirm = { amount ->
-                        presenter.downloadUnreadChapters(dialog.manga, amount)
-                        presenter.clearSelection()
-                    },
-                )
-            }
-            null -> {}
-        }
-
-        LaunchedEffect(presenter.selectionMode) {
-            // Could perhaps be removed when navigation is in a Compose world
-            if (router.backstackSize == 1) {
-                (activity as? MainActivity)?.showBottomNav(presenter.selectionMode.not())
-            }
-        }
-        LaunchedEffect(presenter.isLoading) {
-            if (!presenter.isLoading) {
-                (activity as? MainActivity)?.ready = true
-            }
-        }
-    }
-
-    override fun handleBack(): Boolean {
-        return when {
-            presenter.selection.isNotEmpty() -> {
-                presenter.clearSelection()
-                true
-            }
-            presenter.searchQuery != null -> {
-                presenter.searchQuery = null
-                true
-            }
-            else -> false
-        }
+        Navigator(screen = LibraryScreen)
     }
 
     override fun onViewCreated(view: View) {
         super.onViewCreated(view)
 
-        settingsSheet = LibrarySettingsSheet(router) { group ->
-            when (group) {
-                is LibrarySettingsSheet.Filter.FilterGroup -> onFilterChanged()
-                else -> {} // Handled via different mechanisms
-            }
-        }
-    }
-
-    override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
-        super.onChangeStarted(handler, type)
-        if (type.isEnter) {
-            presenter.subscribeLibrary()
+        settingsSheet = LibrarySettingsSheet(router)
+        viewScope.launch {
+            LibraryScreen.openSettingsSheetEvent
+                .collectLatest(::showSettingsSheet)
         }
     }
 
@@ -171,111 +41,13 @@ class LibraryController(
         super.onDestroyView(view)
     }
 
-    fun showSettingsSheet() {
-        presenter.categories.getOrNull(presenter.activeCategory)?.let { category ->
+    fun showSettingsSheet(category: Category? = null) {
+        if (category != null) {
             settingsSheet?.show(category)
+        } else {
+            viewScope.launch { LibraryScreen.requestOpenSettingsSheet() }
         }
     }
 
-    private fun onFilterChanged() {
-        viewScope.launchUI {
-            presenter.requestFilterUpdate()
-            activity?.invalidateOptionsMenu()
-        }
-    }
-
-    fun search(query: String) {
-        presenter.searchQuery = query
-    }
-
-    override fun onPrepareOptionsMenu(menu: Menu) {
-        val settingsSheet = settingsSheet ?: return
-        presenter.hasActiveFilters = settingsSheet.filters.hasActiveFilters()
-    }
-
-    private fun openManga(mangaId: Long) {
-        presenter.onOpenManga()
-        router.pushController(MangaController(mangaId))
-    }
-
-    private fun continueReading(libraryManga: LibraryManga) {
-        viewScope.launchIO {
-            val chapter = presenter.getNextUnreadChapter(libraryManga.manga)
-            if (chapter != null) {
-                openChapter(chapter)
-            } else {
-                withUIContext { activity?.toast(R.string.no_next_chapter) }
-            }
-        }
-    }
-
-    private fun openChapter(chapter: Chapter) {
-        activity?.run {
-            startActivity(ReaderActivity.newIntent(this, chapter.mangaId, chapter.id))
-        }
-    }
-
-    /**
-     * Clear all of the manga currently selected, and
-     * invalidate the action mode to revert the top toolbar
-     */
-    private fun clearSelection() {
-        presenter.clearSelection()
-    }
-
-    /**
-     * Move the selected manga to a list of categories.
-     */
-    private fun showMangaCategoriesDialog() {
-        viewScope.launchIO {
-            // Create a copy of selected manga
-            val mangaList = presenter.selection.map { it.manga }
-
-            // Hide the default category because it has a different behavior than the ones from db.
-            val categories = presenter.categories.filter { it.id != 0L }
-
-            // Get indexes of the common categories to preselect.
-            val common = presenter.getCommonCategories(mangaList)
-            // Get indexes of the mix categories to preselect.
-            val mix = presenter.getMixCategories(mangaList)
-            val preselected = categories.map {
-                when (it) {
-                    in common -> CheckboxState.State.Checked(it)
-                    in mix -> CheckboxState.TriState.Exclude(it)
-                    else -> CheckboxState.State.None(it)
-                }
-            }
-            presenter.dialog = LibraryPresenter.Dialog.ChangeCategory(mangaList, preselected)
-        }
-    }
-
-    private fun runDownloadChapterAction(action: DownloadAction) {
-        val mangas = presenter.selection.map { it.manga }.toList()
-        when (action) {
-            DownloadAction.NEXT_1_CHAPTER -> presenter.downloadUnreadChapters(mangas, 1)
-            DownloadAction.NEXT_5_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 5)
-            DownloadAction.NEXT_10_CHAPTERS -> presenter.downloadUnreadChapters(mangas, 10)
-            DownloadAction.UNREAD_CHAPTERS -> presenter.downloadUnreadChapters(mangas, null)
-            DownloadAction.CUSTOM -> {
-                presenter.dialog = LibraryPresenter.Dialog.DownloadCustomAmount(
-                    mangas,
-                    presenter.selection.maxOf { it.unreadCount }.toInt(),
-                )
-                return
-            }
-            else -> {}
-        }
-        presenter.clearSelection()
-    }
-
-    private fun markReadStatus(read: Boolean) {
-        val mangaList = presenter.selection.toList()
-        presenter.markReadStatus(mangaList.map { it.manga }, read)
-        presenter.clearSelection()
-    }
-
-    private fun showDeleteMangaDialog() {
-        val mangaList = presenter.selection.map { it.manga }
-        presenter.dialog = LibraryPresenter.Dialog.DeleteManga(mangaList)
-    }
+    fun search(query: String) = LibraryScreen.search(query)
 }

+ 270 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreen.kt

@@ -0,0 +1,270 @@
+package eu.kanade.tachiyomi.ui.library
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.HelpOutline
+import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.util.fastAll
+import cafe.adriel.voyager.core.model.rememberScreenModel
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.bluelinelabs.conductor.Router
+import eu.kanade.domain.category.model.Category
+import eu.kanade.domain.library.model.LibraryManga
+import eu.kanade.domain.library.model.display
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.isLocal
+import eu.kanade.presentation.components.ChangeCategoryDialog
+import eu.kanade.presentation.components.DeleteLibraryMangaDialog
+import eu.kanade.presentation.components.EmptyScreen
+import eu.kanade.presentation.components.EmptyScreenAction
+import eu.kanade.presentation.components.LibraryBottomActionMenu
+import eu.kanade.presentation.components.LoadingScreen
+import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.library.components.LibraryContent
+import eu.kanade.presentation.library.components.LibraryToolbar
+import eu.kanade.presentation.manga.components.DownloadCustomAmountDialog
+import eu.kanade.presentation.util.LocalRouter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.library.LibraryUpdateService
+import eu.kanade.tachiyomi.ui.base.controller.pushController
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
+import eu.kanade.tachiyomi.ui.category.CategoryController
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.util.lang.launchIO
+import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+object LibraryScreen : Screen {
+
+    @Composable
+    override fun Content() {
+        val router = LocalRouter.currentOrThrow
+        val context = LocalContext.current
+        val scope = rememberCoroutineScope()
+        val haptic = LocalHapticFeedback.current
+
+        val screenModel = rememberScreenModel { LibraryScreenModel() }
+        val state by screenModel.state.collectAsState()
+
+        val snackbarHostState = remember { SnackbarHostState() }
+
+        val onClickRefresh: (Category?) -> Boolean = {
+            val started = LibraryUpdateService.start(context, it)
+            scope.launch {
+                val msgRes = if (started) R.string.updating_category else R.string.update_already_running
+                snackbarHostState.showSnackbar(context.getString(msgRes))
+            }
+            started
+        }
+        val onClickFilter: () -> Unit = {
+            scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategory]) }
+        }
+
+        Scaffold(
+            topBar = { scrollBehavior ->
+                val title = state.getToolbarTitle(
+                    defaultTitle = stringResource(R.string.label_library),
+                    defaultCategoryTitle = stringResource(R.string.label_default),
+                    page = screenModel.activeCategory,
+                )
+                val tabVisible = state.showCategoryTabs && state.categories.size > 1
+                LibraryToolbar(
+                    hasActiveFilters = state.hasActiveFilters,
+                    selectedCount = state.selection.size,
+                    title = title,
+                    incognitoMode = !tabVisible && screenModel.isIncognitoMode,
+                    downloadedOnlyMode = !tabVisible && screenModel.isDownloadOnly,
+                    onClickUnselectAll = screenModel::clearSelection,
+                    onClickSelectAll = { screenModel.selectAll(screenModel.activeCategory) },
+                    onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategory) },
+                    onClickFilter = onClickFilter,
+                    onClickRefresh = { onClickRefresh(null) },
+                    onClickOpenRandomManga = {
+                        scope.launch {
+                            val randomItem = screenModel.getRandomLibraryItemForCurrentCategory()
+                            if (randomItem != null) {
+                                router.openManga(randomItem.libraryManga.manga.id)
+                            } else {
+                                snackbarHostState.showSnackbar(context.getString(R.string.information_no_entries_found))
+                            }
+                        }
+                    },
+                    searchQuery = state.searchQuery,
+                    onSearchQueryChange = screenModel::search,
+                    scrollBehavior = scrollBehavior.takeIf { !tabVisible }, // For scroll overlay when no tab
+                )
+            },
+            bottomBar = {
+                LibraryBottomActionMenu(
+                    visible = state.selectionMode,
+                    onChangeCategoryClicked = screenModel::openChangeCategoryDialog,
+                    onMarkAsReadClicked = { screenModel.markReadSelection(true) },
+                    onMarkAsUnreadClicked = { screenModel.markReadSelection(false) },
+                    onDownloadClicked = screenModel::runDownloadActionSelection
+                        .takeIf { state.selection.fastAll { !it.manga.isLocal() } },
+                    onDeleteClicked = screenModel::openDeleteMangaDialog,
+                )
+            },
+            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+            contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
+        ) { contentPadding ->
+            if (state.isLoading) {
+                LoadingScreen(modifier = Modifier.padding(contentPadding))
+                return@Scaffold
+            }
+
+            if (state.searchQuery.isNullOrEmpty() && state.library.isEmpty()) {
+                val handler = LocalUriHandler.current
+                EmptyScreen(
+                    textResource = R.string.information_empty_library,
+                    modifier = Modifier.padding(contentPadding),
+                    actions = listOf(
+                        EmptyScreenAction(
+                            stringResId = R.string.getting_started_guide,
+                            icon = Icons.Outlined.HelpOutline,
+                            onClick = { handler.openUri("https://tachiyomi.org/help/guides/getting-started") },
+                        ),
+                    ),
+                )
+                return@Scaffold
+            }
+
+            LibraryContent(
+                categories = state.categories,
+                searchQuery = state.searchQuery,
+                selection = state.selection,
+                contentPadding = contentPadding,
+                currentPage = { screenModel.activeCategory },
+                isLibraryEmpty = state.library.isEmpty(),
+                showPageTabs = state.showCategoryTabs,
+                onChangeCurrentPage = { screenModel.activeCategory = it },
+                onMangaClicked = { router.openManga(it) },
+                onContinueReadingClicked = { it: LibraryManga ->
+                    scope.launchIO {
+                        val chapter = screenModel.getNextUnreadChapter(it.manga)
+                        if (chapter != null) {
+                            context.startActivity(ReaderActivity.newIntent(context, chapter.mangaId, chapter.id))
+                        } else {
+                            snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
+                        }
+                    }
+                    Unit
+                }.takeIf { state.showMangaContinueButton },
+                onToggleSelection = { screenModel.toggleSelection(it) },
+                onToggleRangeSelection = {
+                    screenModel.toggleRangeSelection(it)
+                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+                },
+                onRefresh = onClickRefresh,
+                onGlobalSearchClicked = {
+                    router.pushController(GlobalSearchController(screenModel.state.value.searchQuery))
+                },
+                getNumberOfMangaForCategory = { state.getMangaCountForCategory(it) },
+                getDisplayModeForPage = { state.categories[it].display },
+                getColumnsForOrientation = { screenModel.getColumnsPreferenceForCurrentOrientation(it) },
+                getLibraryForPage = { state.getLibraryItemsByPage(it) },
+                isDownloadOnly = screenModel.isDownloadOnly,
+                isIncognitoMode = screenModel.isIncognitoMode,
+            )
+        }
+
+        val onDismissRequest = screenModel::closeDialog
+        when (val dialog = state.dialog) {
+            is LibraryScreenModel.Dialog.ChangeCategory -> {
+                ChangeCategoryDialog(
+                    initialSelection = dialog.initialSelection,
+                    onDismissRequest = onDismissRequest,
+                    onEditCategories = {
+                        screenModel.clearSelection()
+                        router.pushController(CategoryController())
+                    },
+                    onConfirm = { include, exclude ->
+                        screenModel.clearSelection()
+                        screenModel.setMangaCategories(dialog.manga, include, exclude)
+                    },
+                )
+            }
+            is LibraryScreenModel.Dialog.DeleteManga -> {
+                DeleteLibraryMangaDialog(
+                    containsLocalManga = dialog.manga.any(Manga::isLocal),
+                    onDismissRequest = onDismissRequest,
+                    onConfirm = { deleteManga, deleteChapter ->
+                        screenModel.removeMangas(dialog.manga, deleteManga, deleteChapter)
+                        screenModel.clearSelection()
+                    },
+                )
+            }
+            is LibraryScreenModel.Dialog.DownloadCustomAmount -> {
+                DownloadCustomAmountDialog(
+                    maxAmount = dialog.max,
+                    onDismissRequest = onDismissRequest,
+                    onConfirm = { amount ->
+                        screenModel.downloadUnreadChapters(dialog.manga, amount)
+                        screenModel.clearSelection()
+                    },
+                )
+            }
+            null -> {}
+        }
+
+        BackHandler(enabled = state.selectionMode || state.searchQuery != null) {
+            when {
+                state.selectionMode -> screenModel.clearSelection()
+                state.searchQuery != null -> screenModel.search(null)
+            }
+        }
+
+        LaunchedEffect(state.selectionMode) {
+            // Could perhaps be removed when navigation is in a Compose world
+            if (router.backstackSize == 1) {
+                (context as? MainActivity)?.showBottomNav(!state.selectionMode)
+            }
+        }
+        LaunchedEffect(state.isLoading) {
+            if (!state.isLoading) {
+                (context as? MainActivity)?.ready = true
+            }
+        }
+
+        LaunchedEffect(Unit) {
+            launch { queryEvent.collectLatest(screenModel::search) }
+            launch { requestSettingsSheetEvent.collectLatest { onClickFilter() } }
+        }
+    }
+
+    private fun Router.openManga(mangaId: Long) {
+        pushController(MangaController(mangaId))
+    }
+
+    // For invoking search from other screen
+    private val queryEvent = MutableSharedFlow<String>(replay = 1)
+    fun search(query: String) = queryEvent.tryEmit(query)
+
+    // For opening settings sheet in LibraryController
+    private val requestSettingsSheetEvent = MutableSharedFlow<Unit>()
+    private val openSettingsSheetEvent_ = MutableSharedFlow<Category>()
+    val openSettingsSheetEvent = openSettingsSheetEvent_.asSharedFlow()
+    private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.emit(category)
+    suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.emit(Unit)
+}

+ 378 - 244
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt → app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt

@@ -1,18 +1,15 @@
 package eu.kanade.tachiyomi.ui.library
 
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.produceState
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastMap
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.coroutineScope
 import eu.kanade.core.prefs.CheckboxState
 import eu.kanade.core.prefs.PreferenceMutableState
+import eu.kanade.core.prefs.asState
 import eu.kanade.core.util.fastFilter
 import eu.kanade.core.util.fastFilterNot
 import eu.kanade.core.util.fastMapNotNull
@@ -35,11 +32,8 @@ import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.MangaUpdate
 import eu.kanade.domain.manga.model.isLocal
 import eu.kanade.domain.track.interactor.GetTracksPerManga
-import eu.kanade.presentation.category.visualName
-import eu.kanade.presentation.library.LibraryState
-import eu.kanade.presentation.library.LibraryStateImpl
 import eu.kanade.presentation.library.components.LibraryToolbarTitle
-import eu.kanade.tachiyomi.R
+import eu.kanade.presentation.manga.DownloadAction
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.download.DownloadCache
 import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -47,38 +41,33 @@ import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.chapter.getNextUnread
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchNonCancellable
 import eu.kanade.tachiyomi.util.lang.withIOContext
 import eu.kanade.tachiyomi.util.removeCovers
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
+import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.text.Collator
 import java.util.Collections
 import java.util.Locale
 
-/**
- * Class containing library information.
- */
-private data class Library(val categories: List<Category>, val mangaMap: LibraryMap)
-
 /**
  * Typealias for the library manga, using the category as keys, and list of manga as values.
  */
-typealias LibraryMap = Map<Long, List<LibraryItem>>
+typealias LibraryMap = Map<Category, List<LibraryItem>>
 
-class LibraryPresenter(
-    private val state: LibraryStateImpl = LibraryState() as LibraryStateImpl,
+class LibraryScreenModel(
     private val getLibraryManga: GetLibraryManga = Injekt.get(),
     private val getCategories: GetCategories = Injekt.get(),
     private val getTracksPerManga: GetTracksPerManga = Injekt.get(),
@@ -94,90 +83,114 @@ class LibraryPresenter(
     private val downloadManager: DownloadManager = Injekt.get(),
     private val downloadCache: DownloadCache = Injekt.get(),
     private val trackManager: TrackManager = Injekt.get(),
-) : BasePresenter<LibraryController>(), LibraryState by state {
-
-    private var loadedManga by mutableStateOf(emptyMap<Long, List<LibraryItem>>())
-
-    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<Unit> = Channel(Int.MAX_VALUE)
-    private val filterChanges = _filterChanges.receiveAsFlow().onStart { emit(Unit) }
-
-    private var librarySubscription: Job? = null
-
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        subscribeLibrary()
-    }
+) : StateScreenModel<LibraryScreenModel.State>(State()) {
+
+    // This is active category INDEX NUMBER
+    var activeCategory: Int by libraryPreferences.lastUsedCategory().asState(coroutineScope)
+
+    val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
+    val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
+
+    init {
+        coroutineScope.launchIO {
+            combine(
+                state.map { it.searchQuery }.distinctUntilChanged(),
+                getLibraryFlow(),
+                getTracksPerManga.subscribe(),
+                getTrackingFilterFlow(),
+            ) { searchQuery, library, tracks, loggedInTrackServices ->
+                library
+                    .applyFilters(tracks, loggedInTrackServices)
+                    .applySort()
+                    .mapValues { (_, value) ->
+                        if (searchQuery != null) {
+                            // Filter query
+                            value.filter { it.matches(searchQuery) }
+                        } else {
+                            // Don't do anything
+                            value
+                        }
+                    }
+            }
+                .collectLatest {
+                    mutableState.update { state ->
+                        state.copy(
+                            isLoading = false,
+                            library = it,
+                        )
+                    }
+                }
+        }
 
-    fun subscribeLibrary() {
-        /**
-         * TODO:
-         * - Move filter and sort to getMangaForCategory and only filter and sort the current display category instead of whole library as some has 5000+ items in the library
-         * - Create new db view and new query to just fetch the current category save as needed to instance variable
-         * - Fetch badges to maps and retrieve as needed instead of fetching all of them at once
-         */
-        if (librarySubscription == null || librarySubscription!!.isCancelled) {
-            librarySubscription = presenterScope.launchIO {
-                combine(getLibraryFlow(), getTracksPerManga.subscribe(), filterChanges) { library, tracks, _ ->
-                    library.mangaMap
-                        .applyFilters(tracks)
-                        .applySort(library.categories)
+        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,
+                    )
                 }
-                    .collectLatest {
-                        state.isLoading = false
-                        loadedManga = it
-                    }
             }
+            .launchIn(coroutineScope)
+
+        combine(
+            getLibraryItemPreferencesFlow(),
+            getTrackingFilterFlow(),
+        ) { prefs, trackFilter ->
+            val a = (
+                prefs.filterDownloaded or
+                    prefs.filterUnread or
+                    prefs.filterStarted or
+                    prefs.filterBookmarked or
+                    prefs.filterCompleted
+                ) != TriStateGroup.State.IGNORE.value
+            val b = trackFilter.values.any { it != TriStateGroup.State.IGNORE.value }
+            a || b
         }
+            .distinctUntilChanged()
+            .onEach {
+                mutableState.update { state ->
+                    state.copy(hasActiveFilters = it)
+                }
+            }
+            .launchIn(coroutineScope)
     }
 
     /**
      * Applies library filters to the given map of manga.
      */
-    private fun LibraryMap.applyFilters(trackMap: Map<Long, List<Long>>): LibraryMap {
-        val downloadedOnly = preferences.downloadedOnly().get()
-        val filterDownloaded = libraryPreferences.filterDownloaded().get()
-        val filterUnread = libraryPreferences.filterUnread().get()
-        val filterStarted = libraryPreferences.filterStarted().get()
-        val filterBookmarked = libraryPreferences.filterBookmarked().get()
-        val filterCompleted = libraryPreferences.filterCompleted().get()
-
-        val loggedInTrackServices = trackManager.services.fastFilter { trackService -> trackService.isLogged }
-            .associate { trackService ->
-                trackService.id to libraryPreferences.filterTracking(trackService.id.toInt()).get()
-            }
+    private suspend fun LibraryMap.applyFilters(
+        trackMap: Map<Long, List<Long>>,
+        loggedInTrackServices: Map<Long, Int>,
+    ): LibraryMap {
+        val prefs = getLibraryItemPreferencesFlow().first()
+        val downloadedOnly = prefs.globalFilterDownloaded
+        val filterDownloaded = prefs.filterDownloaded
+        val filterUnread = prefs.filterUnread
+        val filterStarted = prefs.filterStarted
+        val filterBookmarked = prefs.filterBookmarked
+        val filterCompleted = prefs.filterCompleted
+
         val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
 
-        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.EXCLUDE.value) it.key else null }
-        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == State.INCLUDE.value) it.key else null }
+        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.EXCLUDE.value) it.key else null }
+        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.INCLUDE.value) it.key else null }
         val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
 
         val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{ item ->
-            if (!downloadedOnly && filterDownloaded == State.IGNORE.value) return@downloaded true
+            if (!downloadedOnly && filterDownloaded == TriStateGroup.State.IGNORE.value) return@downloaded true
             val isDownloaded = when {
                 item.libraryManga.manga.isLocal() -> true
                 item.downloadCount != -1L -> item.downloadCount > 0
                 else -> downloadManager.getDownloadCount(item.libraryManga.manga) > 0
             }
 
-            return@downloaded if (downloadedOnly || filterDownloaded == State.INCLUDE.value) {
+            return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.INCLUDE.value) {
                 isDownloaded
             } else {
                 !isDownloaded
@@ -185,10 +198,10 @@ class LibraryPresenter(
         }
 
         val filterFnUnread: (LibraryItem) -> Boolean = unread@{ item ->
-            if (filterUnread == State.IGNORE.value) return@unread true
+            if (filterUnread == TriStateGroup.State.IGNORE.value) return@unread true
             val isUnread = item.libraryManga.unreadCount > 0
 
-            return@unread if (filterUnread == State.INCLUDE.value) {
+            return@unread if (filterUnread == TriStateGroup.State.INCLUDE.value) {
                 isUnread
             } else {
                 !isUnread
@@ -196,10 +209,10 @@ class LibraryPresenter(
         }
 
         val filterFnStarted: (LibraryItem) -> Boolean = started@{ item ->
-            if (filterStarted == State.IGNORE.value) return@started true
+            if (filterStarted == TriStateGroup.State.IGNORE.value) return@started true
             val hasStarted = item.libraryManga.hasStarted
 
-            return@started if (filterStarted == State.INCLUDE.value) {
+            return@started if (filterStarted == TriStateGroup.State.INCLUDE.value) {
                 hasStarted
             } else {
                 !hasStarted
@@ -207,11 +220,11 @@ class LibraryPresenter(
         }
 
         val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{ item ->
-            if (filterBookmarked == State.IGNORE.value) return@bookmarked true
+            if (filterBookmarked == TriStateGroup.State.IGNORE.value) return@bookmarked true
 
             val hasBookmarks = item.libraryManga.hasBookmarks
 
-            return@bookmarked if (filterBookmarked == State.INCLUDE.value) {
+            return@bookmarked if (filterBookmarked == TriStateGroup.State.INCLUDE.value) {
                 hasBookmarks
             } else {
                 !hasBookmarks
@@ -219,10 +232,10 @@ class LibraryPresenter(
         }
 
         val filterFnCompleted: (LibraryItem) -> Boolean = completed@{ item ->
-            if (filterCompleted == State.IGNORE.value) return@completed true
+            if (filterCompleted == TriStateGroup.State.IGNORE.value) return@completed true
             val isCompleted = item.libraryManga.manga.status.toInt() == SManga.COMPLETED
 
-            return@completed if (filterCompleted == State.INCLUDE.value) {
+            return@completed if (filterCompleted == TriStateGroup.State.INCLUDE.value) {
                 isCompleted
             } else {
                 !isCompleted
@@ -266,9 +279,7 @@ class LibraryPresenter(
     /**
      * Applies library sorting to the given map of manga.
      */
-    private fun LibraryMap.applySort(categories: List<Category>): LibraryMap {
-        val sortModes = categories.associate { it.id to it.sort }
-
+    private fun LibraryMap.applySort(): LibraryMap {
         val locale = Locale.getDefault()
         val collator = Collator.getInstance(locale).apply {
             strength = Collator.PRIMARY
@@ -278,7 +289,7 @@ class LibraryPresenter(
         }
 
         val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
-            val sort = sortModes[i1.libraryManga.category]!!
+            val sort = keys.find { it.id == i1.libraryManga.category }!!.sort
             when (sort.type) {
                 LibrarySort.Type.Alphabetical -> {
                     sortAlphabetically(i1, i2)
@@ -308,12 +319,11 @@ class LibraryPresenter(
                 LibrarySort.Type.DateAdded -> {
                     i1.libraryManga.manga.dateAdded.compareTo(i2.libraryManga.manga.dateAdded)
                 }
-                else -> throw IllegalStateException("Invalid SortModeSetting: ${sort.type}")
             }
         }
 
         return this.mapValues { entry ->
-            val comparator = if (sortModes[entry.key]!!.isAscending) {
+            val comparator = if (keys.find { it.id == entry.key.id }!!.sort.isAscending) {
                 Comparator(sortFn)
             } else {
                 Collections.reverseOrder(sortFn)
@@ -323,24 +333,52 @@ class LibraryPresenter(
         }
     }
 
+    private fun getLibraryItemPreferencesFlow(): Flow<ItemPreferences> {
+        return combine(
+            libraryPreferences.downloadBadge().changes(),
+            libraryPreferences.unreadBadge().changes(),
+            libraryPreferences.localBadge().changes(),
+            libraryPreferences.languageBadge().changes(),
+
+            preferences.downloadedOnly().changes(),
+            libraryPreferences.filterDownloaded().changes(),
+            libraryPreferences.filterUnread().changes(),
+            libraryPreferences.filterStarted().changes(),
+            libraryPreferences.filterBookmarked().changes(),
+            libraryPreferences.filterCompleted().changes(),
+            transform = {
+                ItemPreferences(
+                    downloadBadge = it[0] as Boolean,
+                    unreadBadge = it[1] as Boolean,
+                    localBadge = it[2] as Boolean,
+                    languageBadge = it[3] as Boolean,
+                    globalFilterDownloaded = it[4] as Boolean,
+                    filterDownloaded = it[5] as Int,
+                    filterUnread = it[6] as Int,
+                    filterStarted = it[7] as Int,
+                    filterBookmarked = it[8] as Int,
+                    filterCompleted = it[9] as Int,
+                )
+            },
+        )
+    }
+
     /**
      * Get the categories and all its manga from the database.
      *
      * @return an observable of the categories and its manga.
      */
-    private fun getLibraryFlow(): Flow<Library> {
+    private fun getLibraryFlow(): Flow<LibraryMap> {
         val libraryMangasFlow = combine(
             getLibraryManga.subscribe(),
-            libraryPreferences.downloadBadge().changes(),
-            libraryPreferences.filterDownloaded().changes(),
-            preferences.downloadedOnly().changes(),
+            getLibraryItemPreferencesFlow(),
             downloadCache.changes,
-        ) { libraryMangaList, downloadBadgePref, filterDownloadedPref, downloadedOnly, _ ->
+        ) { libraryMangaList, prefs, _ ->
             libraryMangaList
                 .map { libraryManga ->
-                    val needsDownloadCounts = downloadBadgePref ||
-                        filterDownloadedPref != State.IGNORE.value ||
-                        downloadedOnly
+                    val needsDownloadCounts = prefs.downloadBadge ||
+                        prefs.filterDownloaded != TriStateGroup.State.IGNORE.value ||
+                        prefs.globalFilterDownloaded
 
                     // Display mode based on user preference: take it from global library setting or category
                     LibraryItem(libraryManga).apply {
@@ -349,39 +387,44 @@ class LibraryPresenter(
                         } else {
                             0
                         }
-                        unreadCount = libraryManga.unreadCount
-                        isLocal = libraryManga.manga.isLocal()
-                        sourceLanguage = sourceManager.getOrStub(libraryManga.manga.source).lang
+                        unreadCount = if (prefs.unreadBadge) libraryManga.unreadCount else 0
+                        isLocal = if (prefs.localBadge) libraryManga.manga.isLocal() else false
+                        sourceLanguage = if (prefs.languageBadge) {
+                            sourceManager.getOrStub(libraryManga.manga.source).lang
+                        } else {
+                            ""
+                        }
                     }
                 }
                 .groupBy { it.libraryManga.category }
         }
 
         return combine(getCategories.subscribe(), libraryMangasFlow) { categories, libraryManga ->
-            val displayCategories = if (libraryManga.isNotEmpty() && libraryManga.containsKey(0).not()) {
+            val displayCategories = if (libraryManga.isNotEmpty() && !libraryManga.containsKey(0)) {
                 categories.fastFilterNot { it.isSystemCategory }
             } else {
                 categories
             }
 
-            state.categories = displayCategories
-            Library(categories, libraryManga)
+            displayCategories.associateWith { libraryManga[it.id] ?: emptyList() }
         }
     }
 
     /**
-     * Requests the library to be filtered.
-     */
-    suspend fun requestFilterUpdate() = withIOContext {
-        _filterChanges.send(Unit)
-    }
-
-    /**
-     * Called when a manga is opened.
+     * Flow of tracking filter preferences
+     *
+     * @return map of track id with the filter value
      */
-    fun onOpenManga() {
-        // Avoid further db updates for the library when it's not needed
-        librarySubscription?.cancel()
+    private fun getTrackingFilterFlow(): Flow<Map<Long, Int>> {
+        val loggedServices = trackManager.services.filter { it.isLogged }
+        val a = loggedServices
+            .map { libraryPreferences.filterTracking(it.id.toInt()).changes() }
+            .toTypedArray()
+        return combine(*a) {
+            loggedServices
+                .mapIndexed { index, trackService -> trackService.id to it[index] }
+                .toMap()
+        }
     }
 
     /**
@@ -389,7 +432,7 @@ class LibraryPresenter(
      *
      * @param mangas the list of manga.
      */
-    suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
+    private suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
         if (mangas.isEmpty()) return emptyList()
         return mangas
             .map { getCategories.await(it.id).toSet() }
@@ -405,13 +448,37 @@ class LibraryPresenter(
      *
      * @param mangas the list of manga.
      */
-    suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
+    private suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
         if (mangas.isEmpty()) return emptyList()
         val mangaCategories = mangas.map { getCategories.await(it.id).toSet() }
         val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2) }
         return mangaCategories.flatten().distinct().subtract(common)
     }
 
+    fun runDownloadActionSelection(action: DownloadAction) {
+        val selection = state.value.selection
+        val mangas = selection.map { it.manga }.toList()
+        when (action) {
+            DownloadAction.NEXT_1_CHAPTER -> downloadUnreadChapters(mangas, 1)
+            DownloadAction.NEXT_5_CHAPTERS -> downloadUnreadChapters(mangas, 5)
+            DownloadAction.NEXT_10_CHAPTERS -> downloadUnreadChapters(mangas, 10)
+            DownloadAction.UNREAD_CHAPTERS -> downloadUnreadChapters(mangas, null)
+            DownloadAction.CUSTOM -> {
+                mutableState.update { state ->
+                    state.copy(
+                        dialog = Dialog.DownloadCustomAmount(
+                            mangas,
+                            selection.maxOf { it.unreadCount }.toInt(),
+                        ),
+                    )
+                }
+                return
+            }
+            else -> {}
+        }
+        clearSelection()
+    }
+
     /**
      * Queues the amount specified of unread chapters from the list of mangas given.
      *
@@ -419,7 +486,7 @@ class LibraryPresenter(
      * @param amount the amount to queue or null to queue all
      */
     fun downloadUnreadChapters(mangas: List<Manga>, amount: Int?) {
-        presenterScope.launchNonCancellable {
+        coroutineScope.launchNonCancellable {
             mangas.forEach { manga ->
                 val chapters = getNextChapters.await(manga.id)
                     .fastFilterNot { chapter ->
@@ -440,18 +507,18 @@ class LibraryPresenter(
 
     /**
      * Marks mangas' chapters read status.
-     *
-     * @param mangas the list of manga.
      */
-    fun markReadStatus(mangas: List<Manga>, read: Boolean) {
-        presenterScope.launchNonCancellable {
+    fun markReadSelection(read: Boolean) {
+        val mangas = state.value.selection.toList()
+        coroutineScope.launchNonCancellable {
             mangas.forEach { manga ->
                 setReadStatus.await(
-                    manga = manga,
+                    manga = manga.manga,
                     read = read,
                 )
             }
         }
+        clearSelection()
     }
 
     /**
@@ -462,7 +529,7 @@ class LibraryPresenter(
      * @param deleteChapters whether to delete downloaded chapters.
      */
     fun removeMangas(mangaList: List<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
-        presenterScope.launchNonCancellable {
+        coroutineScope.launchNonCancellable {
             val mangaToDelete = mangaList.distinctBy { it.id }
 
             if (deleteFromLibrary) {
@@ -495,7 +562,7 @@ class LibraryPresenter(
      * @param removeCategories the categories to remove in all mangas.
      */
     fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Long>, removeCategories: List<Long>) {
-        presenterScope.launchNonCancellable {
+        coroutineScope.launchNonCancellable {
             mangaList.forEach { manga ->
                 val categoryIds = getCategories.await(manga.id)
                     .map { it.id }
@@ -508,148 +575,215 @@ class LibraryPresenter(
         }
     }
 
-    @Composable
-    fun getMangaCountForCategory(categoryId: Long): androidx.compose.runtime.State<Int?> {
-        return produceState<Int?>(initialValue = null, loadedManga) {
-            value = loadedManga[categoryId]?.size
-        }
-    }
-
     fun getColumnsPreferenceForCurrentOrientation(isLandscape: Boolean): PreferenceMutableState<Int> {
-        return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState()
+        return (if (isLandscape) libraryPreferences.landscapeColumns() else libraryPreferences.portraitColumns()).asState(coroutineScope)
     }
 
-    // TODO: This is good but should we separate title from count or get categories with count from db
-    @Composable
-    fun getToolbarTitle(): androidx.compose.runtime.State<LibraryToolbarTitle> {
-        val category = categories.getOrNull(activeCategory)
-
-        val defaultTitle = stringResource(R.string.label_library)
-        val categoryName = category?.visualName ?: defaultTitle
-
-        val default = remember { LibraryToolbarTitle(defaultTitle) }
-
-        return produceState(initialValue = default, category, loadedManga, mangaCountVisibility, tabVisibility) {
-            val title = if (tabVisibility.not()) categoryName else defaultTitle
-            val count = when {
-                category == null || mangaCountVisibility.not() -> null
-                tabVisibility.not() -> loadedManga[category.id]?.size
-                else -> loadedManga.values.flatten().distinctBy { it.libraryManga.manga.id }.size
-            }
-
-            value = when (category) {
-                null -> default
-                else -> LibraryToolbarTitle(title, count)
-            }
-        }
-    }
-
-    @Composable
-    fun getMangaForCategory(page: Int): List<LibraryItem> {
-        val categoryId = remember(categories, page) {
-            categories.getOrNull(page)?.id ?: -1
-        }
-        val unfiltered = remember(loadedManga, categoryId) {
-            loadedManga[categoryId] ?: emptyList()
-        }
-        return remember(unfiltered, searchQuery) {
-            if (searchQuery.isNullOrBlank()) {
-                queriedMangaMap.clear()
-                unfiltered
-            } else {
-                unfiltered.fastFilter { it.matches(searchQuery!!) }
-                    .also { queriedMangaMap[categoryId] = it }
-            }
+    suspend fun getRandomLibraryItemForCurrentCategory(): LibraryItem? {
+        return withIOContext {
+            state.value
+                .getLibraryItemsByCategoryId(activeCategory.toLong())
+                .randomOrNull()
         }
     }
 
     fun clearSelection() {
-        state.selection = emptyList()
+        mutableState.update { it.copy(selection = emptyList()) }
     }
 
     fun toggleSelection(manga: LibraryManga) {
-        state.selection = selection.toMutableList().apply {
-            if (fastAny { it.id == manga.id }) {
-                removeAll { it.id == manga.id }
-            } else {
-                add(manga)
+        mutableState.update { state ->
+            val newSelection = state.selection.toMutableList().apply {
+                if (fastAny { it.id == manga.id }) {
+                    removeAll { it.id == manga.id }
+                } else {
+                    add(manga)
+                }
             }
+            state.copy(selection = newSelection)
         }
     }
 
-    /**
-     * Map is cleared out via [getMangaForCategory] when [searchQuery] is null or blank
-     */
-    private val queriedMangaMap: MutableMap<Long, List<LibraryItem>> = mutableMapOf()
-
-    /**
-     * Used by select all, inverse and range selection.
-     *
-     * If current query is empty then we get manga list from [loadedManga] otherwise from [queriedMangaMap]
-     */
-    private fun getMangaForCategoryWithQuery(categoryId: Long, query: String?): List<LibraryItem> {
-        return if (query.isNullOrBlank()) loadedManga[categoryId].orEmpty() else queriedMangaMap[categoryId].orEmpty()
-    }
-
     /**
      * Selects all mangas between and including the given manga and the last pressed manga from the
      * same category as the given manga
      */
     fun toggleRangeSelection(manga: LibraryManga) {
-        state.selection = selection.toMutableList().apply {
-            val lastSelected = lastOrNull()
-            if (lastSelected?.category != manga.category) {
-                add(manga)
-                return@apply
-            }
+        mutableState.update { state ->
+            val newSelection = state.selection.toMutableList().apply {
+                val lastSelected = lastOrNull()
+                if (lastSelected?.category != manga.category) {
+                    add(manga)
+                    return@apply
+                }
 
-            val items = getMangaForCategoryWithQuery(manga.category, searchQuery)
-                .fastMap { it.libraryManga }
-            val lastMangaIndex = items.indexOf(lastSelected)
-            val curMangaIndex = items.indexOf(manga)
-
-            val selectedIds = fastMap { it.id }
-            val selectionRange = when {
-                lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex)
-                curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex)
-                // We shouldn't reach this point
-                else -> return@apply
-            }
-            val newSelections = selectionRange.mapNotNull { index ->
-                items[index].takeUnless { it.id in selectedIds }
+                val items = state.getLibraryItemsByCategoryId(manga.category)
+                    .fastMap { it.libraryManga }
+                val lastMangaIndex = items.indexOf(lastSelected)
+                val curMangaIndex = items.indexOf(manga)
+
+                val selectedIds = fastMap { it.id }
+                val selectionRange = when {
+                    lastMangaIndex < curMangaIndex -> IntRange(lastMangaIndex, curMangaIndex)
+                    curMangaIndex < lastMangaIndex -> IntRange(curMangaIndex, lastMangaIndex)
+                    // We shouldn't reach this point
+                    else -> return@apply
+                }
+                val newSelections = selectionRange.mapNotNull { index ->
+                    items[index].takeUnless { it.id in selectedIds }
+                }
+                addAll(newSelections)
             }
-            addAll(newSelections)
+            state.copy(selection = newSelection)
         }
     }
 
     fun selectAll(index: Int) {
-        state.selection = state.selection.toMutableList().apply {
-            val categoryId = categories.getOrNull(index)?.id ?: -1
-            val selectedIds = fastMap { it.id }
-            val newSelections = getMangaForCategoryWithQuery(categoryId, searchQuery)
-                .fastMapNotNull { item ->
-                    item.libraryManga.takeUnless { it.id in selectedIds }
-                }
+        mutableState.update { state ->
+            val newSelection = state.selection.toMutableList().apply {
+                val categoryId = state.categories.getOrNull(index)?.id ?: -1
+                val selectedIds = fastMap { it.id }
+                val newSelections = state.getLibraryItemsByCategoryId(categoryId)
+                    .fastMapNotNull { item ->
+                        item.libraryManga.takeUnless { it.id in selectedIds }
+                    }
 
-            addAll(newSelections)
+                addAll(newSelections)
+            }
+            state.copy(selection = newSelection)
         }
     }
 
     fun invertSelection(index: Int) {
-        state.selection = selection.toMutableList().apply {
-            val categoryId = categories[index].id
-            val items = getMangaForCategoryWithQuery(categoryId, searchQuery).fastMap { it.libraryManga }
-            val selectedIds = fastMap { it.id }
-            val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds }
-            val toRemoveIds = toRemove.fastMap { it.id }
-            removeAll { it.id in toRemoveIds }
-            addAll(toAdd)
+        mutableState.update { state ->
+            val newSelection = state.selection.toMutableList().apply {
+                val categoryId = state.categories[index].id
+                val items = state.getLibraryItemsByCategoryId(categoryId).fastMap { it.libraryManga }
+                val selectedIds = fastMap { it.id }
+                val (toRemove, toAdd) = items.fastPartition { it.id in selectedIds }
+                val toRemoveIds = toRemove.fastMap { it.id }
+                removeAll { it.id in toRemoveIds }
+                addAll(toAdd)
+            }
+            state.copy(selection = newSelection)
+        }
+    }
+
+    fun search(query: String?) {
+        mutableState.update { it.copy(searchQuery = query) }
+    }
+
+    fun openChangeCategoryDialog() {
+        coroutineScope.launchIO {
+            // Create a copy of selected manga
+            val mangaList = state.value.selection.map { it.manga }
+
+            // Hide the default category because it has a different behavior than the ones from db.
+            val categories = state.value.categories.filter { it.id != 0L }
+
+            // Get indexes of the common categories to preselect.
+            val common = getCommonCategories(mangaList)
+            // Get indexes of the mix categories to preselect.
+            val mix = getMixCategories(mangaList)
+            val preselected = categories.map {
+                when (it) {
+                    in common -> CheckboxState.State.Checked(it)
+                    in mix -> CheckboxState.TriState.Exclude(it)
+                    else -> CheckboxState.State.None(it)
+                }
+            }
+            mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) }
         }
     }
 
+    fun openDeleteMangaDialog() {
+        val mangaList = state.value.selection.map { it.manga }
+        mutableState.update { it.copy(dialog = Dialog.DeleteManga(mangaList)) }
+    }
+
+    fun closeDialog() {
+        mutableState.update { it.copy(dialog = null) }
+    }
+
     sealed class Dialog {
         data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
         data class DeleteManga(val manga: List<Manga>) : Dialog()
         data class DownloadCustomAmount(val manga: List<Manga>, val max: Int) : Dialog()
     }
+
+    @Immutable
+    private data class ItemPreferences(
+        val downloadBadge: Boolean,
+        val unreadBadge: Boolean,
+        val localBadge: Boolean,
+        val languageBadge: Boolean,
+
+        val globalFilterDownloaded: Boolean,
+        val filterDownloaded: Int,
+        val filterUnread: Int,
+        val filterStarted: Int,
+        val filterBookmarked: Int,
+        val filterCompleted: Int,
+    )
+
+    @Immutable
+    data class State(
+        val isLoading: Boolean = true,
+        val library: LibraryMap = emptyMap(),
+        val searchQuery: String? = null,
+        val selection: List<LibraryManga> = emptyList(),
+        val hasActiveFilters: Boolean = false,
+        val showCategoryTabs: Boolean = false,
+        val showMangaCount: Boolean = false,
+        val showMangaContinueButton: Boolean = false,
+        val dialog: Dialog? = null,
+    ) {
+        val selectionMode = selection.isNotEmpty()
+
+        val categories = library.keys.toList()
+
+        val libraryCount by lazy {
+            library
+                .flatMap { (_, v) -> v }
+                .distinctBy { it.libraryManga.manga.id }
+                .size
+        }
+
+        fun getLibraryItemsByCategoryId(categoryId: Long): List<LibraryItem> {
+            return library.firstNotNullOf { (k, v) -> v.takeIf { k.id == categoryId } }
+        }
+
+        fun getLibraryItemsByPage(page: Int): List<LibraryItem> {
+            return library.values.toTypedArray().getOrNull(page) ?: emptyList()
+        }
+
+        fun getMangaCountForCategory(category: Category): Int? {
+            return library[category]?.size?.takeIf { showMangaCount }
+        }
+
+        fun getToolbarTitle(
+            defaultTitle: String,
+            defaultCategoryTitle: String,
+            page: Int,
+        ): LibraryToolbarTitle {
+            val category = categories.getOrNull(page) ?: return LibraryToolbarTitle(defaultTitle)
+            val categoryName = category.let {
+                if (it.isSystemCategory) {
+                    defaultCategoryTitle
+                } else {
+                    it.name
+                }
+            }
+
+            val title = if (showCategoryTabs) defaultTitle else categoryName
+            val count = when {
+                !showMangaCount -> null
+                !showCategoryTabs -> getMangaCountForCategory(category)
+                // Whole library count
+                else -> libraryCount
+            }
+
+            return LibraryToolbarTitle(title, count)
+        }
+    }
 }

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt

@@ -32,7 +32,6 @@ class LibrarySettingsSheet(
     private val trackManager: TrackManager = Injekt.get(),
     private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
     private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
-    onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
 ) : TabbedBottomSheetDialog(router.activity!!) {
 
     val filters: Filter
@@ -43,13 +42,8 @@ class LibrarySettingsSheet(
 
     init {
         filters = Filter(router.activity!!)
-        filters.onGroupClicked = onGroupClickListener
-
         sort = Sort(router.activity!!)
-        sort.onGroupClicked = onGroupClickListener
-
         display = Display(router.activity!!)
-        display.onGroupClicked = onGroupClickListener
     }
 
     /**