diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt index 8d6f072ff..4af59feba 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourceFeedScreen.kt @@ -84,7 +84,8 @@ fun SourceFeedScreen( name: String, isLoading: Boolean, items: List, - 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 -> diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt index a067234ee..235d7a534 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceDialogs.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt index e5001dfa5..57549e990 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt @@ -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, + 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 <-- diff --git a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt index 1bc600ee1..50dd3dbe6 100644 --- a/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt index 709d6de38..3951428ee 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/ChapterSettingsDialog.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index 2bbb66fc8..b175c047a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -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) - } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt new file mode 100644 index 000000000..f566d87e3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/AutoCompleteItem.kt @@ -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, + hint: String, + values: List, + skipAutoFillTags: List, + validPrefixes: List, + onChange: (List) -> 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, + 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() + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index e52576c8d..81c52bd03 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt index 143556070..b595baefb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.ui.browse.source.browse -import android.content.Context import android.content.res.Configuration import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.runtime.Immutable @@ -14,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) : Dialog() // SY <-- @@ -575,13 +510,46 @@ open class BrowseSourceScreenModel( val dialog: Dialog? = null, // SY --> val savedSearches: List = 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 { - if (source !is CatalogueSource) return emptyList() - return getExhSavedSearch.await(source.id, source::getFilterList) + fun onMangaDexRandom(onRandomFound: (String) -> Unit) { + coroutineScope.launchIO { + val random = source.getMainSource()?.fetchRandomMangaUrl() + ?: return@launchIO + onRandomFound(random) + } } // EXH <-- } - -fun FilterList.toItems(): List> { - 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 - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/MangaDexFilterHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/MangaDexFilterHeader.kt new file mode 100644 index 000000000..669d4c639 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/MangaDexFilterHeader.kt @@ -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)) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchItem.kt new file mode 100644 index 000000000..9a61c1a3f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchItem.kt @@ -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, + 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, + ), + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchesAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchesAdapter.kt deleted file mode 100644 index 3151b247a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SavedSearchesAdapter.kt +++ /dev/null @@ -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 = emptyList()) : - RecyclerView.Adapter() { - - 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 = emptyList()) { - binding.savedSearches.removeAllViews() - if (chips.isEmpty()) { - binding.savedSearchesTitle.isVisible = false - } else { - binding.savedSearchesTitle.isVisible = true - chips.forEach { - binding.savedSearches.addView(it) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt new file mode 100644 index 000000000..3f9ca20f3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterDialog.kt @@ -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, + 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>() + .map { FilterItem(filter = it, onUpdate = onUpdate /* SY --> */, startExpanded /* SY <-- */) } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt deleted file mode 100644 index dcc7ea4e1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt +++ /dev/null @@ -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 = 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>) { - filterNavView.adapter.updateDataSet(items) - } - - // SY --> - fun setSavedSearches(searches: List) { - filterNavView.setSavedSearches(searches) - } - - fun hideFilterButton() { - filterNavView.hideFilterButton() - } - // SY <-- - - class FilterNavigationView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - // SY --> - searches: List = 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> = FlexibleAdapter>(null) - .setDisplayHeadersAtStartUp(true) - - private val binding = SourceFilterSheetBinding.inflate( - LayoutInflater.from(context), - null, - false, - ) - - init { - // SY --> - recycler.adapter = ConcatAdapter( - listOfNotNull( - navigator?.let { - source?.getMainSource() - ?.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) { - savedSearchesAdapter.chips = getSavedSearchesChips(searches) - savedSearchesAdapter.notifyItemChanged(0) - } - - private fun getSavedSearchesChips(searches: List): List { - 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 <-- - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt index 3898c7a34..1832c5bdf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreen.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 0718c5221..1e31dab92 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -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()?.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 = emptyList(), val filters: FilterList = FilterList(), + val savedSearches: List = emptyList(), val dialog: SourceFeedScreenModel.Dialog? = null, ) { - val filterItems: List> by lazy { filters.toItems() } - val isLoading get() = items.isEmpty() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt deleted file mode 100644 index 0343b4e09..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/AutoComplete.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_autocomplete - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - @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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/CheckboxItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/CheckboxItem.kt deleted file mode 100644 index beb52ec52..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/CheckboxItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_checkbox - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt deleted file mode 100644 index 39d334e9e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt +++ /dev/null @@ -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>() { - - init { - // --> EH - isExpanded = Injekt.get().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>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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 - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HeaderItem.kt deleted file mode 100644 index 7c0692036..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/HeaderItem.kt +++ /dev/null @@ -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() { - - @SuppressLint("PrivateResource") - override fun getLayoutRes(): Int { - return R.layout.design_navigation_item_subheader - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt deleted file mode 100644 index c499b50ae..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt +++ /dev/null @@ -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 { - - 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 { - - 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 { - - 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 { - - 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 { - - 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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SelectItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SelectItem.kt deleted file mode 100644 index d64e6c89f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SelectItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_spinner - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - holder.text.text = filter.name + ": " - - val spinner = holder.spinner - spinner.prompt = filter.name - spinner.adapter = ArrayAdapter( - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SeparatorItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SeparatorItem.kt deleted file mode 100644 index af21aeabb..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SeparatorItem.kt +++ /dev/null @@ -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() { - - @SuppressLint("PrivateResource") - override fun getLayoutRes(): Int { - return R.layout.design_navigation_item_separator - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - } - - 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) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt deleted file mode 100644 index f64837004..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortGroup.kt +++ /dev/null @@ -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>() { - - 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>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortItem.kt deleted file mode 100644 index 160856f82..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SortItem.kt +++ /dev/null @@ -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(group) { - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_checkedtext - } - - override fun getItemViewType(): Int { - return 102 - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TextItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TextItem.kt deleted file mode 100644 index b0f5010af..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TextItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.navigation_view_text - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TriStateItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TriStateItem.kt deleted file mode 100644 index 04e4e1edc..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/TriStateItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return TR.layout.navigation_view_checkedtext - } - - override fun getItemViewType(): Int { - return 103 - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): Holder { - return Holder(view, adapter) - } - - override fun bindViewHolder(adapter: FlexibleAdapter>, holder: Holder, position: Int, payloads: List?) { - 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 - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt index 44aaa1ee6..37300b8cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt deleted file mode 100755 index 3c6bd01e7..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt deleted file mode 100755 index 38c946e01..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt +++ /dev/null @@ -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<*>?) { - } -} diff --git a/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt b/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt deleted file mode 100644 index c90ed79f9..000000000 --- a/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt +++ /dev/null @@ -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() { - - 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() - } - } - } - } -} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt index 54b98cb0f..237e09410 100644 --- a/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsScreenModel.kt @@ -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) } } } diff --git a/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt b/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt index a94993eda..d8dc77806 100644 --- a/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt +++ b/app/src/main/java/exh/md/similar/MangaDexSimilarScreenModel.kt @@ -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) } } } diff --git a/app/src/main/res/drawable/empty_drawable_32dp.xml b/app/src/main/res/drawable/empty_drawable_32dp.xml deleted file mode 100755 index 565890191..000000000 --- a/app/src/main/res/drawable/empty_drawable_32dp.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml b/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml deleted file mode 100644 index fadc173d4..000000000 --- a/app/src/main/res/drawable/ic_arrow_down_white_32dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_arrow_up_white_32dp.xml b/app/src/main/res/drawable/ic_arrow_up_white_32dp.xml deleted file mode 100644 index 9ce9ae5f5..000000000 --- a/app/src/main/res/drawable/ic_arrow_up_white_32dp.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_check_box_24dp.xml b/app/src/main/res/drawable/ic_check_box_24dp.xml deleted file mode 100755 index f3f55d2ca..000000000 --- a/app/src/main/res/drawable/ic_check_box_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml b/app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml deleted file mode 100755 index 1e39096b7..000000000 --- a/app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_box_x_24dp.xml b/app/src/main/res/drawable/ic_check_box_x_24dp.xml deleted file mode 100755 index bd6a98ffa..000000000 --- a/app/src/main/res/drawable/ic_check_box_x_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_expand_less_24dp.xml b/app/src/main/res/drawable/ic_expand_less_24dp.xml deleted file mode 100644 index 7f427269d..000000000 --- a/app/src/main/res/drawable/ic_expand_less_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/compose_controller.xml b/app/src/main/res/layout/compose_controller.xml deleted file mode 100644 index 617287296..000000000 --- a/app/src/main/res/layout/compose_controller.xml +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/app/src/main/res/layout/navigation_view_autocomplete.xml b/app/src/main/res/layout/navigation_view_autocomplete.xml deleted file mode 100644 index 36fd9ad2d..000000000 --- a/app/src/main/res/layout/navigation_view_autocomplete.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/navigation_view_checkbox.xml b/app/src/main/res/layout/navigation_view_checkbox.xml deleted file mode 100755 index 882a7d85d..000000000 --- a/app/src/main/res/layout/navigation_view_checkbox.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/navigation_view_checkedtext.xml b/app/src/main/res/layout/navigation_view_checkedtext.xml deleted file mode 100755 index 70b8729f7..000000000 --- a/app/src/main/res/layout/navigation_view_checkedtext.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/navigation_view_group.xml b/app/src/main/res/layout/navigation_view_group.xml deleted file mode 100755 index 8c3c90303..000000000 --- a/app/src/main/res/layout/navigation_view_group.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/navigation_view_radio.xml b/app/src/main/res/layout/navigation_view_radio.xml deleted file mode 100755 index 4f8d00e9d..000000000 --- a/app/src/main/res/layout/navigation_view_radio.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/navigation_view_spinner.xml b/app/src/main/res/layout/navigation_view_spinner.xml deleted file mode 100755 index 3d8d35efd..000000000 --- a/app/src/main/res/layout/navigation_view_spinner.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/navigation_view_text.xml b/app/src/main/res/layout/navigation_view_text.xml deleted file mode 100755 index 3b18c5607..000000000 --- a/app/src/main/res/layout/navigation_view_text.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/source_filter_mangadex_header.xml b/app/src/main/res/layout/source_filter_mangadex_header.xml deleted file mode 100644 index 8529a1ce1..000000000 --- a/app/src/main/res/layout/source_filter_mangadex_header.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/source_filter_sheet.xml b/app/src/main/res/layout/source_filter_sheet.xml deleted file mode 100644 index c6328d606..000000000 --- a/app/src/main/res/layout/source_filter_sheet.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - -