From 727399611ddff3530d66ad7a758f357f95053b92 Mon Sep 17 00:00:00 2001
From: arkon <eugcheung94@gmail.com>
Date: Fri, 9 Dec 2022 17:34:24 -0500
Subject: [PATCH] Migrate library settings sheet to Compose

---
 .../library/service/LibraryPreferences.kt     |  14 +-
 .../presentation/components/SettingsItems.kt  |  49 +-
 .../presentation/components/TabbedDialog.kt   |  22 +-
 .../library/LibrarySettingsDialog.kt          | 249 +++++++++
 .../java/eu/kanade/tachiyomi/Migrations.kt    |   6 +-
 .../ui/library/LibraryScreenModel.kt          |  35 +-
 .../ui/library/LibrarySettingsScreenModel.kt  |  84 ++++
 .../ui/library/LibrarySettingsSheet.kt        | 474 ------------------
 .../kanade/tachiyomi/ui/library/LibraryTab.kt |  23 +-
 .../kanade/tachiyomi/ui/main/MainActivity.kt  |  28 --
 .../widget/ExtendedNavigationView.kt          | 270 ----------
 .../eu/kanade/tachiyomi/widget/TriState.kt    |  19 +
 12 files changed, 443 insertions(+), 830 deletions(-)
 create mode 100644 app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt
 delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
 delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt

diff --git a/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt b/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt
index a29893dd69..2394696012 100644
--- a/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt
+++ b/app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt
@@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
 import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
 import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
 import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView
+import eu.kanade.tachiyomi.widget.TriState
 import tachiyomi.core.preference.PreferenceStore
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.library.model.LibrarySort
@@ -36,17 +36,17 @@ class LibraryPreferences(
 
     // region Filter
 
-    fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", TriState.DISABLED.value)
 
-    fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", TriState.DISABLED.value)
 
-    fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", TriState.DISABLED.value)
 
-    fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", TriState.DISABLED.value)
 
-    fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", TriState.DISABLED.value)
 
-    fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value)
+    fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", TriState.DISABLED.value)
 
     // endregion
 
diff --git a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt
index e4fa2172a8..f670acf54b 100644
--- a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt
@@ -1,5 +1,6 @@
 package eu.kanade.presentation.components
 
+import androidx.annotation.StringRes
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
@@ -14,6 +15,7 @@ import androidx.compose.material.icons.filled.ArrowUpward
 import androidx.compose.material.icons.rounded.CheckBox
 import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
 import androidx.compose.material.icons.rounded.DisabledByDefault
+import androidx.compose.material3.Checkbox
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.RadioButton
@@ -21,19 +23,35 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import tachiyomi.domain.manga.model.TriStateFilter
+import tachiyomi.presentation.core.theme.header
+
+@Composable
+fun HeadingItem(
+    @StringRes labelRes: Int,
+) {
+    Text(
+        text = stringResource(labelRes),
+        style = MaterialTheme.typography.header,
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
+    )
+}
 
 @Composable
 fun TriStateItem(
     label: String,
     state: TriStateFilter,
+    enabled: Boolean = true,
     onClick: ((TriStateFilter) -> Unit)?,
 ) {
     Row(
         modifier = Modifier
             .clickable(
-                enabled = onClick != null,
+                enabled = enabled && onClick != null,
                 onClick = {
                     when (state) {
                         TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
@@ -47,7 +65,7 @@ fun TriStateItem(
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(24.dp),
     ) {
-        val stateAlpha = if (onClick != null) 1f else ContentAlpha.disabled
+        val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
 
         Icon(
             imageVector = when (state) {
@@ -56,7 +74,7 @@ fun TriStateItem(
                 TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
             },
             contentDescription = null,
-            tint = if (state == TriStateFilter.DISABLED) {
+            tint = if (!enabled || state == TriStateFilter.DISABLED) {
                 MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
             } else {
                 when (onClick) {
@@ -109,6 +127,31 @@ fun SortItem(
     }
 }
 
+@Composable
+fun CheckboxItem(
+    label: String,
+    checked: Boolean,
+    onClick: () -> Unit,
+) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth()
+            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(24.dp),
+    ) {
+        Checkbox(
+            checked = checked,
+            onCheckedChange = null,
+        )
+        Text(
+            text = label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}
+
 @Composable
 fun RadioItem(
     label: String,
diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt
index f7438e175b..15af354496 100644
--- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt
@@ -1,11 +1,11 @@
 package eu.kanade.presentation.components
 
+import androidx.compose.animation.animateContentSize
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
@@ -20,12 +20,9 @@ import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastForEachIndexed
@@ -85,26 +82,13 @@ fun TabbedDialog(
             }
             Divider()
 
-            val density = LocalDensity.current
-            var largestHeight by rememberSaveable { mutableStateOf(0f) }
             HorizontalPager(
-                modifier = Modifier.heightIn(min = largestHeight.dp),
+                modifier = Modifier.animateContentSize(),
                 count = tabTitles.size,
                 state = pagerState,
                 verticalAlignment = Alignment.Top,
             ) { page ->
-                Box(
-                    modifier = Modifier.onSizeChanged {
-                        with(density) {
-                            val heightDp = it.height.toDp()
-                            if (heightDp.value > largestHeight) {
-                                largestHeight = heightDp.value
-                            }
-                        }
-                    },
-                ) {
-                    content(contentPadding, page)
-                }
+                content(contentPadding, page)
             }
         }
     }
diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
new file mode 100644
index 0000000000..afa6ccad66
--- /dev/null
+++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
@@ -0,0 +1,249 @@
+package eu.kanade.presentation.library
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import eu.kanade.domain.library.service.LibraryPreferences
+import eu.kanade.presentation.components.CheckboxItem
+import eu.kanade.presentation.components.HeadingItem
+import eu.kanade.presentation.components.RadioItem
+import eu.kanade.presentation.components.SortItem
+import eu.kanade.presentation.components.TabbedDialog
+import eu.kanade.presentation.components.TabbedDialogPaddings
+import eu.kanade.presentation.components.TriStateItem
+import eu.kanade.presentation.util.collectAsState
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.library.LibrarySettingsScreenModel
+import eu.kanade.tachiyomi.widget.toTriStateFilter
+import tachiyomi.domain.category.model.Category
+import tachiyomi.domain.library.model.LibraryDisplayMode
+import tachiyomi.domain.library.model.LibrarySort
+import tachiyomi.domain.library.model.display
+import tachiyomi.domain.library.model.sort
+import tachiyomi.domain.manga.model.TriStateFilter
+
+@Composable
+fun LibrarySettingsDialog(
+    onDismissRequest: () -> Unit,
+    screenModel: LibrarySettingsScreenModel,
+    activeCategoryIndex: Int,
+) {
+    val state by screenModel.state.collectAsState()
+    val category by remember(activeCategoryIndex) {
+        derivedStateOf { state.categories[activeCategoryIndex] }
+    }
+
+    TabbedDialog(
+        onDismissRequest = onDismissRequest,
+        tabTitles = listOf(
+            stringResource(R.string.action_filter),
+            stringResource(R.string.action_sort),
+            stringResource(R.string.action_display),
+        ),
+    ) { contentPadding, page ->
+        Column(
+            modifier = Modifier
+                .padding(contentPadding)
+                .padding(vertical = TabbedDialogPaddings.Vertical)
+                .verticalScroll(rememberScrollState()),
+        ) {
+            when (page) {
+                0 -> FilterPage(
+                    screenModel = screenModel,
+                )
+                1 -> SortPage(
+                    category = category,
+                    screenModel = screenModel,
+                )
+                2 -> DisplayPage(
+                    category = category,
+                    screenModel = screenModel,
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun ColumnScope.FilterPage(
+    screenModel: LibrarySettingsScreenModel,
+) {
+    val filterDownloaded by screenModel.libraryPreferences.filterDownloaded().collectAsState()
+    val downloadedOnly by screenModel.preferences.downloadedOnly().collectAsState()
+    TriStateItem(
+        label = stringResource(R.string.label_downloaded),
+        state = if (downloadedOnly) {
+            TriStateFilter.ENABLED_IS
+        } else {
+            filterDownloaded.toTriStateFilter()
+        },
+        enabled = !downloadedOnly,
+        onClick = { screenModel.toggleFilter(LibraryPreferences::filterDownloaded) },
+    )
+    val filterUnread by screenModel.libraryPreferences.filterUnread().collectAsState()
+    TriStateItem(
+        label = stringResource(R.string.action_filter_unread),
+        state = filterUnread.toTriStateFilter(),
+        onClick = { screenModel.toggleFilter(LibraryPreferences::filterUnread) },
+    )
+    val filterStarted by screenModel.libraryPreferences.filterStarted().collectAsState()
+    TriStateItem(
+        label = stringResource(R.string.label_started),
+        state = filterStarted.toTriStateFilter(),
+        onClick = { screenModel.toggleFilter(LibraryPreferences::filterStarted) },
+    )
+    val filterBookmarked by screenModel.libraryPreferences.filterBookmarked().collectAsState()
+    TriStateItem(
+        label = stringResource(R.string.action_filter_bookmarked),
+        state = filterBookmarked.toTriStateFilter(),
+        onClick = { screenModel.toggleFilter(LibraryPreferences::filterBookmarked) },
+    )
+    val filterCompleted by screenModel.libraryPreferences.filterCompleted().collectAsState()
+    TriStateItem(
+        label = stringResource(R.string.completed),
+        state = filterCompleted.toTriStateFilter(),
+        onClick = { screenModel.toggleFilter(LibraryPreferences::filterCompleted) },
+    )
+
+    when (screenModel.trackServices.size) {
+        0 -> {
+            // No trackers
+        }
+        1 -> {
+            val service = screenModel.trackServices[0]
+            val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
+            TriStateItem(
+                label = stringResource(R.string.action_filter_tracked),
+                state = filterTracker.toTriStateFilter(),
+                onClick = { screenModel.toggleTracker(service.id.toInt()) },
+            )
+        }
+        else -> {
+            HeadingItem(R.string.action_filter_tracked)
+            screenModel.trackServices.map { service ->
+                val filterTracker by screenModel.libraryPreferences.filterTracking(service.id.toInt()).collectAsState()
+                TriStateItem(
+                    label = stringResource(service.nameRes()),
+                    state = filterTracker.toTriStateFilter(),
+                    onClick = { screenModel.toggleTracker(service.id.toInt()) },
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun ColumnScope.SortPage(
+    category: Category,
+    screenModel: LibrarySettingsScreenModel,
+) {
+    val sortingMode = category.sort.type
+    val sortDescending = !category.sort.isAscending
+
+    listOf(
+        R.string.action_sort_alpha to LibrarySort.Type.Alphabetical,
+        R.string.action_sort_total to LibrarySort.Type.TotalChapters,
+        R.string.action_sort_last_read to LibrarySort.Type.LastRead,
+        R.string.action_sort_last_manga_update to LibrarySort.Type.LastUpdate,
+        R.string.action_sort_unread_count to LibrarySort.Type.UnreadCount,
+        R.string.action_sort_latest_chapter to LibrarySort.Type.LatestChapter,
+        R.string.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
+        R.string.action_sort_date_added to LibrarySort.Type.DateAdded,
+    ).map { (titleRes, mode) ->
+        SortItem(
+            label = stringResource(titleRes),
+            sortDescending = sortDescending.takeIf { sortingMode == mode },
+            onClick = {
+                val isTogglingDirection = sortingMode == mode
+                val direction = when {
+                    isTogglingDirection -> if (sortDescending) LibrarySort.Direction.Ascending else LibrarySort.Direction.Descending
+                    else -> if (sortDescending) LibrarySort.Direction.Descending else LibrarySort.Direction.Ascending
+                }
+                screenModel.setSort(category, mode, direction)
+            },
+        )
+    }
+}
+
+@Composable
+private fun ColumnScope.DisplayPage(
+    category: Category,
+    screenModel: LibrarySettingsScreenModel,
+) {
+    HeadingItem(R.string.action_display_mode)
+    listOf(
+        R.string.action_display_grid to LibraryDisplayMode.CompactGrid,
+        R.string.action_display_comfortable_grid to LibraryDisplayMode.ComfortableGrid,
+        R.string.action_display_cover_only_grid to LibraryDisplayMode.CoverOnlyGrid,
+        R.string.action_display_list to LibraryDisplayMode.List,
+    ).map { (titleRes, mode) ->
+        RadioItem(
+            label = stringResource(titleRes),
+            selected = category.display == mode,
+            onClick = { screenModel.setDisplayMode(category, mode) },
+        )
+    }
+
+    HeadingItem(R.string.badges_header)
+    val downloadBadge by screenModel.libraryPreferences.downloadBadge().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_download_badge),
+        checked = downloadBadge,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::downloadBadge)
+        },
+    )
+    val localBadge by screenModel.libraryPreferences.localBadge().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_local_badge),
+        checked = localBadge,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::localBadge)
+        },
+    )
+    val languageBadge by screenModel.libraryPreferences.languageBadge().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_language_badge),
+        checked = languageBadge,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::languageBadge)
+        },
+    )
+
+    HeadingItem(R.string.tabs_header)
+    val categoryTabs by screenModel.libraryPreferences.categoryTabs().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_show_tabs),
+        checked = categoryTabs,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::categoryTabs)
+        },
+    )
+    val categoryNumberOfItems by screenModel.libraryPreferences.categoryNumberOfItems().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_show_number_of_items),
+        checked = categoryNumberOfItems,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::categoryNumberOfItems)
+        },
+    )
+
+    HeadingItem(R.string.other_header)
+    val showContinueReadingButton by screenModel.libraryPreferences.showContinueReadingButton().collectAsState()
+    CheckboxItem(
+        label = stringResource(R.string.action_display_show_continue_reading_button),
+        checked = showContinueReadingButton,
+        onClick = {
+            screenModel.togglePreference(LibraryPreferences::showContinueReadingButton)
+        },
+    )
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
index 821a46c296..dd2e6ee5e2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
@@ -23,7 +23,7 @@ import eu.kanade.tachiyomi.util.preference.minusAssign
 import eu.kanade.tachiyomi.util.preference.plusAssign
 import eu.kanade.tachiyomi.util.system.DeviceUtil
 import eu.kanade.tachiyomi.util.system.toast
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView
+import eu.kanade.tachiyomi.widget.TriState
 import tachiyomi.core.preference.PreferenceStore
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -114,9 +114,9 @@ object Migrations {
                 fun convertBooleanPrefToTriState(key: String): Int {
                     val oldPrefValue = prefs.getBoolean(key, false)
                     return if (oldPrefValue) {
-                        ExtendedNavigationView.Item.TriStateGroup.State.ENABLED_IS.value
+                        TriState.ENABLED_IS.value
                     } else {
-                        ExtendedNavigationView.Item.TriStateGroup.State.DISABLED.value
+                        TriState.DISABLED.value
                     }
                 }
                 prefs.edit {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
index 8c13b4a851..e6724463b0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
@@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.model.SManga
 import eu.kanade.tachiyomi.source.online.HttpSource
 import eu.kanade.tachiyomi.util.chapter.getNextUnread
 import eu.kanade.tachiyomi.util.removeCovers
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup
+import eu.kanade.tachiyomi.widget.TriState
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
@@ -149,8 +149,8 @@ class LibraryScreenModel(
                     prefs.filterStarted or
                     prefs.filterBookmarked or
                     prefs.filterCompleted
-                ) != TriStateGroup.State.DISABLED.value
-            val b = trackFilter.values.any { it != TriStateGroup.State.DISABLED.value }
+                ) != TriState.DISABLED.value
+            val b = trackFilter.values.any { it != TriState.DISABLED.value }
             a || b
         }
             .distinctUntilChanged()
@@ -179,17 +179,17 @@ class LibraryScreenModel(
 
         val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
 
-        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_NOT.value) it.key else null }
-        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateGroup.State.ENABLED_IS.value) it.key else null }
+        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT.value) it.key else null }
+        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS.value) it.key else null }
         val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
 
         val filterFnDownloaded: (LibraryItem) -> Boolean = downloaded@{
-            if (!downloadedOnly && filterDownloaded == TriStateGroup.State.DISABLED.value) return@downloaded true
+            if (!downloadedOnly && filterDownloaded == TriState.DISABLED.value) return@downloaded true
 
             val isDownloaded = it.libraryManga.manga.isLocal() ||
                 it.downloadCount > 0 ||
                 downloadManager.getDownloadCount(it.libraryManga.manga) > 0
-            return@downloaded if (downloadedOnly || filterDownloaded == TriStateGroup.State.ENABLED_IS.value) {
+            return@downloaded if (downloadedOnly || filterDownloaded == TriState.ENABLED_IS.value) {
                 isDownloaded
             } else {
                 !isDownloaded
@@ -197,10 +197,10 @@ class LibraryScreenModel(
         }
 
         val filterFnUnread: (LibraryItem) -> Boolean = unread@{
-            if (filterUnread == TriStateGroup.State.DISABLED.value) return@unread true
+            if (filterUnread == TriState.DISABLED.value) return@unread true
 
             val isUnread = it.libraryManga.unreadCount > 0
-            return@unread if (filterUnread == TriStateGroup.State.ENABLED_IS.value) {
+            return@unread if (filterUnread == TriState.ENABLED_IS.value) {
                 isUnread
             } else {
                 !isUnread
@@ -208,10 +208,10 @@ class LibraryScreenModel(
         }
 
         val filterFnStarted: (LibraryItem) -> Boolean = started@{
-            if (filterStarted == TriStateGroup.State.DISABLED.value) return@started true
+            if (filterStarted == TriState.DISABLED.value) return@started true
 
             val hasStarted = it.libraryManga.hasStarted
-            return@started if (filterStarted == TriStateGroup.State.ENABLED_IS.value) {
+            return@started if (filterStarted == TriState.ENABLED_IS.value) {
                 hasStarted
             } else {
                 !hasStarted
@@ -219,10 +219,10 @@ class LibraryScreenModel(
         }
 
         val filterFnBookmarked: (LibraryItem) -> Boolean = bookmarked@{
-            if (filterBookmarked == TriStateGroup.State.DISABLED.value) return@bookmarked true
+            if (filterBookmarked == TriState.DISABLED.value) return@bookmarked true
 
             val hasBookmarks = it.libraryManga.hasBookmarks
-            return@bookmarked if (filterBookmarked == TriStateGroup.State.ENABLED_IS.value) {
+            return@bookmarked if (filterBookmarked == TriState.ENABLED_IS.value) {
                 hasBookmarks
             } else {
                 !hasBookmarks
@@ -230,10 +230,10 @@ class LibraryScreenModel(
         }
 
         val filterFnCompleted: (LibraryItem) -> Boolean = completed@{
-            if (filterCompleted == TriStateGroup.State.DISABLED.value) return@completed true
+            if (filterCompleted == TriState.DISABLED.value) return@completed true
 
             val isCompleted = it.libraryManga.manga.status.toInt() == SManga.COMPLETED
-            return@completed if (filterCompleted == TriStateGroup.State.ENABLED_IS.value) {
+            return@completed if (filterCompleted == TriState.ENABLED_IS.value) {
                 isCompleted
             } else {
                 !isCompleted
@@ -572,6 +572,10 @@ class LibraryScreenModel(
         }
     }
 
+    fun showSettingsDialog() {
+        mutableState.update { it.copy(dialog = Dialog.SettingsSheet) }
+    }
+
     fun clearSelection() {
         mutableState.update { it.copy(selection = emptyList()) }
     }
@@ -690,6 +694,7 @@ class LibraryScreenModel(
     }
 
     sealed class Dialog {
+        object SettingsSheet : Dialog()
         data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog()
         data class DeleteManga(val manga: List<Manga>) : Dialog()
     }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt
new file mode 100644
index 0000000000..6cb681f89c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt
@@ -0,0 +1,84 @@
+package eu.kanade.tachiyomi.ui.library
+
+import androidx.compose.runtime.Immutable
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.coroutineScope
+import eu.kanade.domain.base.BasePreferences
+import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
+import eu.kanade.domain.category.interactor.SetSortModeForCategory
+import eu.kanade.domain.library.service.LibraryPreferences
+import eu.kanade.tachiyomi.data.track.TrackManager
+import eu.kanade.tachiyomi.util.preference.toggle
+import eu.kanade.tachiyomi.widget.TriState
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.update
+import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.getAndSet
+import tachiyomi.core.util.lang.launchIO
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.category.model.Category
+import tachiyomi.domain.library.model.LibraryDisplayMode
+import tachiyomi.domain.library.model.LibrarySort
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class LibrarySettingsScreenModel(
+    val preferences: BasePreferences = Injekt.get(),
+    val libraryPreferences: LibraryPreferences = Injekt.get(),
+    private val getCategories: GetCategories = Injekt.get(),
+    private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
+    private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
+    trackManager: TrackManager = Injekt.get(),
+) : StateScreenModel<LibrarySettingsScreenModel.State>(State()) {
+
+    val trackServices = trackManager.services.filter { service -> service.isLogged }
+
+    init {
+        coroutineScope.launchIO {
+            getCategories.subscribe()
+                .collectLatest {
+                    mutableState.update { state ->
+                        state.copy(
+                            categories = it,
+                        )
+                    }
+                }
+        }
+    }
+
+    fun togglePreference(preference: (LibraryPreferences) -> Preference<Boolean>) {
+        preference(libraryPreferences).toggle()
+    }
+
+    fun toggleFilter(preference: (LibraryPreferences) -> Preference<Int>) {
+        preference(libraryPreferences).getAndSet {
+            when (it) {
+                TriState.DISABLED.value -> TriState.ENABLED_IS.value
+                TriState.ENABLED_IS.value -> TriState.ENABLED_NOT.value
+                TriState.ENABLED_NOT.value -> TriState.DISABLED.value
+                else -> throw IllegalStateException("Unknown TriStateGroup state: $this")
+            }
+        }
+    }
+
+    fun toggleTracker(id: Int) {
+        toggleFilter { libraryPreferences.filterTracking(id) }
+    }
+
+    fun setDisplayMode(category: Category, mode: LibraryDisplayMode) {
+        coroutineScope.launchIO {
+            setDisplayModeForCategory.await(category, mode)
+        }
+    }
+
+    fun setSort(category: Category, mode: LibrarySort.Type, direction: LibrarySort.Direction) {
+        coroutineScope.launchIO {
+            setSortModeForCategory.await(category, mode, direction)
+        }
+    }
+
+    @Immutable
+    data class State(
+        val categories: List<Category> = emptyList(),
+    )
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
deleted file mode 100644
index 8db1fc3475..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
+++ /dev/null
@@ -1,474 +0,0 @@
-package eu.kanade.tachiyomi.ui.library
-
-import android.app.Activity
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import eu.kanade.domain.base.BasePreferences
-import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
-import eu.kanade.domain.category.interactor.SetSortModeForCategory
-import eu.kanade.domain.library.service.LibraryPreferences
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.data.track.TrackManager
-import eu.kanade.tachiyomi.data.track.TrackService
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView
-import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
-import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import tachiyomi.core.util.lang.launchIO
-import tachiyomi.domain.category.model.Category
-import tachiyomi.domain.library.model.LibraryDisplayMode
-import tachiyomi.domain.library.model.LibrarySort
-import tachiyomi.domain.library.model.display
-import tachiyomi.domain.library.model.sort
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import uy.kohesive.injekt.injectLazy
-
-class LibrarySettingsSheet(
-    activity: Activity,
-    private val trackManager: TrackManager = Injekt.get(),
-    private val setDisplayModeForCategory: SetDisplayModeForCategory = Injekt.get(),
-    private val setSortModeForCategory: SetSortModeForCategory = Injekt.get(),
-) : TabbedBottomSheetDialog(activity) {
-
-    val filters: Filter
-    private val sort: Sort
-    private val display: Display
-
-    val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
-
-    init {
-        filters = Filter(activity)
-        sort = Sort(activity)
-        display = Display(activity)
-    }
-
-    /**
-     * adjusts selected button to match real state.
-     * @param currentCategory ID of currently shown category
-     */
-    fun show(currentCategory: Category) {
-        filters.adjustFilterSelection()
-
-        sort.currentCategory = currentCategory
-        sort.adjustDisplaySelection()
-
-        display.currentCategory = currentCategory
-        display.adjustDisplaySelection()
-
-        super.show()
-    }
-
-    override fun getTabViews(): List<View> = listOf(
-        filters,
-        sort,
-        display,
-    )
-
-    override fun getTabTitles(): List<Int> = listOf(
-        R.string.action_filter,
-        R.string.action_sort,
-        R.string.action_display,
-    )
-
-    /**
-     * Filters group (unread, downloaded, ...).
-     */
-    inner class Filter @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-        Settings(context, attrs) {
-
-        private val filterGroup = FilterGroup()
-
-        init {
-            setGroups(listOf(filterGroup))
-        }
-
-        // Refreshes Filter Setting selections
-        fun adjustFilterSelection() {
-            filterGroup.initModels()
-            filterGroup.items.forEach { adapter.notifyItemChanged(it) }
-        }
-
-        /**
-         * Returns true if there's at least one filter from [FilterGroup] active.
-         */
-        fun hasActiveFilters(): Boolean {
-            return filterGroup.items.filterIsInstance<Item.TriStateGroup>().any { it.state != State.DISABLED.value }
-        }
-
-        inner class FilterGroup : Group {
-
-            private val downloaded = Item.TriStateGroup(R.string.label_downloaded, this)
-            private val unread = Item.TriStateGroup(R.string.action_filter_unread, this)
-            private val started = Item.TriStateGroup(R.string.label_started, this)
-            private val bookmarked = Item.TriStateGroup(R.string.action_filter_bookmarked, this)
-            private val completed = Item.TriStateGroup(R.string.completed, this)
-            private val trackFilters: Map<Long, Item.TriStateGroup>
-
-            override val header = null
-            override val items: List<Item>
-            override val footer = null
-
-            init {
-                trackManager.services.filter { service -> service.isLogged }
-                    .also { services ->
-                        val size = services.size
-                        trackFilters = services.associate { service ->
-                            Pair(service.id, Item.TriStateGroup(getServiceResId(service, size), this))
-                        }
-                        val list: MutableList<Item> = mutableListOf(downloaded, unread, started, bookmarked, completed)
-                        if (size > 1) list.add(Item.Header(R.string.action_filter_tracked))
-                        list.addAll(trackFilters.values)
-                        items = list
-                    }
-            }
-
-            private fun getServiceResId(service: TrackService, size: Int): Int {
-                return if (size > 1) service.nameRes() else R.string.action_filter_tracked
-            }
-
-            override fun initModels() {
-                if (preferences.downloadedOnly().get()) {
-                    downloaded.state = State.ENABLED_IS.value
-                    downloaded.enabled = false
-                } else {
-                    downloaded.state = libraryPreferences.filterDownloaded().get()
-                    downloaded.enabled = true
-                }
-                unread.state = libraryPreferences.filterUnread().get()
-                started.state = libraryPreferences.filterStarted().get()
-                bookmarked.state = libraryPreferences.filterBookmarked().get()
-                completed.state = libraryPreferences.filterCompleted().get()
-
-                trackFilters.forEach { trackFilter ->
-                    trackFilter.value.state = libraryPreferences.filterTracking(trackFilter.key.toInt()).get()
-                }
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.TriStateGroup
-                val newState = when (item.state) {
-                    State.DISABLED.value -> State.ENABLED_IS.value
-                    State.ENABLED_IS.value -> State.ENABLED_NOT.value
-                    State.ENABLED_NOT.value -> State.DISABLED.value
-                    else -> throw Exception("Unknown State")
-                }
-                item.state = newState
-                when (item) {
-                    downloaded -> libraryPreferences.filterDownloaded().set(newState)
-                    unread -> libraryPreferences.filterUnread().set(newState)
-                    started -> libraryPreferences.filterStarted().set(newState)
-                    bookmarked -> libraryPreferences.filterBookmarked().set(newState)
-                    completed -> libraryPreferences.filterCompleted().set(newState)
-                    else -> {
-                        trackFilters.forEach { trackFilter ->
-                            if (trackFilter.value == item) {
-                                libraryPreferences.filterTracking(trackFilter.key.toInt()).set(newState)
-                            }
-                        }
-                    }
-                }
-
-                adapter.notifyItemChanged(item)
-            }
-        }
-    }
-
-    /**
-     * Sorting group (alphabetically, by last read, ...) and ascending or descending.
-     */
-    inner class Sort @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-        Settings(context, attrs) {
-
-        private val sort = SortGroup()
-
-        init {
-            setGroups(listOf(sort))
-        }
-
-        // Refreshes Display Setting selections
-        fun adjustDisplaySelection() {
-            sort.initModels()
-            sort.items.forEach { adapter.notifyItemChanged(it) }
-        }
-
-        inner class SortGroup : Group {
-
-            private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
-            private val total = Item.MultiSort(R.string.action_sort_total, this)
-            private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
-            private val lastChecked = Item.MultiSort(R.string.action_sort_last_manga_update, this)
-            private val unread = Item.MultiSort(R.string.action_sort_unread_count, this)
-            private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
-            private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this)
-            private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
-
-            override val header = null
-            override val items =
-                listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded)
-            override val footer = null
-
-            override fun initModels() {
-                val sort = currentCategory.sort
-                val order = if (sort.isAscending) Item.MultiSort.SORT_ASC else Item.MultiSort.SORT_DESC
-
-                alphabetically.state =
-                    if (sort.type == LibrarySort.Type.Alphabetical) order else Item.MultiSort.SORT_NONE
-                lastRead.state =
-                    if (sort.type == LibrarySort.Type.LastRead) order else Item.MultiSort.SORT_NONE
-                lastChecked.state =
-                    if (sort.type == LibrarySort.Type.LastUpdate) order else Item.MultiSort.SORT_NONE
-                unread.state =
-                    if (sort.type == LibrarySort.Type.UnreadCount) order else Item.MultiSort.SORT_NONE
-                total.state =
-                    if (sort.type == LibrarySort.Type.TotalChapters) order else Item.MultiSort.SORT_NONE
-                latestChapter.state =
-                    if (sort.type == LibrarySort.Type.LatestChapter) order else Item.MultiSort.SORT_NONE
-                chapterFetchDate.state =
-                    if (sort.type == LibrarySort.Type.ChapterFetchDate) order else Item.MultiSort.SORT_NONE
-                dateAdded.state =
-                    if (sort.type == LibrarySort.Type.DateAdded) order else Item.MultiSort.SORT_NONE
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.MultiStateGroup
-                val prevState = item.state
-
-                item.group.items.forEach {
-                    (it as Item.MultiStateGroup).state =
-                        Item.MultiSort.SORT_NONE
-                }
-                item.state = when (prevState) {
-                    Item.MultiSort.SORT_NONE -> Item.MultiSort.SORT_ASC
-                    Item.MultiSort.SORT_ASC -> Item.MultiSort.SORT_DESC
-                    Item.MultiSort.SORT_DESC -> Item.MultiSort.SORT_ASC
-                    else -> throw Exception("Unknown state")
-                }
-
-                setSortPreference(item)
-
-                item.group.items.forEach { adapter.notifyItemChanged(it) }
-            }
-
-            private fun setSortPreference(item: Item.MultiStateGroup) {
-                val mode = when (item) {
-                    alphabetically -> LibrarySort.Type.Alphabetical
-                    lastRead -> LibrarySort.Type.LastRead
-                    lastChecked -> LibrarySort.Type.LastUpdate
-                    unread -> LibrarySort.Type.UnreadCount
-                    total -> LibrarySort.Type.TotalChapters
-                    latestChapter -> LibrarySort.Type.LatestChapter
-                    chapterFetchDate -> LibrarySort.Type.ChapterFetchDate
-                    dateAdded -> LibrarySort.Type.DateAdded
-                    else -> throw NotImplementedError("Unknown display mode")
-                }
-                val direction = if (item.state == Item.MultiSort.SORT_ASC) {
-                    LibrarySort.Direction.Ascending
-                } else {
-                    LibrarySort.Direction.Descending
-                }
-
-                sheetScope.launchIO {
-                    setSortModeForCategory.await(currentCategory!!, mode, direction)
-                }
-            }
-        }
-    }
-
-    /**
-     * Display group, to show the library as a list or a grid.
-     */
-    inner class Display @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
-        Settings(context, attrs) {
-
-        private val displayGroup: DisplayGroup
-        private val badgeGroup: BadgeGroup
-        private val tabsGroup: TabsGroup
-        private val otherGroup: OtherGroup
-
-        init {
-            displayGroup = DisplayGroup()
-            badgeGroup = BadgeGroup()
-            tabsGroup = TabsGroup()
-            otherGroup = OtherGroup()
-            setGroups(listOf(displayGroup, badgeGroup, tabsGroup, otherGroup))
-        }
-
-        // Refreshes Display Setting selections
-        fun adjustDisplaySelection() {
-            val mode = getDisplayModePreference()
-            displayGroup.setGroupSelections(mode)
-            displayGroup.items.forEach { adapter.notifyItemChanged(it) }
-        }
-
-        // Gets user preference of currently selected display mode at current category
-        private fun getDisplayModePreference(): LibraryDisplayMode {
-            return currentCategory.display
-        }
-
-        inner class DisplayGroup : Group {
-
-            private val compactGrid = Item.Radio(R.string.action_display_grid, this)
-            private val comfortableGrid = Item.Radio(R.string.action_display_comfortable_grid, this)
-            private val coverOnlyGrid = Item.Radio(R.string.action_display_cover_only_grid, this)
-            private val list = Item.Radio(R.string.action_display_list, this)
-
-            override val header = Item.Header(R.string.action_display_mode)
-            override val items = listOf(compactGrid, comfortableGrid, coverOnlyGrid, list)
-            override val footer = null
-
-            override fun initModels() {
-                val mode = getDisplayModePreference()
-                setGroupSelections(mode)
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.Radio
-                if (item.checked) return
-
-                item.group.items.forEach { (it as Item.Radio).checked = false }
-                item.checked = true
-
-                setDisplayModePreference(item)
-
-                item.group.items.forEach { adapter.notifyItemChanged(it) }
-            }
-
-            // Sets display group selections based on given mode
-            fun setGroupSelections(mode: LibraryDisplayMode) {
-                compactGrid.checked = mode == LibraryDisplayMode.CompactGrid
-                comfortableGrid.checked = mode == LibraryDisplayMode.ComfortableGrid
-                coverOnlyGrid.checked = mode == LibraryDisplayMode.CoverOnlyGrid
-                list.checked = mode == LibraryDisplayMode.List
-            }
-
-            private fun setDisplayModePreference(item: Item) {
-                val flag = when (item) {
-                    compactGrid -> LibraryDisplayMode.CompactGrid
-                    comfortableGrid -> LibraryDisplayMode.ComfortableGrid
-                    coverOnlyGrid -> LibraryDisplayMode.CoverOnlyGrid
-                    list -> LibraryDisplayMode.List
-                    else -> throw NotImplementedError("Unknown display mode")
-                }
-
-                sheetScope.launchIO {
-                    setDisplayModeForCategory.await(currentCategory!!, flag)
-                }
-            }
-        }
-
-        inner class BadgeGroup : Group {
-            private val downloadBadge = Item.CheckboxGroup(R.string.action_display_download_badge, this)
-            private val localBadge = Item.CheckboxGroup(R.string.action_display_local_badge, this)
-            private val languageBadge = Item.CheckboxGroup(R.string.action_display_language_badge, this)
-
-            override val header = Item.Header(R.string.badges_header)
-            override val items = listOf(downloadBadge, localBadge, languageBadge)
-            override val footer = null
-
-            override fun initModels() {
-                downloadBadge.checked = libraryPreferences.downloadBadge().get()
-                localBadge.checked = libraryPreferences.localBadge().get()
-                languageBadge.checked = libraryPreferences.languageBadge().get()
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.CheckboxGroup
-                item.checked = !item.checked
-                when (item) {
-                    downloadBadge -> libraryPreferences.downloadBadge().set((item.checked))
-                    localBadge -> libraryPreferences.localBadge().set((item.checked))
-                    languageBadge -> libraryPreferences.languageBadge().set((item.checked))
-                    else -> {}
-                }
-                adapter.notifyItemChanged(item)
-            }
-        }
-
-        inner class TabsGroup : Group {
-            private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this)
-            private val showNumberOfItems = Item.CheckboxGroup(R.string.action_display_show_number_of_items, this)
-
-            override val header = Item.Header(R.string.tabs_header)
-            override val items = listOf(showTabs, showNumberOfItems)
-            override val footer = null
-
-            override fun initModels() {
-                showTabs.checked = libraryPreferences.categoryTabs().get()
-                showNumberOfItems.checked = libraryPreferences.categoryNumberOfItems().get()
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.CheckboxGroup
-                item.checked = !item.checked
-                when (item) {
-                    showTabs -> libraryPreferences.categoryTabs().set(item.checked)
-                    showNumberOfItems -> libraryPreferences.categoryNumberOfItems().set(item.checked)
-                    else -> {}
-                }
-                adapter.notifyItemChanged(item)
-            }
-        }
-
-        inner class OtherGroup : Group {
-            private val showContinueReadingButton = Item.CheckboxGroup(R.string.action_display_show_continue_reading_button, this)
-
-            override val header = Item.Header(R.string.other_header)
-            override val items = listOf(showContinueReadingButton)
-            override val footer = null
-
-            override fun initModels() {
-                showContinueReadingButton.checked = libraryPreferences.showContinueReadingButton().get()
-            }
-
-            override fun onItemClicked(item: Item) {
-                item as Item.CheckboxGroup
-                item.checked = !item.checked
-                when (item) {
-                    showContinueReadingButton -> libraryPreferences.showContinueReadingButton().set(item.checked)
-                    else -> {}
-                }
-                adapter.notifyItemChanged(item)
-            }
-        }
-    }
-
-    open inner class Settings(context: Context, attrs: AttributeSet?) :
-        ExtendedNavigationView(context, attrs) {
-
-        val preferences: BasePreferences by injectLazy()
-        val libraryPreferences: LibraryPreferences by injectLazy()
-        lateinit var adapter: Adapter
-
-        /**
-         * Click listener to notify the parent fragment when an item from a group is clicked.
-         */
-        var onGroupClicked: (Group) -> Unit = {}
-
-        var currentCategory: Category? = null
-
-        fun setGroups(groups: List<Group>) {
-            adapter = Adapter(groups.map { it.createItems() }.flatten())
-            recycler.adapter = adapter
-
-            groups.forEach { it.initModels() }
-            addView(recycler)
-        }
-
-        /**
-         * Adapter of the recycler view.
-         */
-        inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
-
-            override fun onItemClicked(item: Item) {
-                if (item is GroupedItem) {
-                    item.group.onItemClicked(item)
-                    onGroupClicked(item.group)
-                }
-            }
-        }
-    }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
index 0e44d2d7b8..7f4749fa4a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
@@ -33,6 +33,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.EmptyScreenAction
 import eu.kanade.presentation.library.DeleteLibraryMangaDialog
+import eu.kanade.presentation.library.LibrarySettingsDialog
 import eu.kanade.presentation.library.components.LibraryContent
 import eu.kanade.presentation.library.components.LibraryToolbar
 import eu.kanade.presentation.manga.components.LibraryBottomActionMenu
@@ -83,6 +84,7 @@ object LibraryTab : Tab {
         val haptic = LocalHapticFeedback.current
 
         val screenModel = rememberScreenModel { LibraryScreenModel() }
+        val settingsScreenModel = rememberScreenModel { LibrarySettingsScreenModel() }
         val state by screenModel.state.collectAsState()
 
         val snackbarHostState = remember { SnackbarHostState() }
@@ -95,9 +97,6 @@ object LibraryTab : Tab {
             }
             started
         }
-        val onClickFilter: () -> Unit = {
-            scope.launch { sendSettingsSheetIntent(state.categories[screenModel.activeCategoryIndex]) }
-        }
 
         Scaffold(
             topBar = { scrollBehavior ->
@@ -114,7 +113,7 @@ object LibraryTab : Tab {
                     onClickUnselectAll = screenModel::clearSelection,
                     onClickSelectAll = { screenModel.selectAll(screenModel.activeCategoryIndex) },
                     onClickInvertSelection = { screenModel.invertSelection(screenModel.activeCategoryIndex) },
-                    onClickFilter = onClickFilter,
+                    onClickFilter = { screenModel.showSettingsDialog() },
                     onClickRefresh = { onClickRefresh(null) },
                     onClickOpenRandomManga = {
                         scope.launch {
@@ -201,6 +200,11 @@ object LibraryTab : Tab {
 
         val onDismissRequest = screenModel::closeDialog
         when (val dialog = state.dialog) {
+            is LibraryScreenModel.Dialog.SettingsSheet -> LibrarySettingsDialog(
+                onDismissRequest = onDismissRequest,
+                screenModel = settingsScreenModel,
+                activeCategoryIndex = screenModel.activeCategoryIndex,
+            )
             is LibraryScreenModel.Dialog.ChangeCategory -> {
                 ChangeCategoryDialog(
                     initialSelection = dialog.initialSelection,
@@ -235,8 +239,8 @@ object LibraryTab : Tab {
             }
         }
 
-        LaunchedEffect(state.selectionMode) {
-            HomeScreen.showBottomNav(!state.selectionMode)
+        LaunchedEffect(state.selectionMode, state.dialog) {
+            HomeScreen.showBottomNav(!state.selectionMode && state.dialog !is LibraryScreenModel.Dialog.SettingsSheet)
         }
 
         LaunchedEffect(state.isLoading) {
@@ -247,7 +251,7 @@ object LibraryTab : Tab {
 
         LaunchedEffect(Unit) {
             launch { queryEvent.receiveAsFlow().collect(screenModel::search) }
-            launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { onClickFilter() } }
+            launch { requestSettingsSheetEvent.receiveAsFlow().collectLatest { screenModel.showSettingsDialog() } }
         }
     }
 
@@ -257,8 +261,5 @@ object LibraryTab : Tab {
 
     // For opening settings sheet in LibraryController
     private val requestSettingsSheetEvent = Channel<Unit>()
-    private val openSettingsSheetEvent_ = Channel<Category>()
-    val openSettingsSheetEvent = openSettingsSheetEvent_.receiveAsFlow()
-    private suspend fun sendSettingsSheetIntent(category: Category) = openSettingsSheetEvent_.send(category)
-    suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
+    private suspend fun requestOpenSettingsSheet() = requestSettingsSheetEvent.send(Unit)
 }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index e5536a764a..dc3e767208 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -78,8 +78,6 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
 import eu.kanade.tachiyomi.ui.home.HomeScreen
-import eu.kanade.tachiyomi.ui.library.LibrarySettingsSheet
-import eu.kanade.tachiyomi.ui.library.LibraryTab
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
 import eu.kanade.tachiyomi.util.system.dpToPx
@@ -87,7 +85,6 @@ import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
 import eu.kanade.tachiyomi.util.system.openInBrowser
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.setComposeContent
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.callbackFlow
@@ -100,7 +97,6 @@ import kotlinx.coroutines.launch
 import logcat.LogPriority
 import tachiyomi.core.Constants
 import tachiyomi.core.util.system.logcat
-import tachiyomi.domain.category.model.Category
 import tachiyomi.presentation.core.components.material.Scaffold
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -121,11 +117,6 @@ class MainActivity : BaseActivity() {
     // To be checked by splash screen. If true then splash screen will be removed.
     var ready = false
 
-    /**
-     * Sheet containing filter/sort/display items.
-     */
-    private var settingsSheet: LibrarySettingsSheet? = null
-
     private var navigator: Navigator? = null
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -160,11 +151,6 @@ class MainActivity : BaseActivity() {
         // Draw edge-to-edge
         WindowCompat.setDecorFitsSystemWindows(window, false)
 
-        settingsSheet = LibrarySettingsSheet(this)
-        LibraryTab.openSettingsSheetEvent
-            .onEach(::showSettingsSheet)
-            .launchIn(lifecycleScope)
-
         setComposeContent {
             val incognito by preferences.incognitoMode().collectAsState()
             val downloadOnly by preferences.downloadedOnly().collectAsState()
@@ -303,14 +289,6 @@ class MainActivity : BaseActivity() {
         }
     }
 
-    private fun showSettingsSheet(category: Category? = null) {
-        if (category != null) {
-            settingsSheet?.show(category)
-        } else {
-            lifecycleScope.launch { LibraryTab.requestOpenSettingsSheet() }
-        }
-    }
-
     @Composable
     private fun ConfirmExit() {
         val scope = rememberCoroutineScope()
@@ -470,12 +448,6 @@ class MainActivity : BaseActivity() {
         return true
     }
 
-    override fun onDestroy() {
-        settingsSheet?.sheetScope?.cancel()
-        settingsSheet = null
-        super.onDestroy()
-    }
-
     override fun onBackPressed() {
         if (navigator?.size == 1 &&
             !onBackPressedDispatcher.hasEnabledCallbacks() &&
diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
deleted file mode 100644
index d8cb3bf874..0000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-package eu.kanade.tachiyomi.widget
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.view.View.OnClickListener
-import android.view.ViewGroup
-import androidx.annotation.AttrRes
-import androidx.annotation.CallSuper
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.core.view.isVisible
-import androidx.core.view.updatePadding
-import androidx.recyclerview.widget.RecyclerView
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.util.system.getResourceColor
-
-/**
- * An alternative implementation of [com.google.android.material.navigation.NavigationView], without menu
- * inflation and allowing customizable items (multiple selections, custom views, etc).
- */
-open class ExtendedNavigationView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-) : SimpleNavigationView(context, attrs, defStyleAttr) {
-
-    /**
-     * Every item of the nav view. Generic items must belong to this list, custom items could be
-     * implemented by an abstract class. If more customization is needed in the future, this can be
-     * changed to an interface instead of sealed class.
-     */
-    sealed class Item {
-        /**
-         * A view separator.
-         */
-        class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
-
-        /**
-         * A header with a title.
-         */
-        class Header(val resTitle: Int) : Item()
-
-        /**
-         * A checkbox.
-         */
-        open class Checkbox(val resTitle: Int, var checked: Boolean = false, var enabled: Boolean = true) : Item()
-
-        /**
-         * A checkbox belonging to a group. The group must handle selections and restrictions.
-         */
-        class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false, enabled: Boolean = true) :
-            Checkbox(resTitle, checked, enabled), GroupedItem
-
-        /**
-         * A radio belonging to a group (a sole radio makes no sense). The group must handle
-         * selections and restrictions.
-         */
-        class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false, var enabled: Boolean = true) :
-            Item(), GroupedItem
-
-        /**
-         * An item with which needs more than two states (selected/deselected).
-         */
-        abstract class MultiState(val resTitle: Int, var state: Int = 0, var enabled: Boolean = true, var isVisible: Boolean = true) : Item() {
-
-            /**
-             * Returns the drawable associated to every possible each state.
-             */
-            abstract fun getStateDrawable(context: Context): Drawable?
-
-            /**
-             * Creates a vector tinted with the accent color.
-             *
-             * @param context any context.
-             * @param resId the vector resource to load and tint
-             */
-            fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
-                return AppCompatResources.getDrawable(context, resId)!!.apply {
-                    setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
-                }
-            }
-        }
-
-        /**
-         * An item with which needs more than two states (selected/deselected) belonging to a group.
-         * The group must handle selections and restrictions.
-         */
-        abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0, enabled: Boolean = true) :
-            MultiState(resTitle, state, enabled), GroupedItem
-
-        /**
-         * A multistate item for sorting lists (unselected, ascending, descending).
-         */
-        class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
-
-            companion object {
-                const val SORT_NONE = 0
-                const val SORT_ASC = 1
-                const val SORT_DESC = 2
-            }
-
-            override fun getStateDrawable(context: Context): Drawable? {
-                return when (state) {
-                    SORT_ASC -> tintVector(context, R.drawable.ic_arrow_up_white_32dp)
-                    SORT_DESC -> tintVector(context, R.drawable.ic_arrow_down_white_32dp)
-                    SORT_NONE -> AppCompatResources.getDrawable(context, R.drawable.empty_drawable_32dp)
-                    else -> null
-                }
-            }
-        }
-
-        /**
-         * A checkbox with 3 states (unselected, checked, explicitly unchecked).
-         */
-        class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
-
-            enum class State(val value: Int) {
-                DISABLED(0),
-                ENABLED_IS(1),
-                ENABLED_NOT(2),
-            }
-
-            override fun getStateDrawable(context: Context): Drawable? {
-                return when (state) {
-                    State.DISABLED.value -> tintVector(context, R.drawable.ic_check_box_outline_blank_24dp, R.attr.colorControlNormal)
-                    State.ENABLED_IS.value -> tintVector(context, R.drawable.ic_check_box_24dp)
-                    State.ENABLED_NOT.value -> tintVector(context, R.drawable.ic_check_box_x_24dp)
-                    else -> throw Exception("Unknown state")
-                }
-            }
-        }
-    }
-
-    /**
-     * Interface for an item belonging to a group.
-     */
-    interface GroupedItem {
-        val group: Group
-    }
-
-    /**
-     * A group containing a list of items.
-     */
-    interface Group {
-
-        /**
-         * An optional header for the group, typically a [Item.Header].
-         */
-        val header: Item?
-
-        /**
-         * An optional footer for the group, typically a [Item.Separator].
-         */
-        val footer: Item?
-
-        /**
-         * The items of the group, excluding header and footer.
-         */
-        val items: List<Item>
-
-        /**
-         * Creates all the elements of this group. Implementations can override this method for more
-         * customization.
-         */
-        fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
-
-        /**
-         * Called after creating the list of items. Implementations should load the current values
-         * into the models.
-         */
-        fun initModels()
-
-        /**
-         * Called when an item of this group is clicked. The group is responsible for all the
-         * selections of its items.
-         */
-        fun onItemClicked(item: Item)
-    }
-
-    /**
-     * Base adapter for the navigation view. It knows how to create and render every subclass of
-     * [Item].
-     */
-    abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
-
-        private val onClick = OnClickListener {
-            val pos = recycler.getChildAdapterPosition(it)
-            val item = items[pos]
-            onItemClicked(item)
-        }
-
-        fun notifyItemChanged(item: Item) {
-            val pos = items.indexOf(item)
-            if (pos != -1) notifyItemChanged(pos)
-        }
-
-        override fun getItemCount(): Int {
-            return items.size
-        }
-
-        @CallSuper
-        override fun getItemViewType(position: Int): Int {
-            return when (items[position]) {
-                is Item.Header -> VIEW_TYPE_HEADER
-                is Item.Separator -> VIEW_TYPE_SEPARATOR
-                is Item.Radio -> VIEW_TYPE_RADIO
-                is Item.Checkbox -> VIEW_TYPE_CHECKBOX
-                is Item.MultiState -> VIEW_TYPE_MULTISTATE
-            }
-        }
-
-        @CallSuper
-        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
-            return when (viewType) {
-                VIEW_TYPE_HEADER -> HeaderHolder(parent)
-                VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
-                VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
-                VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
-                VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
-                else -> throw Exception("Unknown view type")
-            }
-        }
-
-        @CallSuper
-        override fun onBindViewHolder(holder: Holder, position: Int) {
-            when (holder) {
-                is HeaderHolder -> {
-                    val item = items[position] as Item.Header
-                    holder.title.setText(item.resTitle)
-                }
-                is SeparatorHolder -> {
-                    val view = holder.itemView
-                    val item = items[position] as Item.Separator
-                    view.updatePadding(top = item.paddingTop, bottom = item.paddingBottom)
-                }
-                is RadioHolder -> {
-                    val item = items[position] as Item.Radio
-                    holder.radio.setText(item.resTitle)
-                    holder.radio.isChecked = item.checked
-
-                    holder.itemView.isClickable = item.enabled
-                    holder.radio.isEnabled = item.enabled
-                }
-                is CheckboxHolder -> {
-                    val item = items[position] as Item.CheckboxGroup
-                    holder.check.setText(item.resTitle)
-                    holder.check.isChecked = item.checked
-
-                    holder.itemView.isClickable = item.enabled
-                    holder.check.isEnabled = item.enabled
-                }
-                is MultiStateHolder -> {
-                    val item = items[position] as Item.MultiStateGroup
-                    val drawable = item.getStateDrawable(context)
-                    holder.text.setText(item.resTitle)
-                    holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
-
-                    holder.itemView.isClickable = item.enabled
-                    holder.text.isEnabled = item.enabled
-
-                    // Mimics checkbox/radio button
-                    holder.text.alpha = if (item.enabled) 1f else 0.4f
-                    holder.itemView.isVisible = item.isVisible
-                }
-            }
-        }
-
-        abstract fun onItemClicked(item: Item)
-    }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt
new file mode 100644
index 0000000000..0b25a1dbdf
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TriState.kt
@@ -0,0 +1,19 @@
+package eu.kanade.tachiyomi.widget
+
+import tachiyomi.domain.manga.model.TriStateFilter
+
+// TODO: replace this with TriStateFilter entirely
+enum class TriState(val value: Int) {
+    DISABLED(0),
+    ENABLED_IS(1),
+    ENABLED_NOT(2),
+}
+
+fun Int.toTriStateFilter(): TriStateFilter {
+    return when (this) {
+        TriState.DISABLED.value -> TriStateFilter.DISABLED
+        TriState.ENABLED_IS.value -> TriStateFilter.ENABLED_IS
+        TriState.ENABLED_NOT.value -> TriStateFilter.ENABLED_NOT
+        else -> throw IllegalStateException("Unknown TriStateGroup state: $this")
+    }
+}