Migrate source filter sheet to Compose (#9135)

(cherry picked from commit 92132c59f5417ef81a7bbba6849be849282fc25e)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt
#	app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt
#	app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt
#	app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt
#	app/src/main/res/drawable/empty_drawable_32dp.xml
#	app/src/main/res/drawable/ic_check_box_24dp.xml
#	app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml
#	app/src/main/res/drawable/ic_check_box_x_24dp.xml
#	app/src/main/res/layout/navigation_view_checkbox.xml
#	app/src/main/res/layout/navigation_view_checkedtext.xml
#	app/src/main/res/layout/navigation_view_group.xml
#	app/src/main/res/layout/navigation_view_radio.xml
#	app/src/main/res/layout/navigation_view_spinner.xml
#	app/src/main/res/layout/navigation_view_text.xml
#	app/src/main/res/layout/source_filter_sheet.xml
This commit is contained in:
arkon 2023-02-23 22:32:40 -05:00 committed by Jobobby04
parent 1a5bf9f763
commit 314a740906
53 changed files with 1122 additions and 2032 deletions

View File

@ -84,7 +84,8 @@ fun SourceFeedScreen(
name: String,
isLoading: Boolean,
items: List<SourceFeedUI>,
onFabClick: (() -> Unit)?,
hasFilters: Boolean,
onFabClick: () -> Unit,
onClickBrowse: () -> Unit,
onClickLatest: () -> Unit,
onClickSavedSearch: (SavedSearch) -> Unit,
@ -107,8 +108,8 @@ fun SourceFeedScreen(
},
floatingActionButton = {
BrowseSourceFloatingActionButton(
isVisible = onFabClick != null,
onFabClick = onFabClick ?: {},
isVisible = hasFilters,
onFabClick = onFabClick,
)
},
) { paddingValues ->

View File

@ -51,26 +51,6 @@ fun RemoveMangaDialog(
)
}
@Composable
fun FailedToLoadSavedSearchDialog(
onDismissRequest: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.ok))
}
},
title = {
Text(text = stringResource(R.string.save_search_failed_to_load))
},
text = {
Text(text = stringResource(R.string.save_search_failed_to_load_message))
},
)
}
@Composable
fun SavedSearchDeleteDialog(
onDismissRequest: () -> Unit,

View File

@ -1,53 +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.graphics.painter.Painter
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(
@ -69,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),
) {
@ -100,120 +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
}
var expanded by remember { mutableStateOf(false) }
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 },
) {
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 = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Checkbox(
checked = checked,
onCheckedChange = null,
)
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
fun RadioItem(
label: String,
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,
)
}
}
// SY -->
@Composable
fun IconItem(
label: String,
icon: Painter,
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),
) {
Icon(
painter = icon,
contentDescription = label,
tint = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
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(),
)
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
},
)
}
}
}
}
// SY <--

View File

@ -15,11 +15,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastForEach
import eu.kanade.domain.library.model.LibraryGroup
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.components.CheckboxItem
import eu.kanade.presentation.components.HeadingItem
import eu.kanade.presentation.components.IconItem
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
@ -34,6 +29,11 @@ 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.IconItem
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
@Composable
fun LibrarySettingsDialog(

View File

@ -29,14 +29,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(

View File

@ -3,12 +3,10 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
@ -36,7 +34,6 @@ data class SourceSearchScreen(
@Composable
override fun Content() {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow
@ -101,9 +98,5 @@ data class SourceSearchScreen(
onMangaLongClick = { navigator.push(MangaScreen(it.id, true)) },
)
}
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, navigator)
}
}
}

View File

@ -0,0 +1,199 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.InputChip
import androidx.compose.material3.InputChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
@Composable
fun AutoCompleteItem(
name: String,
state: List<String>,
hint: String,
values: List<String>,
skipAutoFillTags: List<String>,
validPrefixes: List<String>,
onChange: (List<String>) -> Unit,
) {
val newState = remember { state.toMutableStateList() }
DisposableEffect(newState) {
onChange(newState)
onDispose {}
}
Column(
Modifier.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
) {
AutoCompleteTextField(
values = values,
label = name,
placeholder = hint,
onValueFilter = { tag ->
val prefix = validPrefixes.find { tag.startsWith(it) }
val tagNoPrefix = if (prefix != null) {
tag.removePrefix(prefix)
} else {
tag
}
{ it.contains(tagNoPrefix, true) }
},
onSubmit = { tag ->
val tagNoPrefix = validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
if (tagNoPrefix !in skipAutoFillTags) {
newState += tag
true
} else {
false
}
},
)
FlowRow(
modifier = Modifier.padding(end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
newState.forEach {
InputChip(
selected = false,
onClick = {
newState -= it
},
label = {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
trailingIcon = {
Icon(Icons.Default.Close, contentDescription = it)
},
colors = InputChipDefaults.inputChipColors(
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
labelColor = MaterialTheme.colorScheme.onSurface,
),
)
}
}
}
}
@Composable
fun AutoCompleteTextField(
label: String? = null,
placeholder: String? = null,
values: List<String>,
onValueFilter: ((String) -> ((String) -> Boolean)),
onSubmit: (String) -> Boolean,
) {
var expanded by remember { mutableStateOf(false) }
var value by remember { mutableStateOf(TextFieldValue("")) }
val focusManager = LocalFocusManager.current
fun submit() {
if (onSubmit(value.text)) {
focusManager.clearFocus()
value = TextFieldValue("")
}
}
BackHandler(expanded) {
focusManager.clearFocus()
expanded = false
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it },
) {
OutlinedTextField(
value = value,
onValueChange = { value = it },
label = if (label != null) {
{ Text(label) }
} else {
null
},
placeholder = if (placeholder != null) {
{ Text(placeholder) }
} else {
null
},
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
.runOnEnterKeyPressed { submit() },
singleLine = true,
keyboardActions = KeyboardActions { submit() },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded,
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
val filteredValues by produceState(emptyList(), value) {
withContext(Dispatchers.Default) {
val filter = onValueFilter(value.text)
this@produceState.value = values.asSequence().filter(filter).take(100).toList()
}
}
if (value.text.length > 2 && filteredValues.isNotEmpty()) {
ExposedDropdownMenu(
modifier = Modifier
.exposedDropdownSize(matchTextFieldWidth = true),
expanded = expanded,
onDismissRequest = { expanded = false },
) {
filteredValues.fastForEach {
DropdownMenuItem(
text = { Text(it) },
onClick = {
value = TextFieldValue(it, TextRange(it.length))
submit()
},
)
}
}
}
}
}

View File

@ -39,7 +39,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.BrowseSourceContent
import eu.kanade.presentation.browse.MissingSourceScreen
import eu.kanade.presentation.browse.components.BrowseSourceToolbar
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
import eu.kanade.presentation.browse.components.RemoveMangaDialog
import eu.kanade.presentation.browse.components.SavedSearchCreateDialog
import eu.kanade.presentation.browse.components.SavedSearchDeleteDialog
@ -58,6 +57,8 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listi
import eu.kanade.tachiyomi.ui.category.CategoryScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import eu.kanade.tachiyomi.util.system.toast
import exh.md.follows.MangaDexFollowsScreen
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
@ -103,6 +104,10 @@ data class BrowseSourceScreen(
}
}
// SY -->
val context = LocalContext.current
// SY <--
if (screenModel.source is SourceManager.StubSource) {
MissingSourceScreen(
source = screenModel.source,
@ -112,7 +117,6 @@ data class BrowseSourceScreen(
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val haptic = LocalHapticFeedback.current
val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() }
@ -195,7 +199,7 @@ data class BrowseSourceScreen(
},
)
}
/* SY --> if (state.filters.isNotEmpty())*/ run /* SY <-- */ {
if (/* SY --> */ state.filterable /* SY <-- */) {
FilterChip(
selected = state.listing is Listing.Search,
onClick = screenModel::openFilterSheet,
@ -264,7 +268,58 @@ 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)
onDismissRequest()
},
onUpdate = {
screenModel.setFilters(it)
},
// SY -->
startExpanded = screenModel.startExpanded,
onSave = {
screenModel.onSaveSearch()
},
savedSearches = state.savedSearches,
onSavedSearch = { search ->
screenModel.onSavedSearch(search) {
context.toast(it)
}
},
onSavedSearchPress = {
screenModel.onSavedSearchPress(it)
},
openMangaDexRandom = if (screenModel.sourceIsMangaDex) {
{
screenModel.onMangaDexRandom {
navigator.replace(
BrowseSourceScreen(
sourceId,
"id:$it",
),
)
}
}
} else {
null
},
openMangaDexFollows = if (screenModel.sourceIsMangaDex) {
{
navigator.replace(MangaDexFollowsScreen(sourceId))
}
} else {
null
},
// SY <--
)
}
is BrowseSourceScreenModel.Dialog.AddDuplicateManga -> {
DuplicateMangaDialog(
onDismissRequest = onDismissRequest,
@ -304,14 +359,9 @@ data class BrowseSourceScreen(
screenModel.deleteSearch(dialog.idToDelete)
},
)
BrowseSourceScreenModel.Dialog.FailedToLoadSavedSearch -> FailedToLoadSavedSearchDialog(onDismissRequest)
else -> {}
}
LaunchedEffect(state.filters) {
screenModel.initFilterSheet(context, navigator)
}
LaunchedEffect(Unit) {
queryEvent.receiveAsFlow()
.collectLatest {

View File

@ -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,8 +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 cafe.adriel.voyager.navigator.Navigator
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.core.prefs.asState
import eu.kanade.domain.UnsortedPreferences
import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
@ -33,6 +30,7 @@ import eu.kanade.domain.source.interactor.InsertSavedSearch
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
@ -42,25 +40,11 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem
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.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.toast
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import exh.source.mangaDexSourceIds
import exh.util.nullIfBlank
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@ -132,6 +116,7 @@ open class BrowseSourceScreenModel(
// SY -->
unsortedPreferences: UnsortedPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(),
private val insertSavedSearch: InsertSavedSearch = Injekt.get(),
@ -148,7 +133,11 @@ open class BrowseSourceScreenModel(
// SY -->
val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState(coroutineScope)
val startExpanded by uiPreferences.expandFilters().asState(coroutineScope)
private val filterSerializer = FilterSerializer()
val sourceIsMangaDex = sourceId in mangaDexSourceIds
// SY <--
init {
@ -189,22 +178,18 @@ open class BrowseSourceScreenModel(
if (source is CatalogueSource) {
getExhSavedSearch.subscribe(source.id, source::getFilterList)
.map { it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, EXHSavedSearch::name)) }
.onEach { savedSearches ->
mutableState.update { it.copy(savedSearches = savedSearches) }
withUIContext {
/*withUIContext {
filterSheet?.setSavedSearches(savedSearches)
}
}*/
}
.launchIn(coroutineScope)
}
// SY <--
}
/**
* Sheet containing filter items.
*/
private var filterSheet: SourceFilterSheet? = null
/**
* Flow of Pager flow tied to [State.listing]
*/
@ -265,6 +250,16 @@ open 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
// SY -->
@ -450,7 +445,7 @@ open 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 })
}
@ -464,7 +459,7 @@ open class BrowseSourceScreenModel(
}
fun openFilterSheet() {
filterSheet?.show()
setDialog(Dialog.Filter)
}
fun setDialog(dialog: Dialog?) {
@ -475,66 +470,6 @@ open class BrowseSourceScreenModel(
mutableState.update { it.copy(toolbarQuery = query) }
}
open fun initFilterSheet(context: Context, navigator: Navigator) {
source as? CatalogueSource ?: return
val state = state.value
/*if (state.filters.isEmpty()) {
return
}*/
filterSheet = SourceFilterSheet(
context = context,
// SY -->
navigator = navigator,
source = source,
searches = state.savedSearches,
// SY <--
onFilterClicked = { search(filters = state.filters) },
onResetClicked = {
resetFilters()
filterSheet?.setFilters(state.filterItems)
},
// EXH -->
onSaveClicked = {
coroutineScope.launchIO {
val names = loadSearches().map { it.name }
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearch(names)) }
}
},
onSavedSearchClicked = { idOfSearch ->
coroutineScope.launchIO {
val search = loadSearch(idOfSearch)
if (search == null) {
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
return@launchIO
}
if (search.filterList == null && state.filters.isNotEmpty()) {
withUIContext {
context.toast(R.string.save_search_invalid)
}
return@launchIO
}
val allDefault = search.filterList != null && state.filters == source.getFilterList()
filterSheet?.dismiss()
search(
query = search.query,
filters = if (allDefault) null else search.filterList,
)
}
},
onSavedSearchDeleteClicked = { idToDelete, name ->
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(idToDelete, name)) }
},
// EXH <--
)
filterSheet?.setFilters(state.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())
@ -552,6 +487,7 @@ open 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(
@ -561,7 +497,6 @@ open class BrowseSourceScreenModel(
data class Migrate(val newManga: Manga) : Dialog()
// SY -->
object FailedToLoadSavedSearch : Dialog()
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog()
data class CreateSavedSearch(val currentSavedSearches: List<String>) : Dialog()
// SY <--
@ -575,13 +510,46 @@ open class BrowseSourceScreenModel(
val dialog: Dialog? = null,
// SY -->
val savedSearches: List<EXHSavedSearch> = emptyList(),
val filterable: Boolean = true,
// SY <--
) {
val filterItems get() = filters.toItems()
val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
}
// EXH -->
fun onSaveSearch() {
coroutineScope.launchIO {
val names = state.value.savedSearches.map { it.name }
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearch(names)) }
}
}
fun onSavedSearch(
search: EXHSavedSearch,
onToast: (Int) -> Unit,
) {
coroutineScope.launchIO {
if (search.filterList == null && state.value.filters.isNotEmpty()) {
withUIContext {
onToast(R.string.save_search_invalid)
}
return@launchIO
}
val allDefault = search.filterList != null && search.filterList == (source as? CatalogueSource)?.getFilterList()
setDialog(null)
search(
query = search.query,
filters = if (allDefault) null else search.filterList,
)
}
}
fun onSavedSearchPress(search: EXHSavedSearch) {
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(search.id, search.name)) }
}
fun saveSearch(
name: String,
) {
@ -607,56 +575,12 @@ open class BrowseSourceScreenModel(
}
}
suspend fun loadSearch(searchId: Long): EXHSavedSearch? {
if (source !is CatalogueSource) return null
return getExhSavedSearch.awaitOne(searchId, source::getFilterList)
}
suspend fun loadSearches(): List<EXHSavedSearch> {
if (source !is CatalogueSource) return emptyList()
return getExhSavedSearch.await(source.id, source::getFilterList)
fun onMangaDexRandom(onRandomFound: (String) -> Unit) {
coroutineScope.launchIO {
val random = source.getMainSource<MangaDex>()?.fetchRandomMangaUrl()
?: return@launchIO
onRandomFound(random)
}
}
// EXH <--
}
fun FilterList.toItems(): List<IFlexible<*>> {
return mapNotNull { filter ->
when (filter) {
// --> EXH
is SourceModelFilter.AutoComplete -> AutoComplete(filter)
// <-- EXH
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)
// SY -->
is SourceModelFilter.AutoComplete -> AutoCompleteSectionItem(it)
// SY <--
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
}
}
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.SettingsItemsPaddings
import tachiyomi.presentation.core.components.material.TextButton
@Composable
fun MangaDexFilterHeader(
openMangaDexRandom: () -> Unit,
openMangaDexFollows: () -> Unit,
) {
Row(
Modifier.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal),
horizontalArrangement = Arrangement.SpaceBetween,
) {
TextButton(onClick = openMangaDexRandom) {
Text(stringResource(R.string.random))
}
TextButton(onClick = openMangaDexFollows) {
Text(stringResource(R.string.mangadex_follows))
}
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.SuggestionChip
import eu.kanade.presentation.components.SuggestionChipDefaults
import eu.kanade.tachiyomi.R
import tachiyomi.domain.source.model.EXHSavedSearch
import tachiyomi.presentation.core.components.SettingsItemsPaddings
@Composable
fun SavedSearchItem(
savedSearches: List<EXHSavedSearch>,
onSavedSearch: (EXHSavedSearch) -> Unit,
onSavedSearchPress: (EXHSavedSearch) -> Unit,
) {
if (savedSearches.isEmpty()) return
Column(
Modifier
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
) {
Text(
text = stringResource(R.string.saved_searches),
style = MaterialTheme.typography.bodySmall,
)
FlowRow(
modifier = Modifier.padding(end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
savedSearches.forEach {
SuggestionChip(
onClick = { onSavedSearch(it) },
onLongClick = { onSavedSearchPress(it) },
label = {
Text(
text = it.name,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
colors = SuggestionChipDefaults.suggestionChipColors(
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
labelColor = MaterialTheme.colorScheme.onSurface,
),
)
}
}
}
}

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.databinding.SourceFilterSheetSavedSearchesBinding
class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
RecyclerView.Adapter<SavedSearchesAdapter.SavedSearchesViewHolder>() {
private lateinit var binding: SourceFilterSheetSavedSearchesBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
binding = SourceFilterSheetSavedSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SavedSearchesViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
holder.bind(chips)
}
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(chips: List<Chip> = emptyList()) {
binding.savedSearches.removeAllViews()
if (chips.isEmpty()) {
binding.savedSearchesTitle.isVisible = false
} else {
binding.savedSearchesTitle.isVisible = true
chips.forEach {
binding.savedSearches.addView(it)
}
}
}
}
}

View File

@ -0,0 +1,226 @@
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.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.domain.source.model.EXHSavedSearch
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,
// SY -->
startExpanded: Boolean,
savedSearches: List<EXHSavedSearch>,
onSave: () -> Unit,
onSavedSearch: (EXHSavedSearch) -> Unit,
onSavedSearchPress: (EXHSavedSearch) -> Unit,
openMangaDexRandom: (() -> Unit)?,
openMangaDexFollows: (() -> Unit)?,
// SY <--
) {
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))
// SY -->
IconButton(onClick = onSave) {
Icon(
Icons.Default.Save,
contentDescription = stringResource(R.string.action_save),
tint = MaterialTheme.colorScheme.onBackground,
)
}
// SY <--
Button(onClick = onFilter) {
Text(stringResource(R.string.action_filter))
}
}
Divider()
}
if (openMangaDexRandom != null && openMangaDexFollows != null) {
item {
MangaDexFilterHeader(
openMangaDexRandom = openMangaDexRandom,
openMangaDexFollows = openMangaDexFollows,
)
}
}
item {
SavedSearchItem(
savedSearches = savedSearches,
onSavedSearch = onSavedSearch,
onSavedSearchPress = onSavedSearchPress,
)
}
items(filters) {
FilterItem(it, updateFilters /* SY --> */, startExpanded /* SY <-- */)
}
}
}
}
@Composable
private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit/* SY --> */, startExpanded: Boolean /* SY <-- */) {
when (filter) {
// SY -->
is Filter.AutoComplete -> {
AutoCompleteItem(
name = filter.name,
state = filter.state,
hint = filter.hint,
values = filter.values,
skipAutoFillTags = filter.skipAutoFillTags,
validPrefixes = filter.validPrefixes,
) {
filter.state = it
onUpdate()
}
}
// SY <--
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,
// SY -->
startExpanded = startExpanded,
// SY <--
) {
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,
// SY -->
startExpanded = startExpanded,
// SY <--
) {
Column {
filter.state
.filterIsInstance<Filter<*>>()
.map { FilterItem(filter = it, onUpdate = onUpdate /* SY --> */, startExpanded /* SY <-- */) }
}
}
}
}
}

View File

@ -1,167 +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 androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import cafe.adriel.voyager.navigator.Navigator
import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.widget.SimpleNavigationView
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import exh.md.MangaDexFabHeaderAdapter
import exh.source.getMainSource
import tachiyomi.domain.source.model.EXHSavedSearch
class SourceFilterSheet(
context: Context,
// SY -->
navigator: Navigator,
source: CatalogueSource,
searches: List<EXHSavedSearch> = emptyList(),
// SY <--
private val onFilterClicked: () -> Unit,
private val onResetClicked: () -> Unit,
// EXH -->
private val onSaveClicked: () -> Unit,
var onSavedSearchClicked: (Long) -> Unit = {},
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> },
// EXH <--
) : BaseBottomSheetDialog(context) {
private var filterNavView: FilterNavigationView = FilterNavigationView(
context,
// SY -->
searches = searches,
source = source,
navigator = navigator,
dismissSheet = ::dismiss,
// SY <--
)
override fun createView(inflater: LayoutInflater): View {
filterNavView.onFilterClicked = {
onFilterClicked()
this.dismiss()
}
filterNavView.onResetClicked = onResetClicked
// EXH -->
filterNavView.onSaveClicked = onSaveClicked
filterNavView.onSavedSearchClicked = onSavedSearchClicked
filterNavView.onSavedSearchDeleteClicked = onSavedSearchDeleteClicked
// EXH <--
return filterNavView
}
fun setFilters(items: List<IFlexible<*>>) {
filterNavView.adapter.updateDataSet(items)
}
// SY -->
fun setSavedSearches(searches: List<EXHSavedSearch>) {
filterNavView.setSavedSearches(searches)
}
fun hideFilterButton() {
filterNavView.hideFilterButton()
}
// SY <--
class FilterNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
// SY -->
searches: List<EXHSavedSearch> = emptyList(),
source: CatalogueSource? = null,
navigator: Navigator? = null,
dismissSheet: (() -> Unit)? = null,
// SY <--
) :
SimpleNavigationView(context, attrs) {
var onFilterClicked = {}
var onResetClicked = {}
// SY -->
var onSaveClicked = {}
var onSavedSearchClicked: (Long) -> Unit = {}
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> }
private val savedSearchesAdapter = SavedSearchesAdapter(getSavedSearchesChips(searches))
// SY <--
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
private val binding = SourceFilterSheetBinding.inflate(
LayoutInflater.from(context),
null,
false,
)
init {
// SY -->
recycler.adapter = ConcatAdapter(
listOfNotNull(
navigator?.let {
source?.getMainSource<MangaDex>()
?.let {
MangaDexFabHeaderAdapter(navigator, it) {
dismissSheet?.invoke()
}
}
},
savedSearchesAdapter,
adapter,
),
)
// SY <--
recycler.setHasFixedSize(true)
(binding.root.getChildAt(1) as ViewGroup).addView(recycler)
addView(binding.root)
// SY -->
binding.saveSearchBtn.setOnClickListener { onSaveClicked() }
// SY <--
binding.filterBtn.setOnClickListener { onFilterClicked() }
binding.resetBtn.setOnClickListener { onResetClicked() }
}
// EXH -->
fun setSavedSearches(searches: List<EXHSavedSearch>) {
savedSearchesAdapter.chips = getSavedSearchesChips(searches)
savedSearchesAdapter.notifyItemChanged(0)
}
private fun getSavedSearchesChips(searches: List<EXHSavedSearch>): List<Chip> {
return searches
.map { search ->
Chip(context).apply {
text = search.name
setOnClickListener { onSavedSearchClicked(search.id) }
setOnLongClickListener {
onSavedSearchDeleteClicked(search.id, search.name); true
}
}
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.text.toString() })
}
fun hideFilterButton() {
binding.filterBtn.isVisible = false
}
// EXH <--
}
}

View File

@ -1,12 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.source.feed
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
@ -14,42 +11,34 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.interactor.GetRemoteManga
import eu.kanade.presentation.browse.SourceFeedScreen
import eu.kanade.presentation.browse.components.FailedToLoadSavedSearchDialog
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.util.system.toast
import exh.md.follows.MangaDexFollowsScreen
import exh.util.nullIfBlank
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import tachiyomi.core.util.lang.launchUI
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.SavedSearch
import xyz.nulldev.ts.api.http.serializer.FilterSerializer
class SourceFeedScreen(val sourceId: Long) : Screen() {
@Transient
private var filterSheet: SourceFilterSheet? = null
@Composable
override fun Content() {
val screenModel = rememberScreenModel { SourceFeedScreenModel(sourceId) }
val state by screenModel.state.collectAsState()
val navigator = LocalNavigator.currentOrThrow
val context = LocalContext.current
SourceFeedScreen(
name = screenModel.source.name,
isLoading = state.isLoading,
items = state.items,
onFabClick = if (state.filters.isEmpty()) null else { { filterSheet?.show() } },
hasFilters = state.filters.isNotEmpty(),
onFabClick = screenModel::openFilterSheet,
onClickBrowse = { onBrowseClick(navigator, screenModel.source) },
onClickLatest = { onLatestClick(navigator, screenModel.source) },
onClickSavedSearch = { onSavedSearchClick(navigator, screenModel.source, it) },
@ -82,8 +71,70 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
},
)
}
SourceFeedScreenModel.Dialog.FailedToLoadSavedSearch -> {
FailedToLoadSavedSearchDialog(onDismissRequest)
SourceFeedScreenModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
onReset = {},
onFilter = {
screenModel.onFilter { query, filters ->
onBrowseClick(
navigator = navigator,
sourceId = sourceId,
search = query,
filters = filters,
)
}
},
onUpdate = {
screenModel.setFilters(it)
},
startExpanded = screenModel.startExpanded,
onSave = {},
savedSearches = state.savedSearches,
onSavedSearch = { search ->
screenModel.onSavedSearch(
search,
onBrowseClick = { query, searchId ->
onBrowseClick(
navigator = navigator,
sourceId = sourceId,
search = query,
savedSearch = searchId,
)
},
onToast = {
context.toast(it)
},
)
},
onSavedSearchPress = { search ->
screenModel.onSavedSearchAddToFeed(search) {
context.toast(it)
}
},
openMangaDexRandom = if (screenModel.sourceIsMangaDex) {
{
screenModel.onMangaDexRandom {
navigator.replace(
BrowseSourceScreen(
sourceId,
"id:$it",
),
)
}
}
} else {
null
},
openMangaDexFollows = if (screenModel.sourceIsMangaDex) {
{
navigator.replace(MangaDexFollowsScreen(sourceId))
}
} else {
null
},
)
}
null -> Unit
}
@ -91,95 +142,6 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
BackHandler(state.searchQuery != null) {
screenModel.search(null)
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
LaunchedEffect(state.filters) {
initFilterSheet(state, screenModel, scope, context, navigator)
}
}
fun initFilterSheet(
state: SourceFeedState,
screenModel: SourceFeedScreenModel,
viewScope: CoroutineScope,
context: Context,
navigator: Navigator,
) {
val filterSerializer = FilterSerializer()
filterSheet = SourceFilterSheet(
context = context,
// SY -->
navigator = navigator,
source = screenModel.source,
searches = emptyList(),
// SY <--
onFilterClicked = {
val allDefault = state.filters == screenModel.source.getFilterList()
filterSheet?.dismiss()
if (allDefault) {
onBrowseClick(
navigator,
screenModel.source.id,
state.searchQuery?.nullIfBlank(),
)
} else {
onBrowseClick(
navigator,
screenModel.source.id,
state.searchQuery?.nullIfBlank(),
filters = Json.encodeToString(filterSerializer.serialize(state.filters)),
)
}
},
onResetClicked = {},
onSaveClicked = {},
onSavedSearchClicked = { idOfSearch ->
viewScope.launchUI {
val search = screenModel.loadSearch(idOfSearch)
if (search == null) {
screenModel.openFailedToLoadSavedSearch()
return@launchUI
}
if (search.filterList == null && state.filters.isNotEmpty()) {
context.toast(R.string.save_search_invalid)
return@launchUI
}
if (search.filterList != null) {
screenModel.setFilters(FilterList(search.filterList!!))
filterSheet?.setFilters(state.filterItems)
}
val allDefault = search.filterList != null && state.filters == screenModel.source.getFilterList()
filterSheet?.dismiss()
if (!allDefault) {
onBrowseClick(
navigator,
screenModel.source.id,
search = state.searchQuery?.nullIfBlank(),
savedSearch = search.id,
)
}
}
},
onSavedSearchDeleteClicked = { idOfSearch, name ->
viewScope.launchUI {
if (screenModel.hasTooManyFeeds()) {
context.toast(R.string.too_many_in_feed)
return@launchUI
}
screenModel.openAddFeed(idOfSearch, name)
}
},
)
viewScope.launchUI {
filterSheet?.setSavedSearches(screenModel.loadSearches())
}
filterSheet?.setFilters(state.filterItems)
}
private fun onMangaClick(navigator: Navigator, manga: Manga) {

View File

@ -3,10 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.source.feed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
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.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.copyFrom
import eu.kanade.domain.manga.model.toDomainManga
@ -17,11 +18,16 @@ import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.browse.SourceFeedUI
import eu.kanade.tachiyomi.R
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.browse.toItems
import eu.kanade.tachiyomi.source.online.all.MangaDex
import exh.source.getMainSource
import exh.source.mangaDexSourceIds
import exh.util.nullIfBlank
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -32,16 +38,20 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import logcat.LogPriority
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withNonCancellableContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.toMangaUpdate
import tachiyomi.domain.source.model.EXHSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import uy.kohesive.injekt.Injekt
@ -52,6 +62,7 @@ import tachiyomi.domain.manga.model.Manga as DomainManga
open class SourceFeedScreenModel(
val sourceId: Long,
uiPreferences: UiPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
@ -66,11 +77,20 @@ open class SourceFeedScreenModel(
val source = sourceManager.getOrStub(sourceId) as CatalogueSource
val sourceIsMangaDex = sourceId in mangaDexSourceIds
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
val startExpanded by uiPreferences.expandFilters().asState(coroutineScope)
init {
setFilters(source.getFilterList())
coroutineScope.launchIO {
val searches = loadSearches()
mutableState.update { it.copy(savedSearches = searches) }
}
getFeedSavedSearchBySourceId.subscribe(source.id)
.onEach {
val items = getSourcesToGetFeed(it)
@ -88,7 +108,7 @@ open class SourceFeedScreenModel(
mutableState.update { it.copy(filters = filters) }
}
suspend fun hasTooManyFeeds(): Boolean {
private suspend fun hasTooManyFeeds(): Boolean {
return countFeedSavedSearchBySourceId.await(source.id) > 10
}
@ -213,16 +233,84 @@ open class SourceFeedScreenModel(
}
}
suspend fun loadSearch(searchId: Long) =
getExhSavedSearch.awaitOne(searchId, source::getFilterList)
suspend fun loadSearches() =
private suspend fun loadSearches() =
getExhSavedSearch.await(source.id, source::getFilterList)
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, EXHSavedSearch::name))
fun onFilter(onBrowseClick: (query: String?, filters: String?) -> Unit) {
coroutineScope.launchIO {
val allDefault = state.value.filters == source.getFilterList()
dismissDialog()
if (allDefault) {
onBrowseClick(
state.value.searchQuery?.nullIfBlank(),
null,
)
} else {
onBrowseClick(
state.value.searchQuery?.nullIfBlank(),
Json.encodeToString(filterSerializer.serialize(state.value.filters)),
)
}
}
}
fun onSavedSearch(
search: EXHSavedSearch,
onBrowseClick: (query: String?, searchId: Long) -> Unit,
onToast: (Int) -> Unit,
) {
coroutineScope.launchIO {
if (search.filterList == null && state.value.filters.isNotEmpty()) {
withUIContext {
onToast(R.string.save_search_invalid)
}
return@launchIO
}
val allDefault = search.filterList != null && search.filterList == source.getFilterList()
dismissDialog()
if (!allDefault) {
onBrowseClick(
state.value.searchQuery?.nullIfBlank(),
search.id,
)
}
}
}
fun onSavedSearchAddToFeed(
search: EXHSavedSearch,
onToast: (Int) -> Unit,
) {
coroutineScope.launchIO {
if (hasTooManyFeeds()) {
withUIContext {
onToast(R.string.too_many_in_feed)
}
return@launchIO
}
openAddFeed(search.id, search.name)
}
}
fun onMangaDexRandom(onRandomFound: (String) -> Unit) {
coroutineScope.launchIO {
val random = source.getMainSource<MangaDex>()?.fetchRandomMangaUrl()
?: return@launchIO
onRandomFound(random)
}
}
fun search(query: String?) {
mutableState.update { it.copy(searchQuery = query) }
}
fun openFilterSheet() {
mutableState.update { it.copy(dialog = Dialog.Filter) }
}
fun openDeleteFeed(feed: FeedSavedSearch) {
mutableState.update { it.copy(dialog = Dialog.DeleteFeed(feed)) }
}
@ -231,18 +319,14 @@ open class SourceFeedScreenModel(
mutableState.update { it.copy(dialog = Dialog.AddFeed(feedId, name)) }
}
fun openFailedToLoadSavedSearch() {
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
}
fun dismissDialog() {
mutableState.update { it.copy(dialog = null) }
}
sealed class Dialog {
object Filter : Dialog()
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
data class AddFeed(val feedId: Long, val name: String) : Dialog()
object FailedToLoadSavedSearch : Dialog()
}
override fun onDispose() {
@ -256,10 +340,9 @@ data class SourceFeedState(
val searchQuery: String? = null,
val items: List<SourceFeedUI> = emptyList(),
val filters: FilterList = FilterList(),
val savedSearches: List<EXHSavedSearch> = emptyList(),
val dialog: SourceFeedScreenModel.Dialog? = null,
) {
val filterItems: List<IFlexible<*>> by lazy { filters.toItems() }
val isLoading
get() = items.isEmpty()
}

View File

@ -1,129 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.filter
import android.annotation.SuppressLint
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
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.AutoCompleteAdapter
import exh.log.xLogD
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
override fun getLayoutRes(): Int {
return R.layout.navigation_view_autocomplete
}
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?>?) {
@SuppressLint("SetTextI18n")
holder.text.text = "${filter.name}: "
holder.autoComplete.hint = filter.hint
holder.autoComplete.setAdapter(
AutoCompleteAdapter(
holder.itemView.context,
android.R.layout.simple_dropdown_item_1line,
filter.values,
filter.validPrefixes,
),
)
holder.autoComplete.threshold = 3
// select from auto complete
holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ ->
val tag = (adapterView.getItemAtPosition(chipPosition) as String).trim()
val tagNoPrefix = filter.validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
if (tagNoPrefix !in filter.skipAutoFillTags) {
holder.autoComplete.text = null
addTag(tag, holder)
}
}
// done keyboard button is pressed
holder.autoComplete.setOnEditorActionListener { textView, actionId, _ ->
if (actionId != EditorInfo.IME_ACTION_DONE) return@setOnEditorActionListener false
val tag = textView.text.toString().trim()
val tagNoPrefix = filter.validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
if (tagNoPrefix !in filter.skipAutoFillTags) {
textView.text = null
addTag(tag, holder)
}
true
}
// space or comma is detected
holder.autoComplete.addTextChangedListener {
if (it == null || it.isEmpty()) {
return@addTextChangedListener
}
if (it.last() == ',') {
val name = it.toString().dropLast(1).trim()
addTag(name, holder)
holder.autoComplete.text = null
}
}
holder.mainTagChipGroup.removeAllViews()
filter.state.forEach {
addChipToGroup(it, holder)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return filter == (other as AutoComplete).filter
}
override fun hashCode(): Int {
return filter.hashCode()
}
private fun addTag(name: String, holder: Holder) {
if (name.isNotEmpty() && !filter.state.contains(name)) {
addChipToGroup(name, holder)
filter.state += name
} else {
xLogD("Invalid tag: %s", name)
}
}
private fun addChipToGroup(name: String, holder: Holder) {
val chip = Chip(holder.itemView.context)
chip.text = name
chip.isClickable = true
chip.isCheckable = false
chip.isCloseIconVisible = true
holder.mainTagChipGroup.addView(chip)
chip.setOnCloseIconClickListener {
holder.mainTagChipGroup.removeView(chip)
filter.state -= name
}
}
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
val autoComplete: AutoCompleteTextView = itemView.findViewById(R.id.nav_view_item)
val mainTagChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group)
}
}

View File

@ -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)
}
}

View File

@ -1,71 +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.domain.ui.UiPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.util.view.setVectorCompat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
init {
// --> EH
isExpanded = Injekt.get<UiPreferences>().expandFilters().get()
// <-- EH
}
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
}
}
}

View File

@ -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)
}

View File

@ -1,123 +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)
}
}
// SY -->
class AutoCompleteSectionItem(filter: Filter.AutoComplete) : AutoComplete(filter), ISectionable<AutoComplete.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
return filter == (other as AutoCompleteSectionItem).filter
}
override fun hashCode(): Int {
return filter.hashCode()
}
}
// SY <--

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.library
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope

View File

@ -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
}
}

View File

@ -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<*>?) {
}
}

View File

@ -1,54 +0,0 @@
package exh.md
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import exh.md.follows.MangaDexFollowsScreen
import tachiyomi.core.util.lang.launchUI
import tachiyomi.core.util.lang.withIOContext
class MangaDexFabHeaderAdapter(val navigator: Navigator, val source: CatalogueSource, val onClick: () -> Unit) :
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
private lateinit var binding: SourceFilterMangadexHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
binding = SourceFilterMangadexHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SavedSearchesViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
holder.bind()
}
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
binding.mangadexFollows.setOnClickListener {
navigator.replace(MangaDexFollowsScreen(source.id))
onClick()
}
binding.mangadexRandom.setOnClickListener {
launchUI {
val randomMangaUrl = withIOContext {
(source as? RandomMangaSource)?.fetchRandomMangaUrl()
}
navigator.replace(
BrowseSourceScreen(
source.id,
"id:$randomMangaUrl",
),
)
onClick()
}
}
}
}
}

View File

@ -1,7 +1,5 @@
package exh.md.follows
import android.content.Context
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
@ -10,6 +8,7 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import tachiyomi.domain.manga.model.Manga
class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourceId, null) {
@ -22,7 +21,7 @@ class MangaDexFollowsScreenModel(sourceId: Long) : BrowseSourceScreenModel(sourc
return map { it to metadata }
}
override fun initFilterSheet(context: Context, navigator: Navigator) {
// No-op: we don't allow filtering in recs
init {
mutableState.update { it.copy(filterable = false) }
}
}

View File

@ -1,7 +1,5 @@
package exh.md.similar
import android.content.Context
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.source.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
@ -10,6 +8,7 @@ import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
@ -32,7 +31,7 @@ class MangaDexSimilarScreenModel(
return map { it to metadata }
}
override fun initFilterSheet(context: Context, navigator: Navigator) {
// No-op: we don't allow filtering in recs
init {
mutableState.update { it.copy(filterable = false) }
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -1,43 +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="wrap_content"
android:orientation="vertical"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall">
<TextView
android:id="@+id/nav_view_item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="Filter:" />
<AutoCompleteTextView
android:id="@+id/nav_view_item"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:completionThreshold="1"
android:gravity="center_vertical|start"
android:imeOptions="actionDone"
android:inputType="textCapWords" />
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:chipSpacingVertical="4dp"
style="@style/Widget.Tachiyomi.Chip.Action"/>
</LinearLayout>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/mangadex_random"
style="?attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/random"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/mangadex_follows"
style="?attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/mangadex_follows"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,62 +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" />
<ImageButton
android:id="@+id/save_search_btn"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:contentDescription="@string/action_save"
android:background="?attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/filter_btn"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@+id/reset_btn"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_save_black_24dp"
app:tint="?attr/colorAccent" />
<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>

View File

@ -1,27 +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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/saved_searches_title"
style="@style/Widget.Tachiyomi.Chip.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:visibility="visible"
android:text="@string/saved_searches" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/saved_searches"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:chipSpacingHorizontal="4dp" />
</LinearLayout>

View File

@ -0,0 +1,59 @@
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,
// SY -->
startExpanded: Boolean = false,
// SY <--
content: @Composable () -> Unit,
) {
var expanded by remember { mutableStateOf(/* SY --> */startExpanded/* SY <-- */) }
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()
}
}
}

View File

@ -0,0 +1,187 @@
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.graphics.painter.Painter
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,
)
}
// SY -->
@Composable
fun IconItem(
label: String,
icon: Painter,
selected: Boolean,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable(onClick = onClick)
.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Icon(
painter = icon,
contentDescription = label,
tint = if (selected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
},
)
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
}
}
// SY <--