Explorar el Código

Move SettingsItems composables to presentation-core

arkon hace 1 año
padre
commit
87bdee5990

+ 9 - 9
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt

@@ -7,9 +7,9 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
 import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
 import tachiyomi.core.metadata.comicinfo.ComicInfo
 import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
+import tachiyomi.core.preference.TriState
 import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.manga.model.TriStateFilter
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -20,19 +20,19 @@ val Manga.readingModeType: Long
 val Manga.orientationType: Long
     get() = viewerFlags and OrientationType.MASK.toLong()
 
-val Manga.downloadedFilter: TriStateFilter
+val Manga.downloadedFilter: TriState
     get() {
-        if (forceDownloaded()) return TriStateFilter.ENABLED_IS
+        if (forceDownloaded()) return TriState.ENABLED_IS
         return when (downloadedFilterRaw) {
-            Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
-            Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
-            else -> TriStateFilter.DISABLED
+            Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
+            Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
+            else -> TriState.DISABLED
         }
     }
 fun Manga.chaptersFiltered(): Boolean {
-    return unreadFilter != TriStateFilter.DISABLED ||
-        downloadedFilter != TriStateFilter.DISABLED ||
-        bookmarkedFilter != TriStateFilter.DISABLED
+    return unreadFilter != TriState.DISABLED ||
+        downloadedFilter != TriState.DISABLED ||
+        bookmarkedFilter != TriState.DISABLED
 }
 fun Manga.forceDownloaded(): Boolean {
     return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()

+ 0 - 128
app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt

@@ -1,128 +0,0 @@
-package eu.kanade.presentation.components
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.icons.Icons
-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.DropdownMenuItem
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import tachiyomi.domain.manga.model.TriStateFilter
-import tachiyomi.presentation.core.components.SettingsItemsPaddings
-
-@Composable
-fun TriStateItem(
-    label: String,
-    state: TriStateFilter,
-    enabled: Boolean = true,
-    onClick: ((TriStateFilter) -> Unit)?,
-) {
-    Row(
-        modifier = Modifier
-            .clickable(
-                enabled = enabled && onClick != null,
-                onClick = {
-                    when (state) {
-                        TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
-                        TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT)
-                        TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED)
-                    }
-                },
-            )
-            .fillMaxWidth()
-            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(24.dp),
-    ) {
-        val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
-
-        Icon(
-            imageVector = when (state) {
-                TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
-                TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox
-                TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
-            },
-            contentDescription = null,
-            tint = if (!enabled || state == TriStateFilter.DISABLED) {
-                MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
-            } else {
-                when (onClick) {
-                    null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled)
-                    else -> MaterialTheme.colorScheme.primary
-                }
-            },
-        )
-        Text(
-            text = label,
-            color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha),
-            style = MaterialTheme.typography.bodyMedium,
-        )
-    }
-}
-
-@Composable
-fun SelectItem(
-    label: String,
-    options: Array<out Any?>,
-    selectedIndex: Int,
-    onSelect: (Int) -> Unit,
-) {
-    var expanded by remember { mutableStateOf(false) }
-
-    ExposedDropdownMenuBox(
-        expanded = expanded,
-        onExpandedChange = { expanded = !expanded },
-    ) {
-        OutlinedTextField(
-            modifier = Modifier
-                .menuAnchor()
-                .fillMaxWidth()
-                .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
-            label = { Text(text = label) },
-            value = options[selectedIndex].toString(),
-            onValueChange = {},
-            readOnly = true,
-            singleLine = true,
-            trailingIcon = {
-                ExposedDropdownMenuDefaults.TrailingIcon(
-                    expanded = expanded,
-                )
-            },
-            colors = ExposedDropdownMenuDefaults.textFieldColors(),
-        )
-
-        ExposedDropdownMenu(
-            modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
-            expanded = expanded,
-            onDismissRequest = { expanded = false },
-        ) {
-            options.forEachIndexed { index, text ->
-                DropdownMenuItem(
-                    text = { Text(text.toString()) },
-                    onClick = {
-                        onSelect(index)
-                        expanded = false
-                    },
-                )
-            }
-        }
-    }
-}

+ 3 - 3
app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt

@@ -14,21 +14,21 @@ import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.res.stringResource
 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 tachiyomi.core.preference.TriState
 import tachiyomi.domain.category.model.Category
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.library.model.LibrarySort
 import tachiyomi.domain.library.model.sort
 import tachiyomi.domain.library.service.LibraryPreferences
-import tachiyomi.domain.manga.model.TriStateFilter
 import tachiyomi.presentation.core.components.CheckboxItem
 import tachiyomi.presentation.core.components.HeadingItem
 import tachiyomi.presentation.core.components.RadioItem
 import tachiyomi.presentation.core.components.SliderItem
 import tachiyomi.presentation.core.components.SortItem
+import tachiyomi.presentation.core.components.TriStateItem
 
 @Composable
 fun LibrarySettingsDialog(
@@ -74,7 +74,7 @@ private fun ColumnScope.FilterPage(
     TriStateItem(
         label = stringResource(R.string.label_downloaded),
         state = if (downloadedOnly) {
-            TriStateFilter.ENABLED_IS
+            TriState.ENABLED_IS
         } else {
             filterDownloaded
         },

+ 14 - 14
app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt

@@ -27,20 +27,20 @@ import eu.kanade.domain.manga.model.downloadedFilter
 import eu.kanade.domain.manga.model.forceDownloaded
 import eu.kanade.presentation.components.TabbedDialog
 import eu.kanade.presentation.components.TabbedDialogPaddings
-import eu.kanade.presentation.components.TriStateItem
 import eu.kanade.tachiyomi.R
+import tachiyomi.core.preference.TriState
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.manga.model.TriStateFilter
 import tachiyomi.presentation.core.components.RadioItem
 import tachiyomi.presentation.core.components.SortItem
+import tachiyomi.presentation.core.components.TriStateItem
 
 @Composable
 fun ChapterSettingsDialog(
     onDismissRequest: () -> Unit,
     manga: Manga? = null,
-    onDownloadFilterChanged: (TriStateFilter) -> Unit,
-    onUnreadFilterChanged: (TriStateFilter) -> Unit,
-    onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
+    onDownloadFilterChanged: (TriState) -> Unit,
+    onUnreadFilterChanged: (TriState) -> Unit,
+    onBookmarkedFilterChanged: (TriState) -> Unit,
     onSortModeChanged: (Long) -> Unit,
     onDisplayModeChanged: (Long) -> Unit,
     onSetAsDefault: (applyToExistingManga: Boolean) -> Unit,
@@ -78,11 +78,11 @@ fun ChapterSettingsDialog(
             when (page) {
                 0 -> {
                     FilterPage(
-                        downloadFilter = manga?.downloadedFilter ?: TriStateFilter.DISABLED,
+                        downloadFilter = manga?.downloadedFilter ?: TriState.DISABLED,
                         onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { manga?.forceDownloaded() == true },
-                        unreadFilter = manga?.unreadFilter ?: TriStateFilter.DISABLED,
+                        unreadFilter = manga?.unreadFilter ?: TriState.DISABLED,
                         onUnreadFilterChanged = onUnreadFilterChanged,
-                        bookmarkedFilter = manga?.bookmarkedFilter ?: TriStateFilter.DISABLED,
+                        bookmarkedFilter = manga?.bookmarkedFilter ?: TriState.DISABLED,
                         onBookmarkedFilterChanged = onBookmarkedFilterChanged,
                     )
                 }
@@ -106,12 +106,12 @@ fun ChapterSettingsDialog(
 
 @Composable
 private fun ColumnScope.FilterPage(
-    downloadFilter: TriStateFilter,
-    onDownloadFilterChanged: ((TriStateFilter) -> Unit)?,
-    unreadFilter: TriStateFilter,
-    onUnreadFilterChanged: (TriStateFilter) -> Unit,
-    bookmarkedFilter: TriStateFilter,
-    onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
+    downloadFilter: TriState,
+    onDownloadFilterChanged: ((TriState) -> Unit)?,
+    unreadFilter: TriState,
+    onUnreadFilterChanged: (TriState) -> Unit,
+    bookmarkedFilter: TriState,
+    onBookmarkedFilterChanged: (TriState) -> Unit,
 ) {
     TriStateItem(
         label = stringResource(R.string.label_downloaded),

+ 5 - 6
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt

@@ -21,12 +21,11 @@ import eu.kanade.tachiyomi.util.system.isReleaseBuildType
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.system.workManager
 import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.preference.TriState
 import tachiyomi.core.preference.getEnum
 import tachiyomi.domain.backup.service.BackupPreferences
 import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
-import tachiyomi.domain.manga.model.TriStateFilter
-import uy.kohesive.injekt.api.get
 import java.io.File
 
 object Migrations {
@@ -350,12 +349,12 @@ object Migrations {
                         remove(key)
 
                         val newValue = when (pref.get()) {
-                            1 -> TriStateFilter.ENABLED_IS
-                            2 -> TriStateFilter.ENABLED_NOT
-                            else -> TriStateFilter.DISABLED
+                            1 -> TriState.ENABLED_IS
+                            2 -> TriState.ENABLED_NOT
+                            else -> TriState.DISABLED
                         }
 
-                        preferenceStore.getEnum("${key}_v2", TriStateFilter.DISABLED).set(newValue)
+                        preferenceStore.getEnum("${key}_v2", TriState.DISABLED).set(newValue)
                     }
                 }
             }

+ 11 - 11
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt

@@ -16,17 +16,17 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import eu.kanade.presentation.components.AdaptiveSheet
-import eu.kanade.presentation.components.SelectItem
-import eu.kanade.presentation.components.TriStateItem
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.model.Filter
 import eu.kanade.tachiyomi.source.model.FilterList
-import tachiyomi.domain.manga.model.TriStateFilter
+import tachiyomi.core.preference.TriState
 import tachiyomi.presentation.core.components.CheckboxItem
 import tachiyomi.presentation.core.components.CollapsibleBox
 import tachiyomi.presentation.core.components.HeadingItem
+import tachiyomi.presentation.core.components.SelectItem
 import tachiyomi.presentation.core.components.SortItem
 import tachiyomi.presentation.core.components.TextItem
+import tachiyomi.presentation.core.components.TriStateItem
 import tachiyomi.presentation.core.components.material.Button
 import tachiyomi.presentation.core.components.material.Divider
 
@@ -164,19 +164,19 @@ private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) {
     }
 }
 
-private fun Int.toTriStateFilter(): TriStateFilter {
+private fun Int.toTriStateFilter(): TriState {
     return when (this) {
-        Filter.TriState.STATE_IGNORE -> TriStateFilter.DISABLED
-        Filter.TriState.STATE_INCLUDE -> TriStateFilter.ENABLED_IS
-        Filter.TriState.STATE_EXCLUDE -> TriStateFilter.ENABLED_NOT
+        Filter.TriState.STATE_IGNORE -> TriState.DISABLED
+        Filter.TriState.STATE_INCLUDE -> TriState.ENABLED_IS
+        Filter.TriState.STATE_EXCLUDE -> TriState.ENABLED_NOT
         else -> throw IllegalStateException("Unknown TriState state: $this")
     }
 }
 
-private fun TriStateFilter.toTriStateInt(): Int {
+private fun TriState.toTriStateInt(): Int {
     return when (this) {
-        TriStateFilter.DISABLED -> Filter.TriState.STATE_IGNORE
-        TriStateFilter.ENABLED_IS -> Filter.TriState.STATE_INCLUDE
-        TriStateFilter.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE
+        TriState.DISABLED -> Filter.TriState.STATE_IGNORE
+        TriState.ENABLED_IS -> Filter.TriState.STATE_INCLUDE
+        TriState.ENABLED_NOT -> Filter.TriState.STATE_EXCLUDE
     }
 }

+ 17 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt

@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.update
 import tachiyomi.core.preference.CheckboxState
+import tachiyomi.core.preference.TriState
 import tachiyomi.core.util.lang.launchIO
 import tachiyomi.core.util.lang.launchNonCancellable
 import tachiyomi.core.util.lang.withIOContext
@@ -57,7 +58,6 @@ import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.manga.interactor.GetLibraryManga
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.MangaUpdate
-import tachiyomi.domain.manga.model.TriStateFilter
 import tachiyomi.domain.manga.model.applyFilter
 import tachiyomi.domain.source.service.SourceManager
 import tachiyomi.domain.track.interactor.GetTracksPerManga
@@ -153,7 +153,7 @@ class LibraryScreenModel(
                     prefs.filterBookmarked,
                     prefs.filterCompleted,
                 ) + trackFilter.values
-                ).any { it != TriStateFilter.DISABLED }
+                ).any { it != TriState.DISABLED }
         }
             .distinctUntilChanged()
             .onEach {
@@ -169,12 +169,12 @@ class LibraryScreenModel(
      */
     private suspend fun LibraryMap.applyFilters(
         trackMap: Map<Long, List<Long>>,
-        loggedInTrackServices: Map<Long, TriStateFilter>,
+        loggedInTrackServices: Map<Long, TriState>,
     ): LibraryMap {
         val prefs = getLibraryItemPreferencesFlow().first()
         val downloadedOnly = prefs.globalFilterDownloaded
         val filterDownloaded =
-            if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded
+            if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
         val filterUnread = prefs.filterUnread
         val filterStarted = prefs.filterStarted
         val filterBookmarked = prefs.filterBookmarked
@@ -182,8 +182,8 @@ class LibraryScreenModel(
 
         val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
 
-        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null }
-        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null }
+        val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
+        val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
         val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
 
         val filterFnDownloaded: (LibraryItem) -> Boolean = {
@@ -308,11 +308,11 @@ class LibraryScreenModel(
                     localBadge = it[1] as Boolean,
                     languageBadge = it[2] as Boolean,
                     globalFilterDownloaded = it[3] as Boolean,
-                    filterDownloaded = it[4] as TriStateFilter,
-                    filterUnread = it[5] as TriStateFilter,
-                    filterStarted = it[6] as TriStateFilter,
-                    filterBookmarked = it[7] as TriStateFilter,
-                    filterCompleted = it[8] as TriStateFilter,
+                    filterDownloaded = it[4] as TriState,
+                    filterUnread = it[5] as TriState,
+                    filterStarted = it[6] as TriState,
+                    filterBookmarked = it[7] as TriState,
+                    filterCompleted = it[8] as TriState,
                 )
             },
         )
@@ -365,7 +365,7 @@ class LibraryScreenModel(
      *
      * @return map of track id with the filter value
      */
-    private fun getTrackingFilterFlow(): Flow<Map<Long, TriStateFilter>> {
+    private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
         val loggedServices = trackManager.services.filter { it.isLogged }
         return if (loggedServices.isNotEmpty()) {
             val prefFlows = loggedServices
@@ -670,11 +670,11 @@ class LibraryScreenModel(
         val languageBadge: Boolean,
 
         val globalFilterDownloaded: Boolean,
-        val filterDownloaded: TriStateFilter,
-        val filterUnread: TriStateFilter,
-        val filterStarted: TriStateFilter,
-        val filterBookmarked: TriStateFilter,
-        val filterCompleted: TriStateFilter,
+        val filterDownloaded: TriState,
+        val filterUnread: TriState,
+        val filterStarted: TriState,
+        val filterBookmarked: TriState,
+        val filterCompleted: TriState,
     )
 
     @Immutable

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt

@@ -6,6 +6,7 @@ import eu.kanade.domain.base.BasePreferences
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.util.preference.toggle
 import tachiyomi.core.preference.Preference
+import tachiyomi.core.preference.TriState
 import tachiyomi.core.preference.getAndSet
 import tachiyomi.core.util.lang.launchIO
 import tachiyomi.domain.category.interactor.SetDisplayMode
@@ -14,7 +15,6 @@ import tachiyomi.domain.category.model.Category
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.library.model.LibrarySort
 import tachiyomi.domain.library.service.LibraryPreferences
-import tachiyomi.domain.manga.model.TriStateFilter
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -33,7 +33,7 @@ class LibrarySettingsScreenModel(
         preference(libraryPreferences).toggle()
     }
 
-    fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriStateFilter>) {
+    fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) {
         preference(libraryPreferences).getAndSet {
             it.next()
         }

+ 13 - 13
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt

@@ -46,6 +46,7 @@ import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import logcat.LogPriority
 import tachiyomi.core.preference.CheckboxState
+import tachiyomi.core.preference.TriState
 import tachiyomi.core.preference.mapAsCheckboxState
 import tachiyomi.core.util.lang.launchIO
 import tachiyomi.core.util.lang.launchNonCancellable
@@ -67,7 +68,6 @@ import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
 import tachiyomi.domain.manga.interactor.GetMangaWithChapters
 import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.manga.model.TriStateFilter
 import tachiyomi.domain.manga.model.applyFilter
 import tachiyomi.domain.source.service.SourceManager
 import tachiyomi.domain.track.interactor.GetTracks
@@ -743,13 +743,13 @@ class MangaInfoScreenModel(
      * Sets the read filter and requests an UI update.
      * @param state whether to display only unread chapters or all chapters.
      */
-    fun setUnreadFilter(state: TriStateFilter) {
+    fun setUnreadFilter(state: TriState) {
         val manga = successState?.manga ?: return
 
         val flag = when (state) {
-            TriStateFilter.DISABLED -> Manga.SHOW_ALL
-            TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD
-            TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ
+            TriState.DISABLED -> Manga.SHOW_ALL
+            TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD
+            TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ
         }
         coroutineScope.launchNonCancellable {
             setMangaChapterFlags.awaitSetUnreadFilter(manga, flag)
@@ -760,13 +760,13 @@ class MangaInfoScreenModel(
      * Sets the download filter and requests an UI update.
      * @param state whether to display only downloaded chapters or all chapters.
      */
-    fun setDownloadedFilter(state: TriStateFilter) {
+    fun setDownloadedFilter(state: TriState) {
         val manga = successState?.manga ?: return
 
         val flag = when (state) {
-            TriStateFilter.DISABLED -> Manga.SHOW_ALL
-            TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED
-            TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED
+            TriState.DISABLED -> Manga.SHOW_ALL
+            TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED
+            TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED
         }
 
         coroutineScope.launchNonCancellable {
@@ -778,13 +778,13 @@ class MangaInfoScreenModel(
      * Sets the bookmark filter and requests an UI update.
      * @param state whether to display only bookmarked chapters or all chapters.
      */
-    fun setBookmarkedFilter(state: TriStateFilter) {
+    fun setBookmarkedFilter(state: TriState) {
         val manga = successState?.manga ?: return
 
         val flag = when (state) {
-            TriStateFilter.DISABLED -> Manga.SHOW_ALL
-            TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED
-            TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED
+            TriState.DISABLED -> Manga.SHOW_ALL
+            TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED
+            TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED
         }
 
         coroutineScope.launchNonCancellable {

+ 16 - 0
core/src/main/java/tachiyomi/core/preference/TriState.kt

@@ -0,0 +1,16 @@
+package tachiyomi.core.preference
+
+enum class TriState {
+    DISABLED, // Disable filter
+    ENABLED_IS, // Enabled with "is" filter
+    ENABLED_NOT, // Enabled with "not" filter
+    ;
+
+    fun next(): TriState {
+        return when (this) {
+            DISABLED -> ENABLED_IS
+            ENABLED_IS -> ENABLED_NOT
+            ENABLED_NOT -> DISABLED
+        }
+    }
+}

+ 12 - 12
domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt

@@ -1,11 +1,11 @@
 package tachiyomi.domain.library.service
 
 import tachiyomi.core.preference.PreferenceStore
+import tachiyomi.core.preference.TriState
 import tachiyomi.core.preference.getEnum
 import tachiyomi.domain.library.model.LibraryDisplayMode
 import tachiyomi.domain.library.model.LibrarySort
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.manga.model.TriStateFilter
 
 class LibraryPreferences(
     private val preferenceStore: PreferenceStore,
@@ -49,27 +49,27 @@ class LibraryPreferences(
 
     // region Filter
 
-    fun filterDownloaded() = preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriStateFilter.DISABLED)
+    fun filterDownloaded() = preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED)
 
-    fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriStateFilter.DISABLED)
+    fun filterUnread() = preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED)
 
-    fun filterStarted() = preferenceStore.getEnum("pref_filter_library_started_v2", TriStateFilter.DISABLED)
+    fun filterStarted() = preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED)
 
-    fun filterBookmarked() = preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriStateFilter.DISABLED)
+    fun filterBookmarked() = preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED)
 
-    fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED)
+    fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED)
 
-    fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriStateFilter.DISABLED)
+    fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriState.DISABLED)
 
-    fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriStateFilter.DISABLED)
+    fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriState.DISABLED)
 
-    fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriStateFilter.DISABLED)
+    fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriState.DISABLED)
 
-    fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriStateFilter.DISABLED)
+    fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriState.DISABLED)
 
-    fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriStateFilter.DISABLED)
+    fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriState.DISABLED)
 
-    fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriStateFilter.DISABLED)
+    fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED)
 
     // endregion
 

+ 9 - 8
domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt

@@ -1,6 +1,7 @@
 package tachiyomi.domain.manga.model
 
 import eu.kanade.tachiyomi.source.model.UpdateStrategy
+import tachiyomi.core.preference.TriState
 import java.io.Serializable
 
 data class Manga(
@@ -43,18 +44,18 @@ data class Manga(
     val bookmarkedFilterRaw: Long
         get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
 
-    val unreadFilter: TriStateFilter
+    val unreadFilter: TriState
         get() = when (unreadFilterRaw) {
-            CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
-            CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
-            else -> TriStateFilter.DISABLED
+            CHAPTER_SHOW_UNREAD -> TriState.ENABLED_IS
+            CHAPTER_SHOW_READ -> TriState.ENABLED_NOT
+            else -> TriState.DISABLED
         }
 
-    val bookmarkedFilter: TriStateFilter
+    val bookmarkedFilter: TriState
         get() = when (bookmarkedFilterRaw) {
-            CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
-            CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
-            else -> TriStateFilter.DISABLED
+            CHAPTER_SHOW_BOOKMARKED -> TriState.ENABLED_IS
+            CHAPTER_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT
+            else -> TriState.DISABLED
         }
 
     fun sortDescending(): Boolean {

+ 9 - 0
domain/src/main/java/tachiyomi/domain/manga/model/TriState.kt

@@ -0,0 +1,9 @@
+package tachiyomi.domain.manga.model
+
+import tachiyomi.core.preference.TriState
+
+inline fun applyFilter(filter: TriState, predicate: () -> Boolean): Boolean = when (filter) {
+    TriState.DISABLED -> true
+    TriState.ENABLED_IS -> predicate()
+    TriState.ENABLED_NOT -> !predicate()
+}

+ 0 - 22
domain/src/main/java/tachiyomi/domain/manga/model/TriStateFilter.kt

@@ -1,22 +0,0 @@
-package tachiyomi.domain.manga.model
-
-enum class TriStateFilter {
-    DISABLED, // Disable filter
-    ENABLED_IS, // Enabled with "is" filter
-    ENABLED_NOT, // Enabled with "not" filter
-    ;
-
-    fun next(): TriStateFilter {
-        return when (this) {
-            DISABLED -> ENABLED_IS
-            ENABLED_IS -> ENABLED_NOT
-            ENABLED_NOT -> DISABLED
-        }
-    }
-}
-
-inline fun applyFilter(filter: TriStateFilter, predicate: () -> Boolean): Boolean = when (filter) {
-    TriStateFilter.DISABLED -> true
-    TriStateFilter.ENABLED_IS -> predicate()
-    TriStateFilter.ENABLED_NOT -> !predicate()
-}

+ 2 - 0
presentation-core/build.gradle.kts

@@ -21,6 +21,8 @@ android {
 }
 
 dependencies {
+    implementation(project(":core"))
+
     // Compose
     implementation(platform(compose.bom))
     implementation(compose.activity)

+ 116 - 5
presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt

@@ -10,10 +10,17 @@ import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.material.ContentAlpha
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowDownward
 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.DropdownMenuItem
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.OutlinedTextField
@@ -21,11 +28,16 @@ import androidx.compose.material3.RadioButton
 import androidx.compose.material3.Slider
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import tachiyomi.core.preference.TriState
 import tachiyomi.presentation.core.theme.header
 
 object SettingsItemsPaddings {
@@ -175,22 +187,99 @@ fun SliderItem(
 }
 
 @Composable
-private fun BaseSettingsItem(
+fun SelectItem(
     label: String,
-    widget: @Composable RowScope.() -> Unit,
-    onClick: () -> Unit,
+    options: Array<out Any?>,
+    selectedIndex: Int,
+    onSelect: (Int) -> Unit,
+) {
+    var expanded by remember { mutableStateOf(false) }
+
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = !expanded },
+    ) {
+        OutlinedTextField(
+            modifier = Modifier
+                .menuAnchor()
+                .fillMaxWidth()
+                .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+            label = { Text(text = label) },
+            value = options[selectedIndex].toString(),
+            onValueChange = {},
+            readOnly = true,
+            singleLine = true,
+            trailingIcon = {
+                ExposedDropdownMenuDefaults.TrailingIcon(
+                    expanded = expanded,
+                )
+            },
+            colors = ExposedDropdownMenuDefaults.textFieldColors(),
+        )
+
+        ExposedDropdownMenu(
+            modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+        ) {
+            options.forEachIndexed { index, text ->
+                DropdownMenuItem(
+                    text = { Text(text.toString()) },
+                    onClick = {
+                        onSelect(index)
+                        expanded = false
+                    },
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun TriStateItem(
+    label: String,
+    state: TriState,
+    enabled: Boolean = true,
+    onClick: ((TriState) -> Unit)?,
 ) {
     Row(
         modifier = Modifier
-            .clickable(onClick = onClick)
+            .clickable(
+                enabled = enabled && onClick != null,
+                onClick = {
+                    when (state) {
+                        TriState.DISABLED -> onClick?.invoke(TriState.ENABLED_IS)
+                        TriState.ENABLED_IS -> onClick?.invoke(TriState.ENABLED_NOT)
+                        TriState.ENABLED_NOT -> onClick?.invoke(TriState.DISABLED)
+                    }
+                },
+            )
             .fillMaxWidth()
             .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(24.dp),
     ) {
-        widget(this)
+        val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
+
+        Icon(
+            imageVector = when (state) {
+                TriState.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
+                TriState.ENABLED_IS -> Icons.Rounded.CheckBox
+                TriState.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
+            },
+            contentDescription = null,
+            tint = if (!enabled || state == TriState.DISABLED) {
+                MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
+            } else {
+                when (onClick) {
+                    null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled)
+                    else -> MaterialTheme.colorScheme.primary
+                }
+            },
+        )
         Text(
             text = label,
+            color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha),
             style = MaterialTheme.typography.bodyMedium,
         )
     }
@@ -212,3 +301,25 @@ fun TextItem(
         singleLine = true,
     )
 }
+
+@Composable
+private fun BaseSettingsItem(
+    label: String,
+    widget: @Composable RowScope.() -> Unit,
+    onClick: () -> Unit,
+) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(24.dp),
+    ) {
+        widget(this)
+        Text(
+            text = label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}