Browse Source

Migrate source filter sheet to Compose (#9135)

arkon 2 years ago
parent
commit
92132c59f5
36 changed files with 458 additions and 1304 deletions
  1. 49 106
      app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt
  2. 4 4
      app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
  3. 2 2
      app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt
  4. 0 7
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
  5. 16 7
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
  6. 13 76
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
  7. 165 0
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt
  8. 0 63
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
  9. 0 46
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/CheckboxItem.kt
  10. 0 66
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt
  11. 0 41
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HeaderItem.kt
  12. 0 100
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt
  13. 0 59
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SelectItem.kt
  14. 0 38
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SeparatorItem.kt
  15. 0 56
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt
  16. 0 75
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortItem.kt
  17. 0 47
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TextItem.kt
  18. 0 80
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TriStateItem.kt
  19. 0 130
      app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt
  20. 0 21
      app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt
  21. 0 7
      app/src/main/res/drawable/empty_drawable_32dp.xml
  22. 0 15
      app/src/main/res/drawable/ic_arrow_down_white_32dp.xml
  23. 0 15
      app/src/main/res/drawable/ic_arrow_up_white_32dp.xml
  24. 0 9
      app/src/main/res/drawable/ic_check_box_24dp.xml
  25. 0 9
      app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml
  26. 0 9
      app/src/main/res/drawable/ic_check_box_x_24dp.xml
  27. 0 9
      app/src/main/res/drawable/ic_expand_less_24dp.xml
  28. 0 24
      app/src/main/res/layout/navigation_view_checkbox.xml
  29. 0 22
      app/src/main/res/layout/navigation_view_checkedtext.xml
  30. 0 29
      app/src/main/res/layout/navigation_view_group.xml
  31. 0 22
      app/src/main/res/layout/navigation_view_radio.xml
  32. 0 33
      app/src/main/res/layout/navigation_view_spinner.xml
  33. 0 30
      app/src/main/res/layout/navigation_view_text.xml
  34. 0 47
      app/src/main/res/layout/source_filter_sheet.xml
  35. 56 0
      presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt
  36. 153 0
      presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt

+ 49 - 106
app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt

@@ -1,52 +1,32 @@
 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
-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.RadioButton
+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.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,
-) {
-    HeadingItem(stringResource(labelRes))
-}
-
-@Composable
-fun HeadingItem(
-    text: String,
-) {
-    Text(
-        text = text,
-        style = MaterialTheme.typography.header,
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
-    )
-}
+import tachiyomi.presentation.core.components.SettingsItemsPaddings
 
 @Composable
 fun TriStateItem(
@@ -68,7 +48,7 @@ fun TriStateItem(
                 },
             )
             .fillMaxWidth()
-            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(24.dp),
     ) {
@@ -99,87 +79,50 @@ fun TriStateItem(
 }
 
 @Composable
-fun SortItem(
+fun SelectItem(
     label: String,
-    sortDescending: Boolean?,
-    onClick: () -> Unit,
+    options: Array<out Any?>,
+    selectedIndex: Int,
+    onSelect: (Int) -> Unit,
 ) {
-    val arrowIcon = when (sortDescending) {
-        true -> Icons.Default.ArrowDownward
-        false -> Icons.Default.ArrowUpward
-        null -> null
-    }
-
-    Row(
-        modifier = Modifier
-            .clickable(onClick = onClick)
-            .fillMaxWidth()
-            .padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
-        verticalAlignment = Alignment.CenterVertically,
-        horizontalArrangement = Arrangement.spacedBy(24.dp),
-    ) {
-        if (arrowIcon != null) {
-            Icon(
-                imageVector = arrowIcon,
-                contentDescription = null,
-                tint = MaterialTheme.colorScheme.primary,
-            )
-        } else {
-            Spacer(modifier = Modifier.size(24.dp))
-        }
-        Text(
-            text = label,
-            style = MaterialTheme.typography.bodyMedium,
-        )
-    }
-}
+    var expanded by remember { mutableStateOf(false) }
 
-@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),
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = !expanded },
     ) {
-        Checkbox(
-            checked = checked,
-            onCheckedChange = null,
-        )
-        Text(
-            text = label,
-            style = MaterialTheme.typography.bodyMedium,
+        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(),
         )
-    }
-}
 
-@Composable
-fun RadioItem(
-    label: String,
-    selected: 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),
-    ) {
-        RadioButton(
-            selected = selected,
-            onClick = null,
-        )
-        Text(
-            text = label,
-            style = MaterialTheme.typography.bodyMedium,
-        )
+        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
+                    },
+                )
+            }
+        }
     }
 }

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

@@ -10,10 +10,6 @@ import androidx.compose.runtime.getValue
 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
@@ -27,6 +23,10 @@ import tachiyomi.domain.library.model.LibrarySort
 import tachiyomi.domain.library.model.display
 import tachiyomi.domain.library.model.sort
 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.SortItem
 
 @Composable
 fun LibrarySettingsDialog(

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

@@ -25,14 +25,14 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import eu.kanade.domain.manga.model.downloadedFilter
 import eu.kanade.domain.manga.model.forceDownloaded
-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.tachiyomi.R
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.TriStateFilter
+import tachiyomi.presentation.core.components.RadioItem
+import tachiyomi.presentation.core.components.SortItem
 
 @Composable
 fun ChapterSettingsDialog(

+ 0 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt

@@ -8,13 +8,11 @@ import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.res.stringResource
 import androidx.paging.compose.collectAsLazyPagingItems
@@ -45,7 +43,6 @@ data class SourceSearchScreen(
 
     @Composable
     override fun Content() {
-        val context = LocalContext.current
         val uriHandler = LocalUriHandler.current
         val navigator = LocalNavigator.currentOrThrow
         val scope = rememberCoroutineScope()
@@ -123,9 +120,5 @@ data class SourceSearchScreen(
             }
             else -> {}
         }
-
-        LaunchedEffect(state.filters) {
-            screenModel.initFilterSheet(context)
-        }
     }
 }

+ 16 - 7
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt

@@ -28,7 +28,6 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalHapticFeedback
 import androidx.compose.ui.platform.LocalUriHandler
 import androidx.compose.ui.res.stringResource
@@ -93,7 +92,6 @@ data class BrowseSourceScreen(
         }
 
         val scope = rememberCoroutineScope()
-        val context = LocalContext.current
         val haptic = LocalHapticFeedback.current
         val uriHandler = LocalUriHandler.current
         val snackbarHostState = remember { SnackbarHostState() }
@@ -231,7 +229,21 @@ data class BrowseSourceScreen(
 
         val onDismissRequest = { screenModel.setDialog(null) }
         when (val dialog = state.dialog) {
-            is BrowseSourceScreenModel.Dialog.Migrate -> {}
+            is BrowseSourceScreenModel.Dialog.Filter -> {
+                SourceFilterDialog(
+                    onDismissRequest = onDismissRequest,
+                    filters = state.filters,
+                    onReset = {
+                        screenModel.resetFilters()
+                    },
+                    onFilter = {
+                        screenModel.search(filters = state.filters)
+                    },
+                    onUpdate = {
+                        screenModel.setFilters(it)
+                    },
+                )
+            }
             is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
                 DuplicateMangaDialog(
                     onDismissRequest = onDismissRequest,
@@ -259,13 +271,10 @@ data class BrowseSourceScreen(
                     },
                 )
             }
+            is BrowseSourceScreenModel.Dialog.Migrate -> {}
             else -> {}
         }
 
-        LaunchedEffect(state.filters) {
-            screenModel.initFilterSheet(context)
-        }
-
         LaunchedEffect(Unit) {
             queryEvent.receiveAsFlow()
                 .collectLatest {

+ 13 - 76
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt

@@ -1,6 +1,5 @@
 package eu.kanade.tachiyomi.ui.browse.source.browse
 
-import android.content.Context
 import android.content.res.Configuration
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.runtime.Immutable
@@ -14,7 +13,6 @@ import androidx.paging.filter
 import androidx.paging.map
 import cafe.adriel.voyager.core.model.StateScreenModel
 import cafe.adriel.voyager.core.model.coroutineScope
-import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.core.prefs.asState
 import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
 import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
@@ -33,19 +31,6 @@ import eu.kanade.tachiyomi.data.track.TrackService
 import eu.kanade.tachiyomi.source.CatalogueSource
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.model.FilterList
-import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.HeaderItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.SelectItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.SelectSectionItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.SeparatorItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.SortGroup
-import eu.kanade.tachiyomi.ui.browse.source.filter.SortItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.TextItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
-import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
 import eu.kanade.tachiyomi.util.removeCovers
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -125,11 +110,6 @@ class BrowseSourceScreenModel(
         }
     }
 
-    /**
-     * Sheet containing filter items.
-     */
-    private var filterSheet: SourceFilterSheet? = null
-
     /**
      * Flow of Pager flow tied to [State.listing]
      */
@@ -175,6 +155,16 @@ class BrowseSourceScreenModel(
         mutableState.update { it.copy(listing = listing) }
     }
 
+    fun setFilters(filters: FilterList) {
+        if (source !is CatalogueSource) return
+
+        mutableState.update {
+            it.copy(
+                filters = filters,
+            )
+        }
+    }
+
     fun search(query: String? = null, filters: FilterList? = null) {
         if (source !is CatalogueSource) return
 
@@ -350,7 +340,7 @@ class BrowseSourceScreenModel(
         return getDuplicateLibraryManga.await(manga.title)
     }
 
-    fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
+    private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
         moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
     }
 
@@ -364,7 +354,7 @@ class BrowseSourceScreenModel(
     }
 
     fun openFilterSheet() {
-        filterSheet?.show()
+        setDialog(Dialog.Filter)
     }
 
     fun setDialog(dialog: Dialog?) {
@@ -375,23 +365,6 @@ class BrowseSourceScreenModel(
         mutableState.update { it.copy(toolbarQuery = query) }
     }
 
-    fun initFilterSheet(context: Context) {
-        if (state.value.filters.isEmpty()) {
-            return
-        }
-
-        filterSheet = SourceFilterSheet(
-            context = context,
-            onFilterClicked = { search(filters = state.value.filters) },
-            onResetClicked = {
-                resetFilters()
-                filterSheet?.setFilters(state.value.filterItems)
-            },
-        )
-
-        filterSheet?.setFilters(state.value.filterItems)
-    }
-
     sealed class Listing(open val query: String?, open val filters: FilterList) {
         object Popular : Listing(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList())
         object Latest : Listing(query = GetRemoteManga.QUERY_LATEST, filters = FilterList())
@@ -409,6 +382,7 @@ class BrowseSourceScreenModel(
     }
 
     sealed class Dialog {
+        object Filter : Dialog()
         data class RemoveManga(val manga: Manga) : Dialog()
         data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog()
         data class ChangeMangaCategory(
@@ -425,43 +399,6 @@ class BrowseSourceScreenModel(
         val toolbarQuery: String? = null,
         val dialog: Dialog? = null,
     ) {
-        val filterItems get() = filters.toItems()
         val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
     }
 }
-
-private fun FilterList.toItems(): List<IFlexible<*>> {
-    return mapNotNull { filter ->
-        when (filter) {
-            is SourceModelFilter.Header -> HeaderItem(filter)
-            is SourceModelFilter.Separator -> SeparatorItem(filter)
-            is SourceModelFilter.CheckBox -> CheckboxItem(filter)
-            is SourceModelFilter.TriState -> TriStateItem(filter)
-            is SourceModelFilter.Text -> TextItem(filter)
-            is SourceModelFilter.Select<*> -> SelectItem(filter)
-            is SourceModelFilter.Group<*> -> {
-                val group = GroupItem(filter)
-                val subItems = filter.state.mapNotNull {
-                    when (it) {
-                        is SourceModelFilter.CheckBox -> CheckboxSectionItem(it)
-                        is SourceModelFilter.TriState -> TriStateSectionItem(it)
-                        is SourceModelFilter.Text -> TextSectionItem(it)
-                        is SourceModelFilter.Select<*> -> SelectSectionItem(it)
-                        else -> null
-                    }
-                }
-                subItems.forEach { it.header = group }
-                group.subItems = subItems
-                group
-            }
-            is SourceModelFilter.Sort -> {
-                val group = SortGroup(filter)
-                val subItems = filter.values.map {
-                    SortItem(it, group)
-                }
-                group.subItems = subItems
-                group
-            }
-        }
-    }
-}

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

@@ -0,0 +1,165 @@
+package eu.kanade.tachiyomi.ui.browse.source.browse
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+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 eu.kanade.tachiyomi.widget.TriState
+import eu.kanade.tachiyomi.widget.toTriStateFilter
+import tachiyomi.presentation.core.components.CheckboxItem
+import tachiyomi.presentation.core.components.CollapsibleBox
+import tachiyomi.presentation.core.components.HeadingItem
+import tachiyomi.presentation.core.components.LazyColumn
+import tachiyomi.presentation.core.components.SortItem
+import tachiyomi.presentation.core.components.TextItem
+import tachiyomi.presentation.core.components.material.Button
+import tachiyomi.presentation.core.components.material.Divider
+
+@Composable
+fun SourceFilterDialog(
+    onDismissRequest: () -> Unit,
+    filters: FilterList,
+    onReset: () -> Unit,
+    onFilter: () -> Unit,
+    onUpdate: (FilterList) -> Unit,
+) {
+    val updateFilters = { onUpdate(filters) }
+
+    AdaptiveSheet(
+        onDismissRequest = onDismissRequest,
+    ) { contentPadding ->
+        LazyColumn(
+            contentPadding = contentPadding,
+        ) {
+            stickyHeader {
+                Row(
+                    modifier = Modifier
+                        .background(MaterialTheme.colorScheme.background)
+                        .padding(8.dp),
+                ) {
+                    TextButton(onClick = onReset) {
+                        Text(
+                            text = stringResource(R.string.action_reset),
+                            style = LocalTextStyle.current.copy(
+                                color = MaterialTheme.colorScheme.primary,
+                            ),
+                        )
+                    }
+
+                    Spacer(modifier = Modifier.weight(1f))
+
+                    Button(onClick = onFilter) {
+                        Text(stringResource(R.string.action_filter))
+                    }
+                }
+                Divider()
+            }
+
+            items(filters) {
+                FilterItem(it, updateFilters)
+            }
+        }
+    }
+}
+
+@Composable
+private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit) {
+    when (filter) {
+        is Filter.Header -> {
+            HeadingItem(filter.name)
+        }
+        is Filter.Separator -> {
+            Divider()
+        }
+        is Filter.CheckBox -> {
+            CheckboxItem(
+                label = filter.name,
+                checked = filter.state,
+            ) {
+                filter.state = !filter.state
+                onUpdate()
+            }
+        }
+        is Filter.TriState -> {
+            TriStateItem(
+                label = filter.name,
+                state = filter.state.toTriStateFilter(),
+            ) {
+                filter.state = TriState.valueOf(filter.state).next().value
+                onUpdate()
+            }
+        }
+        is Filter.Text -> {
+            TextItem(
+                label = filter.name,
+                value = filter.state,
+            ) {
+                filter.state = it
+                onUpdate()
+            }
+        }
+        is Filter.Select<*> -> {
+            SelectItem(
+                label = filter.name,
+                options = filter.values,
+                selectedIndex = filter.state,
+            ) {
+                filter.state = it
+                onUpdate()
+            }
+        }
+        is Filter.Sort -> {
+            CollapsibleBox(
+                heading = filter.name,
+            ) {
+                Column {
+                    filter.values.mapIndexed { index, item ->
+                        SortItem(
+                            label = item,
+                            sortDescending = filter.state?.ascending?.not()
+                                ?.takeIf { index == filter.state?.index },
+                        ) {
+                            val ascending = if (index == filter.state?.index) {
+                                !filter.state!!.ascending
+                            } else {
+                                filter.state!!.ascending
+                            }
+                            filter.state = Filter.Sort.Selection(
+                                index = index,
+                                ascending = ascending,
+                            )
+                            onUpdate()
+                        }
+                    }
+                }
+            }
+        }
+        is Filter.Group<*> -> {
+            CollapsibleBox(
+                heading = filter.name,
+            ) {
+                Column {
+                    filter.state
+                        .filterIsInstance<Filter<*>>()
+                        .map { FilterItem(filter = it, onUpdate = onUpdate) }
+                }
+            }
+        }
+    }
+}

+ 0 - 63
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt

@@ -1,63 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.browse
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
-import eu.kanade.tachiyomi.widget.SimpleNavigationView
-import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
-
-class SourceFilterSheet(
-    context: Context,
-    private val onFilterClicked: () -> Unit,
-    private val onResetClicked: () -> Unit,
-) : BaseBottomSheetDialog(context) {
-
-    private var filterNavView: FilterNavigationView = FilterNavigationView(context)
-
-    override fun createView(inflater: LayoutInflater): View {
-        filterNavView.onFilterClicked = {
-            onFilterClicked()
-            this.dismiss()
-        }
-        filterNavView.onResetClicked = onResetClicked
-
-        return filterNavView
-    }
-
-    fun setFilters(items: List<IFlexible<*>>) {
-        filterNavView.adapter.updateDataSet(items)
-    }
-
-    class FilterNavigationView @JvmOverloads constructor(
-        context: Context,
-        attrs: AttributeSet? = null,
-    ) :
-        SimpleNavigationView(context, attrs) {
-
-        var onFilterClicked = {}
-        var onResetClicked = {}
-
-        val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
-            .setDisplayHeadersAtStartUp(true)
-
-        private val binding = SourceFilterSheetBinding.inflate(
-            LayoutInflater.from(context),
-            null,
-            false,
-        )
-
-        init {
-            recycler.adapter = adapter
-            recycler.setHasFixedSize(true)
-            (binding.root.getChildAt(1) as ViewGroup).addView(recycler)
-            addView(binding.root)
-            binding.filterBtn.setOnClickListener { onFilterClicked() }
-            binding.resetBtn.setOnClickListener { onResetClicked() }
-        }
-    }
-}

+ 0 - 46
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/CheckboxItem.kt

@@ -1,46 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.CheckBox
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-
-open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_checkbox
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        val view = holder.check
-        view.text = filter.name
-        view.isChecked = filter.state
-        holder.itemView.setOnClickListener {
-            view.toggle()
-            filter.state = view.isChecked
-        }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as CheckboxItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
-        val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
-    }
-}

+ 0 - 66
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt

@@ -1,66 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.davidea.viewholders.ExpandableViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.util.view.setVectorCompat
-
-class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
-
-    init {
-        isExpanded = false
-    }
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_group
-    }
-
-    override fun getItemViewType(): Int {
-        return 101
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        holder.title.text = filter.name
-
-        holder.icon.setVectorCompat(
-            if (isExpanded) {
-                R.drawable.ic_expand_less_24dp
-            } else {
-                R.drawable.ic_expand_more_24dp
-            },
-        )
-
-        holder.itemView.setOnClickListener(holder)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as GroupItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
-        val title: TextView = itemView.findViewById(R.id.title)
-        val icon: ImageView = itemView.findViewById(R.id.expand_icon)
-
-        override fun shouldNotifyParentOnClick(): Boolean {
-            return true
-        }
-    }
-}

+ 0 - 41
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HeaderItem.kt

@@ -1,41 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.annotation.SuppressLint
-import android.view.View
-import android.widget.TextView
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.R
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractHeaderItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.source.model.Filter
-
-class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
-
-    @SuppressLint("PrivateResource")
-    override fun getLayoutRes(): Int {
-        return R.layout.design_navigation_item_subheader
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        val view = holder.itemView as TextView
-        view.text = filter.name
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as HeaderItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
-}

+ 0 - 100
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt

@@ -1,100 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.kanade.tachiyomi.source.model.Filter
-
-class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as TriStateSectionItem
-        if (head != other.head) return false
-        return filter == other.filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode() + (head?.hashCode() ?: 0)
-    }
-}
-
-class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as TextSectionItem
-        if (head != other.head) return false
-        return filter == other.filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode() + (head?.hashCode() ?: 0)
-    }
-}
-
-class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as CheckboxSectionItem
-        if (head != other.head) return false
-        return filter == other.filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode() + (head?.hashCode() ?: 0)
-    }
-}
-
-class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
-
-    private var head: GroupItem? = null
-
-    override fun getHeader(): GroupItem? = head
-
-    override fun setHeader(header: GroupItem?) {
-        head = header
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as SelectSectionItem
-        if (head != other.head) return false
-        return filter == other.filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode() + (head?.hashCode() ?: 0)
-    }
-}

+ 0 - 59
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SelectItem.kt

@@ -1,59 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.ArrayAdapter
-import android.widget.Spinner
-import android.widget.TextView
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.widget.listener.IgnoreFirstSpinnerListener
-
-open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_spinner
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        holder.text.text = filter.name + ": "
-
-        val spinner = holder.spinner
-        spinner.prompt = filter.name
-        spinner.adapter = ArrayAdapter<Any>(
-            holder.itemView.context,
-            android.R.layout.simple_spinner_item,
-            filter.values,
-        ).apply {
-            setDropDownViewResource(R.layout.common_spinner_item)
-        }
-        spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
-            filter.state = pos
-        }
-        spinner.setSelection(filter.state)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as SelectItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
-
-        val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
-        val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
-    }
-}

+ 0 - 38
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SeparatorItem.kt

@@ -1,38 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.annotation.SuppressLint
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.R
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractHeaderItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.source.model.Filter
-
-class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
-
-    @SuppressLint("PrivateResource")
-    override fun getLayoutRes(): Int {
-        return R.layout.design_navigation_item_separator
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as SeparatorItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
-}

+ 0 - 56
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt

@@ -1,56 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.flexibleadapter.items.ISectionable
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.util.view.setVectorCompat
-
-class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
-
-    init {
-        isExpanded = false
-    }
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_group
-    }
-
-    override fun getItemViewType(): Int {
-        return 100
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        holder.title.text = filter.name
-
-        holder.icon.setVectorCompat(
-            if (isExpanded) {
-                R.drawable.ic_expand_less_24dp
-            } else {
-                R.drawable.ic_expand_more_24dp
-            },
-        )
-
-        holder.itemView.setOnClickListener(holder)
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as SortGroup).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
-}

+ 0 - 75
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortItem.kt

@@ -1,75 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.CheckedTextView
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.recyclerview.widget.RecyclerView
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractSectionableItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.util.system.getResourceColor
-
-class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_checkedtext
-    }
-
-    override fun getItemViewType(): Int {
-        return 102
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        val view = holder.text
-        view.text = name
-        val filter = group.filter
-
-        val i = filter.values.indexOf(name)
-
-        fun getIcon() = when (filter.state) {
-            Filter.Sort.Selection(i, false) ->
-                AppCompatResources.getDrawable(view.context, R.drawable.ic_arrow_down_white_32dp)
-                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
-            Filter.Sort.Selection(i, true) ->
-                AppCompatResources.getDrawable(view.context, R.drawable.ic_arrow_up_white_32dp)
-                    ?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
-            else -> AppCompatResources.getDrawable(view.context, R.drawable.empty_drawable_32dp)
-        }
-
-        view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-        holder.itemView.setOnClickListener {
-            val pre = filter.state?.index ?: i
-            if (pre != i) {
-                filter.state = Filter.Sort.Selection(i, false)
-            } else {
-                filter.state = Filter.Sort.Selection(i, filter.state?.ascending == false)
-            }
-
-            group.subItems.forEach { adapter.notifyItemChanged(adapter.getGlobalPositionOf(it)) }
-        }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        other as SortItem
-        return name == other.name && group == other.group
-    }
-
-    override fun hashCode(): Int {
-        var result = name.hashCode()
-        result = 31 * result + group.hashCode()
-        return result
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
-        val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
-    }
-}

+ 0 - 47
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TextItem.kt

@@ -1,47 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.EditText
-import androidx.core.widget.doOnTextChanged
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.textfield.TextInputLayout
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.source.model.Filter
-
-open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
-
-    override fun getLayoutRes(): Int {
-        return R.layout.navigation_view_text
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        holder.wrapper.hint = filter.name
-        holder.edit.setText(filter.state)
-        holder.edit.doOnTextChanged { text, _, _, _ ->
-            filter.state = text.toString()
-        }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as TextItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
-        val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
-        val edit: EditText = itemView.findViewById(R.id.nav_view_item)
-    }
-}

+ 0 - 80
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TriStateItem.kt

@@ -1,80 +0,0 @@
-package eu.kanade.tachiyomi.ui.browse.source.filter
-
-import android.view.View
-import android.widget.CheckedTextView
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.core.view.updatePadding
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.R
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
-import eu.davidea.flexibleadapter.items.IFlexible
-import eu.davidea.viewholders.FlexibleViewHolder
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.util.system.dpToPx
-import eu.kanade.tachiyomi.util.system.getResourceColor
-import eu.kanade.tachiyomi.R as TR
-
-open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriStateItem.Holder>() {
-
-    override fun getLayoutRes(): Int {
-        return TR.layout.navigation_view_checkedtext
-    }
-
-    override fun getItemViewType(): Int {
-        return 103
-    }
-
-    override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
-        return Holder(view, adapter)
-    }
-
-    override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
-        val view = holder.text
-        view.text = filter.name
-
-        fun getIcon() = AppCompatResources.getDrawable(
-            view.context,
-            when (filter.state) {
-                Filter.TriState.STATE_IGNORE -> TR.drawable.ic_check_box_outline_blank_24dp
-                Filter.TriState.STATE_INCLUDE -> TR.drawable.ic_check_box_24dp
-                Filter.TriState.STATE_EXCLUDE -> TR.drawable.ic_check_box_x_24dp
-                else -> throw Exception("Unknown state")
-            },
-        )?.apply {
-            val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
-                view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
-            } else {
-                view.context.getResourceColor(R.attr.colorPrimary)
-            }
-
-            setTint(color)
-        }
-
-        view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-        holder.itemView.setOnClickListener {
-            filter.state = (filter.state + 1) % 3
-            view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
-        }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-        return filter == (other as TriStateItem).filter
-    }
-
-    override fun hashCode(): Int {
-        return filter.hashCode()
-    }
-
-    class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
-        val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
-
-        init {
-            // Align with native checkbox
-            text.updatePadding(left = 4.dpToPx)
-            text.compoundDrawablePadding = 20.dpToPx
-        }
-    }
-}

+ 0 - 130
app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt

@@ -1,130 +0,0 @@
-package eu.kanade.tachiyomi.widget
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.view.ViewGroup
-import android.widget.CheckBox
-import android.widget.CheckedTextView
-import android.widget.EditText
-import android.widget.FrameLayout
-import android.widget.RadioButton
-import android.widget.Spinner
-import android.widget.TextView
-import androidx.appcompat.widget.TintTypedArray
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.R
-import com.google.android.material.textfield.TextInputLayout
-import eu.kanade.tachiyomi.util.view.inflate
-import eu.kanade.tachiyomi.R as TR
-
-@Suppress("LeakingThis")
-@SuppressLint("PrivateResource", "RestrictedApi")
-open class SimpleNavigationView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0,
-) : FrameLayout(context, attrs, defStyleAttr) {
-
-    /**
-     * Recycler view containing all the items.
-     */
-    protected val recycler = RecyclerView(context)
-
-    init {
-        // Custom attributes
-        val a = TintTypedArray.obtainStyledAttributes(
-            context,
-            attrs,
-            R.styleable.NavigationView,
-            defStyleAttr,
-            R.style.Widget_Design_NavigationView,
-        )
-
-        a.recycle()
-
-        recycler.layoutManager = LinearLayoutManager(context)
-    }
-
-    /**
-     * Base view holder.
-     */
-    abstract class Holder(view: View) : RecyclerView.ViewHolder(view)
-
-    /**
-     * Separator view holder.
-     */
-    class SeparatorHolder(parent: ViewGroup) :
-        Holder(parent.inflate(R.layout.design_navigation_item_separator))
-
-    /**
-     * Header view holder.
-     */
-    class HeaderHolder(parent: ViewGroup) :
-        Holder(parent.inflate(TR.layout.navigation_view_group)) {
-
-        val title: TextView = itemView.findViewById(TR.id.title)
-    }
-
-    /**
-     * Clickable view holder.
-     */
-    abstract class ClickableHolder(view: View, listener: OnClickListener?) : Holder(view) {
-        init {
-            itemView.setOnClickListener(listener)
-        }
-    }
-
-    /**
-     * Radio view holder.
-     */
-    class RadioHolder(parent: ViewGroup, listener: OnClickListener?) :
-        ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) {
-
-        val radio: RadioButton = itemView.findViewById(TR.id.nav_view_item)
-    }
-
-    /**
-     * Checkbox view holder.
-     */
-    class CheckboxHolder(parent: ViewGroup, listener: OnClickListener?) :
-        ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) {
-
-        val check: CheckBox = itemView.findViewById(TR.id.nav_view_item)
-    }
-
-    /**
-     * Multi state view holder.
-     */
-    class MultiStateHolder(parent: ViewGroup, listener: OnClickListener?) :
-        ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) {
-
-        val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
-    }
-
-    class SpinnerHolder(parent: ViewGroup, listener: OnClickListener? = null) :
-        ClickableHolder(parent.inflate(TR.layout.navigation_view_spinner), listener) {
-
-        val text: TextView = itemView.findViewById(TR.id.nav_view_item_text)
-        val spinner: Spinner = itemView.findViewById(TR.id.nav_view_item)
-    }
-
-    class EditTextHolder(parent: ViewGroup) :
-        Holder(parent.inflate(TR.layout.navigation_view_text)) {
-
-        val wrapper: TextInputLayout = itemView.findViewById(TR.id.nav_view_item_wrapper)
-        val edit: EditText = itemView.findViewById(TR.id.nav_view_item)
-    }
-
-    protected companion object {
-        const val VIEW_TYPE_HEADER = 100
-        const val VIEW_TYPE_SEPARATOR = 101
-        const val VIEW_TYPE_RADIO = 102
-        const val VIEW_TYPE_CHECKBOX = 103
-        const val VIEW_TYPE_MULTISTATE = 104
-        const val VIEW_TYPE_TEXT = 105
-        const val VIEW_TYPE_LIST = 106
-    }
-}

+ 0 - 21
app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt

@@ -1,21 +0,0 @@
-package eu.kanade.tachiyomi.widget.listener
-
-import android.view.View
-import android.widget.AdapterView
-import android.widget.AdapterView.OnItemSelectedListener
-
-class IgnoreFirstSpinnerListener(private val block: (Int) -> Unit) : OnItemSelectedListener {
-
-    private var firstEvent = true
-
-    override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
-        if (!firstEvent) {
-            block(position)
-        } else {
-            firstEvent = false
-        }
-    }
-
-    override fun onNothingSelected(parent: AdapterView<*>?) {
-    }
-}

+ 0 - 7
app/src/main/res/drawable/empty_drawable_32dp.xml

@@ -1,7 +0,0 @@
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/transparent" />
-    <size
-        android:width="32dp"
-        android:height="32dp" />
-</shape>

+ 0 - 15
app/src/main/res/drawable/ic_arrow_down_white_32dp.xml

@@ -1,15 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32dp"
-    android:height="32dp"
-    android:viewportWidth="32"
-    android:viewportHeight="32">
-    <group
-        android:pivotX="32"
-        android:pivotY="32"
-        android:scaleX="0.8"
-        android:scaleY="0.8">
-        <path
-            android:fillColor="@android:color/white"
-            android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z" />
-    </group>
-</vector>

+ 0 - 15
app/src/main/res/drawable/ic_arrow_up_white_32dp.xml

@@ -1,15 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32dp"
-    android:height="32dp"
-    android:viewportWidth="32"
-    android:viewportHeight="32">
-    <group
-        android:pivotX="32"
-        android:pivotY="32"
-        android:scaleX="0.8"
-        android:scaleY="0.8">
-        <path
-            android:fillColor="@android:color/white"
-            android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z" />
-    </group>
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_check_box_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M10.6,16.2 L17.65,9.15 16.25,7.75 10.6,13.4 7.75,10.55 6.35,11.95ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/>
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_check_box_x_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z" />
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_expand_less_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
-</vector>

+ 0 - 24
app/src/main/res/layout/navigation_view_checkbox.xml

@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:background="?attr/selectableItemBackground"
-    android:focusable="true"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <CheckBox
-        android:id="@+id/nav_view_item"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:background="@android:color/transparent"
-        android:clickable="false"
-        android:gravity="center_vertical|start"
-        android:maxLines="1"
-        android:paddingHorizontal="16dp"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        tools:text="Title" />
-
-</LinearLayout>

+ 0 - 22
app/src/main/res/layout/navigation_view_checkedtext.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:background="?attr/selectableItemBackground"
-    android:focusable="true"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <CheckedTextView
-        android:id="@+id/nav_view_item"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:drawablePadding="16dp"
-        android:gravity="center_vertical|start"
-        android:maxLines="1"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        tools:text="Title" />
-
-</LinearLayout>

+ 0 - 29
app/src/main/res/layout/navigation_view_group.xml

@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    android:gravity="center_vertical"
-    android:orientation="horizontal"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <TextView
-        android:id="@+id/title"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader"
-        tools:text="Header" />
-
-    <ImageView
-        android:id="@+id/expand_icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:tint="?attr/colorOnBackground"
-        tools:ignore="ContentDescription" />
-
-</LinearLayout>

+ 0 - 22
app/src/main/res/layout/navigation_view_radio.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    android:background="?attr/selectableItemBackground"
-    android:focusable="true"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <RadioButton
-        android:id="@+id/nav_view_item"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:background="@android:color/transparent"
-        android:clickable="false"
-        android:gravity="center_vertical|start"
-        android:maxLines="1"
-        android:paddingHorizontal="16dp"
-        android:textAppearance="?attr/textAppearanceBodyMedium" />
-
-</LinearLayout>

+ 0 - 33
app/src/main/res/layout/navigation_view_spinner.xml

@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/listPreferredItemHeightSmall"
-    android:background="?attr/selectableItemBackground"
-    android:focusable="true"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <TextView
-        android:id="@+id/nav_view_item_text"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:layout_weight="1"
-        android:ellipsize="end"
-        android:maxLines="1"
-        android:paddingEnd="8dp"
-        android:textAppearance="?attr/textAppearanceBodyMedium"
-        android:textColor="?android:attr/textColorSecondary"
-        tools:text="Filter:" />
-
-    <Spinner
-        android:id="@+id/nav_view_item"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:gravity="center_vertical|start"
-        android:maxLines="1"
-        android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
-
-</LinearLayout>

+ 0 - 30
app/src/main/res/layout/navigation_view_text.xml

@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?attr/selectableItemBackground"
-    android:baselineAligned="false"
-    android:focusable="true"
-    android:paddingStart="?attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?attr/listPreferredItemPaddingEnd">
-
-    <com.google.android.material.textfield.TextInputLayout
-        android:id="@+id/nav_view_item_wrapper"
-        style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:gravity="center_vertical|start">
-
-        <eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
-            android:id="@+id/nav_view_item"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:imeOptions="actionDone"
-            android:inputType="text"
-            android:maxLines="1"
-            android:textAppearance="?attr/textAppearanceBodyMedium" />
-
-    </com.google.android.material.textfield.TextInputLayout>
-
-</LinearLayout>

+ 0 - 47
app/src/main/res/layout/source_filter_sheet.xml

@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize"
-        android:background="@drawable/transparent_tabs_background"
-        android:elevation="2dp"
-        android:gravity="center"
-        android:paddingHorizontal="?attr/listPreferredItemPaddingStart">
-
-        <Button
-            android:id="@+id/reset_btn"
-            style="?attr/borderlessButtonStyle"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/action_reset"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <Button
-            android:id="@+id/filter_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:text="@string/action_filter"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_gravity="top"
-        android:layout_weight="1"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:paddingBottom="8dp" />
-
-</LinearLayout>

+ 56 - 0
presentation-core/src/main/java/tachiyomi/presentation/core/components/CollapsibleBox.kt

@@ -0,0 +1,56 @@
+package tachiyomi.presentation.core.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+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.Modifier
+import androidx.compose.ui.unit.dp
+import tachiyomi.presentation.core.theme.header
+
+@Composable
+fun CollapsibleBox(
+    heading: String,
+    content: @Composable () -> Unit,
+) {
+    var expanded by remember { mutableStateOf(false) }
+
+    Column {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .clickable { expanded = !expanded }
+                .padding(horizontal = 24.dp, vertical = 12.dp),
+        ) {
+            Text(
+                text = heading,
+                style = MaterialTheme.typography.header,
+            )
+
+            Spacer(modifier = Modifier.weight(1f))
+
+            Icon(
+                imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+                contentDescription = null,
+            )
+        }
+
+        AnimatedVisibility(visible = expanded) {
+            content()
+        }
+    }
+}

+ 153 - 0
presentation-core/src/main/java/tachiyomi/presentation/core/components/SettingsItems.kt

@@ -0,0 +1,153 @@
+package tachiyomi.presentation.core.components
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+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.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDownward
+import androidx.compose.material.icons.filled.ArrowUpward
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.RadioButton
+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.presentation.core.theme.header
+
+object SettingsItemsPaddings {
+    val Horizontal = 24.dp
+    val Vertical = 10.dp
+}
+
+@Composable
+fun HeadingItem(
+    @StringRes labelRes: Int,
+) {
+    HeadingItem(stringResource(labelRes))
+}
+
+@Composable
+fun HeadingItem(
+    text: String,
+) {
+    Text(
+        text = text,
+        style = MaterialTheme.typography.header,
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+    )
+}
+
+@Composable
+fun SortItem(
+    label: String,
+    sortDescending: Boolean?,
+    onClick: () -> Unit,
+) {
+    val arrowIcon = when (sortDescending) {
+        true -> Icons.Default.ArrowDownward
+        false -> Icons.Default.ArrowUpward
+        null -> null
+    }
+
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(24.dp),
+    ) {
+        if (arrowIcon != null) {
+            Icon(
+                imageVector = arrowIcon,
+                contentDescription = null,
+                tint = MaterialTheme.colorScheme.primary,
+            )
+        } else {
+            Spacer(modifier = Modifier.size(24.dp))
+        }
+        Text(
+            text = label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}
+
+@Composable
+fun CheckboxItem(
+    label: String,
+    checked: Boolean,
+    onClick: () -> Unit,
+) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+        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,
+    selected: Boolean,
+    onClick: () -> Unit,
+) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(24.dp),
+    ) {
+        RadioButton(
+            selected = selected,
+            onClick = null,
+        )
+        Text(
+            text = label,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}
+
+@Composable
+fun TextItem(
+    label: String,
+    value: String,
+    onChange: (String) -> Unit,
+) {
+    OutlinedTextField(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp),
+        label = { Text(text = label) },
+        value = value,
+        onValueChange = onChange,
+        singleLine = true,
+    )
+}