Migrate source filter sheet to Compose (#9135)
(cherry picked from commit 92132c59f5417ef81a7bbba6849be849282fc25e) # Conflicts: # app/src/main/java/eu/kanade/presentation/components/SettingsItems.kt # app/src/main/java/eu/kanade/presentation/library/LibrarySettingsDialog.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/GroupItem.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/filter/SectionItems.kt # app/src/main/java/eu/kanade/tachiyomi/widget/SimpleNavigationView.kt # app/src/main/java/eu/kanade/tachiyomi/widget/listener/IgnoreFirstSpinnerListener.kt # app/src/main/res/drawable/empty_drawable_32dp.xml # app/src/main/res/drawable/ic_check_box_24dp.xml # app/src/main/res/drawable/ic_check_box_outline_blank_24dp.xml # app/src/main/res/drawable/ic_check_box_x_24dp.xml # app/src/main/res/layout/navigation_view_checkbox.xml # app/src/main/res/layout/navigation_view_checkedtext.xml # app/src/main/res/layout/navigation_view_group.xml # app/src/main/res/layout/navigation_view_radio.xml # app/src/main/res/layout/navigation_view_spinner.xml # app/src/main/res/layout/navigation_view_text.xml # app/src/main/res/layout/source_filter_sheet.xml
This commit is contained in:
parent
1a5bf9f763
commit
314a740906
@ -84,7 +84,8 @@ fun SourceFeedScreen(
|
||||
name: String,
|
||||
isLoading: Boolean,
|
||||
items: List<SourceFeedUI>,
|
||||
onFabClick: (() -> Unit)?,
|
||||
hasFilters: Boolean,
|
||||
onFabClick: () -> Unit,
|
||||
onClickBrowse: () -> Unit,
|
||||
onClickLatest: () -> Unit,
|
||||
onClickSavedSearch: (SavedSearch) -> Unit,
|
||||
@ -107,8 +108,8 @@ fun SourceFeedScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
BrowseSourceFloatingActionButton(
|
||||
isVisible = onFabClick != null,
|
||||
onFabClick = onFabClick ?: {},
|
||||
isVisible = hasFilters,
|
||||
onFabClick = onFabClick,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
|
@ -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,
|
||||
|
@ -1,53 +1,32 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material.icons.rounded.CheckBox
|
||||
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
|
||||
import androidx.compose.material.icons.rounded.DisabledByDefault
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
|
||||
@Composable
|
||||
fun HeadingItem(
|
||||
@StringRes labelRes: Int,
|
||||
) {
|
||||
HeadingItem(stringResource(labelRes))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeadingItem(
|
||||
text: String,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.header,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
|
||||
@Composable
|
||||
fun TriStateItem(
|
||||
@ -69,7 +48,7 @@ fun TriStateItem(
|
||||
},
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
@ -100,120 +79,50 @@ fun TriStateItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SortItem(
|
||||
fun SelectItem(
|
||||
label: String,
|
||||
sortDescending: Boolean?,
|
||||
onClick: () -> Unit,
|
||||
options: Array<out Any?>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
) {
|
||||
val arrowIcon = when (sortDescending) {
|
||||
true -> Icons.Default.ArrowDownward
|
||||
false -> Icons.Default.ArrowUpward
|
||||
null -> null
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
) {
|
||||
if (arrowIcon != null) {
|
||||
Icon(
|
||||
imageVector = arrowIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckboxItem(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@Composable
|
||||
fun IconItem(
|
||||
label: String,
|
||||
icon: Painter,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = label,
|
||||
tint = if (selected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
label = { Text(text = label) },
|
||||
value = options[selectedIndex].toString(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded,
|
||||
)
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
options.forEachIndexed { index, text ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(text.toString()) },
|
||||
onClick = {
|
||||
onSelect(index)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,199 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.InputChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
|
||||
|
||||
@Composable
|
||||
fun AutoCompleteItem(
|
||||
name: String,
|
||||
state: List<String>,
|
||||
hint: String,
|
||||
values: List<String>,
|
||||
skipAutoFillTags: List<String>,
|
||||
validPrefixes: List<String>,
|
||||
onChange: (List<String>) -> Unit,
|
||||
) {
|
||||
val newState = remember { state.toMutableStateList() }
|
||||
DisposableEffect(newState) {
|
||||
onChange(newState)
|
||||
onDispose {}
|
||||
}
|
||||
Column(
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
) {
|
||||
AutoCompleteTextField(
|
||||
values = values,
|
||||
label = name,
|
||||
placeholder = hint,
|
||||
onValueFilter = { tag ->
|
||||
val prefix = validPrefixes.find { tag.startsWith(it) }
|
||||
val tagNoPrefix = if (prefix != null) {
|
||||
tag.removePrefix(prefix)
|
||||
} else {
|
||||
tag
|
||||
}
|
||||
|
||||
{ it.contains(tagNoPrefix, true) }
|
||||
},
|
||||
onSubmit = { tag ->
|
||||
val tagNoPrefix = validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
|
||||
if (tagNoPrefix !in skipAutoFillTags) {
|
||||
newState += tag
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
newState.forEach {
|
||||
InputChip(
|
||||
selected = false,
|
||||
onClick = {
|
||||
newState -= it
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(Icons.Default.Close, contentDescription = it)
|
||||
},
|
||||
colors = InputChipDefaults.inputChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoCompleteTextField(
|
||||
label: String? = null,
|
||||
placeholder: String? = null,
|
||||
values: List<String>,
|
||||
onValueFilter: ((String) -> ((String) -> Boolean)),
|
||||
onSubmit: (String) -> Boolean,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var value by remember { mutableStateOf(TextFieldValue("")) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
fun submit() {
|
||||
if (onSubmit(value.text)) {
|
||||
focusManager.clearFocus()
|
||||
value = TextFieldValue("")
|
||||
}
|
||||
}
|
||||
BackHandler(expanded) {
|
||||
focusManager.clearFocus()
|
||||
expanded = false
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
label = if (label != null) {
|
||||
{ Text(label) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
placeholder = if (placeholder != null) {
|
||||
{ Text(placeholder) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.runOnEnterKeyPressed { submit() },
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions { submit() },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded,
|
||||
)
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
)
|
||||
|
||||
val filteredValues by produceState(emptyList(), value) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val filter = onValueFilter(value.text)
|
||||
this@produceState.value = values.asSequence().filter(filter).take(100).toList()
|
||||
}
|
||||
}
|
||||
if (value.text.length > 2 && filteredValues.isNotEmpty()) {
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier
|
||||
.exposedDropdownSize(matchTextFieldWidth = true),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
filteredValues.fastForEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(it) },
|
||||
onClick = {
|
||||
value = TextFieldValue(it, TextRange(it.length))
|
||||
submit()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.runtime.Immutable
|
||||
@ -14,8 +13,6 @@ import androidx.paging.filter
|
||||
import androidx.paging.map
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.domain.UnsortedPreferences
|
||||
import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||
@ -33,6 +30,7 @@ import eu.kanade.domain.source.interactor.InsertSavedSearch
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||
@ -42,25 +40,11 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoComplete
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.AutoCompleteSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.CheckboxSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.GroupItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.HeaderItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SelectItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SelectSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SeparatorItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SortGroup
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.SortItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TextItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
|
||||
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.util.removeCovers
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -132,6 +116,7 @@ open class BrowseSourceScreenModel(
|
||||
|
||||
// SY -->
|
||||
unsortedPreferences: UnsortedPreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
private val getFlatMetadataById: GetFlatMetadataById = Injekt.get(),
|
||||
private val deleteSavedSearchById: DeleteSavedSearchById = Injekt.get(),
|
||||
private val insertSavedSearch: InsertSavedSearch = Injekt.get(),
|
||||
@ -148,7 +133,11 @@ open class BrowseSourceScreenModel(
|
||||
// SY -->
|
||||
val ehentaiBrowseDisplayMode by unsortedPreferences.enhancedEHentaiView().asState(coroutineScope)
|
||||
|
||||
val startExpanded by uiPreferences.expandFilters().asState(coroutineScope)
|
||||
|
||||
private val filterSerializer = FilterSerializer()
|
||||
|
||||
val sourceIsMangaDex = sourceId in mangaDexSourceIds
|
||||
// SY <--
|
||||
|
||||
init {
|
||||
@ -189,22 +178,18 @@ open class BrowseSourceScreenModel(
|
||||
|
||||
if (source is CatalogueSource) {
|
||||
getExhSavedSearch.subscribe(source.id, source::getFilterList)
|
||||
.map { it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, EXHSavedSearch::name)) }
|
||||
.onEach { savedSearches ->
|
||||
mutableState.update { it.copy(savedSearches = savedSearches) }
|
||||
withUIContext {
|
||||
/*withUIContext {
|
||||
filterSheet?.setSavedSearches(savedSearches)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
/**
|
||||
* Sheet containing filter items.
|
||||
*/
|
||||
private var filterSheet: SourceFilterSheet? = null
|
||||
|
||||
/**
|
||||
* Flow of Pager flow tied to [State.listing]
|
||||
*/
|
||||
@ -265,6 +250,16 @@ open class BrowseSourceScreenModel(
|
||||
mutableState.update { it.copy(listing = listing) }
|
||||
}
|
||||
|
||||
fun setFilters(filters: FilterList) {
|
||||
if (source !is CatalogueSource) return
|
||||
|
||||
mutableState.update {
|
||||
it.copy(
|
||||
filters = filters,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String? = null, filters: FilterList? = null) {
|
||||
if (source !is CatalogueSource) return
|
||||
// SY -->
|
||||
@ -450,7 +445,7 @@ open class BrowseSourceScreenModel(
|
||||
return getDuplicateLibraryManga.await(manga.title)
|
||||
}
|
||||
|
||||
fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
|
||||
private fun moveMangaToCategories(manga: Manga, vararg categories: Category) {
|
||||
moveMangaToCategories(manga, categories.filter { it.id != 0L }.map { it.id })
|
||||
}
|
||||
|
||||
@ -464,7 +459,7 @@ open class BrowseSourceScreenModel(
|
||||
}
|
||||
|
||||
fun openFilterSheet() {
|
||||
filterSheet?.show()
|
||||
setDialog(Dialog.Filter)
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
@ -475,66 +470,6 @@ open class BrowseSourceScreenModel(
|
||||
mutableState.update { it.copy(toolbarQuery = query) }
|
||||
}
|
||||
|
||||
open fun initFilterSheet(context: Context, navigator: Navigator) {
|
||||
source as? CatalogueSource ?: return
|
||||
val state = state.value
|
||||
/*if (state.filters.isEmpty()) {
|
||||
return
|
||||
}*/
|
||||
|
||||
filterSheet = SourceFilterSheet(
|
||||
context = context,
|
||||
// SY -->
|
||||
navigator = navigator,
|
||||
source = source,
|
||||
searches = state.savedSearches,
|
||||
// SY <--
|
||||
onFilterClicked = { search(filters = state.filters) },
|
||||
onResetClicked = {
|
||||
resetFilters()
|
||||
filterSheet?.setFilters(state.filterItems)
|
||||
},
|
||||
// EXH -->
|
||||
onSaveClicked = {
|
||||
coroutineScope.launchIO {
|
||||
val names = loadSearches().map { it.name }
|
||||
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearch(names)) }
|
||||
}
|
||||
},
|
||||
onSavedSearchClicked = { idOfSearch ->
|
||||
coroutineScope.launchIO {
|
||||
val search = loadSearch(idOfSearch)
|
||||
|
||||
if (search == null) {
|
||||
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
if (search.filterList == null && state.filters.isNotEmpty()) {
|
||||
withUIContext {
|
||||
context.toast(R.string.save_search_invalid)
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val allDefault = search.filterList != null && state.filters == source.getFilterList()
|
||||
filterSheet?.dismiss()
|
||||
|
||||
search(
|
||||
query = search.query,
|
||||
filters = if (allDefault) null else search.filterList,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSavedSearchDeleteClicked = { idToDelete, name ->
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(idToDelete, name)) }
|
||||
},
|
||||
// EXH <--
|
||||
)
|
||||
|
||||
filterSheet?.setFilters(state.filterItems)
|
||||
}
|
||||
|
||||
sealed class Listing(open val query: String?, open val filters: FilterList) {
|
||||
object Popular : Listing(query = GetRemoteManga.QUERY_POPULAR, filters = FilterList())
|
||||
object Latest : Listing(query = GetRemoteManga.QUERY_LATEST, filters = FilterList())
|
||||
@ -552,6 +487,7 @@ open class BrowseSourceScreenModel(
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object Filter : Dialog()
|
||||
data class RemoveManga(val manga: Manga) : Dialog()
|
||||
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog()
|
||||
data class ChangeMangaCategory(
|
||||
@ -561,7 +497,6 @@ open class BrowseSourceScreenModel(
|
||||
data class Migrate(val newManga: Manga) : Dialog()
|
||||
|
||||
// SY -->
|
||||
object FailedToLoadSavedSearch : Dialog()
|
||||
data class DeleteSavedSearch(val idToDelete: Long, val name: String) : Dialog()
|
||||
data class CreateSavedSearch(val currentSavedSearches: List<String>) : Dialog()
|
||||
// SY <--
|
||||
@ -575,13 +510,46 @@ open class BrowseSourceScreenModel(
|
||||
val dialog: Dialog? = null,
|
||||
// SY -->
|
||||
val savedSearches: List<EXHSavedSearch> = emptyList(),
|
||||
val filterable: Boolean = true,
|
||||
// SY <--
|
||||
) {
|
||||
val filterItems get() = filters.toItems()
|
||||
val isUserQuery get() = listing is Listing.Search && !listing.query.isNullOrEmpty()
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun onSaveSearch() {
|
||||
coroutineScope.launchIO {
|
||||
val names = state.value.savedSearches.map { it.name }
|
||||
mutableState.update { it.copy(dialog = Dialog.CreateSavedSearch(names)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onSavedSearch(
|
||||
search: EXHSavedSearch,
|
||||
onToast: (Int) -> Unit,
|
||||
) {
|
||||
coroutineScope.launchIO {
|
||||
if (search.filterList == null && state.value.filters.isNotEmpty()) {
|
||||
withUIContext {
|
||||
onToast(R.string.save_search_invalid)
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val allDefault = search.filterList != null && search.filterList == (source as? CatalogueSource)?.getFilterList()
|
||||
setDialog(null)
|
||||
|
||||
search(
|
||||
query = search.query,
|
||||
filters = if (allDefault) null else search.filterList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSavedSearchPress(search: EXHSavedSearch) {
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteSavedSearch(search.id, search.name)) }
|
||||
}
|
||||
|
||||
fun saveSearch(
|
||||
name: String,
|
||||
) {
|
||||
@ -607,56 +575,12 @@ open class BrowseSourceScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSearch(searchId: Long): EXHSavedSearch? {
|
||||
if (source !is CatalogueSource) return null
|
||||
return getExhSavedSearch.awaitOne(searchId, source::getFilterList)
|
||||
}
|
||||
|
||||
suspend fun loadSearches(): List<EXHSavedSearch> {
|
||||
if (source !is CatalogueSource) return emptyList()
|
||||
return getExhSavedSearch.await(source.id, source::getFilterList)
|
||||
fun onMangaDexRandom(onRandomFound: (String) -> Unit) {
|
||||
coroutineScope.launchIO {
|
||||
val random = source.getMainSource<MangaDex>()?.fetchRandomMangaUrl()
|
||||
?: return@launchIO
|
||||
onRandomFound(random)
|
||||
}
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
|
||||
fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull { filter ->
|
||||
when (filter) {
|
||||
// --> EXH
|
||||
is SourceModelFilter.AutoComplete -> AutoComplete(filter)
|
||||
// <-- EXH
|
||||
is SourceModelFilter.Header -> HeaderItem(filter)
|
||||
is SourceModelFilter.Separator -> SeparatorItem(filter)
|
||||
is SourceModelFilter.CheckBox -> CheckboxItem(filter)
|
||||
is SourceModelFilter.TriState -> TriStateItem(filter)
|
||||
is SourceModelFilter.Text -> TextItem(filter)
|
||||
is SourceModelFilter.Select<*> -> SelectItem(filter)
|
||||
is SourceModelFilter.Group<*> -> {
|
||||
val group = GroupItem(filter)
|
||||
val subItems = filter.state.mapNotNull {
|
||||
when (it) {
|
||||
is SourceModelFilter.CheckBox -> CheckboxSectionItem(it)
|
||||
is SourceModelFilter.TriState -> TriStateSectionItem(it)
|
||||
is SourceModelFilter.Text -> TextSectionItem(it)
|
||||
is SourceModelFilter.Select<*> -> SelectSectionItem(it)
|
||||
// SY -->
|
||||
is SourceModelFilter.AutoComplete -> AutoCompleteSectionItem(it)
|
||||
// SY <--
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
is SourceModelFilter.Sort -> {
|
||||
val group = SortGroup(filter)
|
||||
val subItems = filter.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.SuggestionChip
|
||||
import eu.kanade.presentation.components.SuggestionChipDefaults
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.source.model.EXHSavedSearch
|
||||
import tachiyomi.presentation.core.components.SettingsItemsPaddings
|
||||
|
||||
@Composable
|
||||
fun SavedSearchItem(
|
||||
savedSearches: List<EXHSavedSearch>,
|
||||
onSavedSearch: (EXHSavedSearch) -> Unit,
|
||||
onSavedSearchPress: (EXHSavedSearch) -> Unit,
|
||||
) {
|
||||
if (savedSearches.isEmpty()) return
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = SettingsItemsPaddings.Horizontal,
|
||||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.saved_searches),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
savedSearches.forEach {
|
||||
SuggestionChip(
|
||||
onClick = { onSavedSearch(it) },
|
||||
onLongClick = { onSavedSearchPress(it) },
|
||||
label = {
|
||||
Text(
|
||||
text = it.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
colors = SuggestionChipDefaults.suggestionChipColors(
|
||||
containerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
labelColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.databinding.SourceFilterSheetSavedSearchesBinding
|
||||
|
||||
class SavedSearchesAdapter(var chips: List<Chip> = emptyList()) :
|
||||
RecyclerView.Adapter<SavedSearchesAdapter.SavedSearchesViewHolder>() {
|
||||
|
||||
private lateinit var binding: SourceFilterSheetSavedSearchesBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
|
||||
binding = SourceFilterSheetSavedSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SavedSearchesViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
|
||||
holder.bind(chips)
|
||||
}
|
||||
|
||||
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(chips: List<Chip> = emptyList()) {
|
||||
binding.savedSearches.removeAllViews()
|
||||
if (chips.isEmpty()) {
|
||||
binding.savedSearchesTitle.isVisible = false
|
||||
} else {
|
||||
binding.savedSearchesTitle.isVisible = true
|
||||
chips.forEach {
|
||||
binding.savedSearches.addView(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.presentation.components.SelectItem
|
||||
import eu.kanade.presentation.components.TriStateItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.widget.TriState
|
||||
import eu.kanade.tachiyomi.widget.toTriStateFilter
|
||||
import tachiyomi.domain.source.model.EXHSavedSearch
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.CollapsibleBox
|
||||
import tachiyomi.presentation.core.components.HeadingItem
|
||||
import tachiyomi.presentation.core.components.LazyColumn
|
||||
import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.TextItem
|
||||
import tachiyomi.presentation.core.components.material.Button
|
||||
import tachiyomi.presentation.core.components.material.Divider
|
||||
|
||||
@Composable
|
||||
fun SourceFilterDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
filters: FilterList,
|
||||
onReset: () -> Unit,
|
||||
onFilter: () -> Unit,
|
||||
onUpdate: (FilterList) -> Unit,
|
||||
// SY -->
|
||||
startExpanded: Boolean,
|
||||
savedSearches: List<EXHSavedSearch>,
|
||||
onSave: () -> Unit,
|
||||
onSavedSearch: (EXHSavedSearch) -> Unit,
|
||||
onSavedSearchPress: (EXHSavedSearch) -> Unit,
|
||||
openMangaDexRandom: (() -> Unit)?,
|
||||
openMangaDexFollows: (() -> Unit)?,
|
||||
// SY <--
|
||||
) {
|
||||
val updateFilters = { onUpdate(filters) }
|
||||
|
||||
AdaptiveSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) { contentPadding ->
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
stickyHeader {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
TextButton(onClick = onReset) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_reset),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// SY -->
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
Icons.Default.Save,
|
||||
contentDescription = stringResource(R.string.action_save),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
Button(onClick = onFilter) {
|
||||
Text(stringResource(R.string.action_filter))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (openMangaDexRandom != null && openMangaDexFollows != null) {
|
||||
item {
|
||||
MangaDexFilterHeader(
|
||||
openMangaDexRandom = openMangaDexRandom,
|
||||
openMangaDexFollows = openMangaDexFollows,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SavedSearchItem(
|
||||
savedSearches = savedSearches,
|
||||
onSavedSearch = onSavedSearch,
|
||||
onSavedSearchPress = onSavedSearchPress,
|
||||
)
|
||||
}
|
||||
|
||||
items(filters) {
|
||||
FilterItem(it, updateFilters /* SY --> */, startExpanded /* SY <-- */)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterItem(filter: Filter<*>, onUpdate: () -> Unit/* SY --> */, startExpanded: Boolean /* SY <-- */) {
|
||||
when (filter) {
|
||||
// SY -->
|
||||
is Filter.AutoComplete -> {
|
||||
AutoCompleteItem(
|
||||
name = filter.name,
|
||||
state = filter.state,
|
||||
hint = filter.hint,
|
||||
values = filter.values,
|
||||
skipAutoFillTags = filter.skipAutoFillTags,
|
||||
validPrefixes = filter.validPrefixes,
|
||||
) {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
is Filter.Header -> {
|
||||
HeadingItem(filter.name)
|
||||
}
|
||||
is Filter.Separator -> {
|
||||
Divider()
|
||||
}
|
||||
is Filter.CheckBox -> {
|
||||
CheckboxItem(
|
||||
label = filter.name,
|
||||
checked = filter.state,
|
||||
) {
|
||||
filter.state = !filter.state
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
is Filter.TriState -> {
|
||||
TriStateItem(
|
||||
label = filter.name,
|
||||
state = filter.state.toTriStateFilter(),
|
||||
) {
|
||||
filter.state = TriState.valueOf(filter.state).next().value
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
is Filter.Text -> {
|
||||
TextItem(
|
||||
label = filter.name,
|
||||
value = filter.state,
|
||||
) {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
is Filter.Select<*> -> {
|
||||
SelectItem(
|
||||
label = filter.name,
|
||||
options = filter.values,
|
||||
selectedIndex = filter.state,
|
||||
) {
|
||||
filter.state = it
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
CollapsibleBox(
|
||||
heading = filter.name,
|
||||
// SY -->
|
||||
startExpanded = startExpanded,
|
||||
// SY <--
|
||||
) {
|
||||
Column {
|
||||
filter.values.mapIndexed { index, item ->
|
||||
SortItem(
|
||||
label = item,
|
||||
sortDescending = filter.state?.ascending?.not()
|
||||
?.takeIf { index == filter.state?.index },
|
||||
) {
|
||||
val ascending = if (index == filter.state?.index) {
|
||||
!filter.state!!.ascending
|
||||
} else {
|
||||
filter.state!!.ascending
|
||||
}
|
||||
filter.state = Filter.Sort.Selection(
|
||||
index = index,
|
||||
ascending = ascending,
|
||||
)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is Filter.Group<*> -> {
|
||||
CollapsibleBox(
|
||||
heading = filter.name,
|
||||
// SY -->
|
||||
startExpanded = startExpanded,
|
||||
// SY <--
|
||||
) {
|
||||
Column {
|
||||
filter.state
|
||||
.filterIsInstance<Filter<*>>()
|
||||
.map { FilterItem(filter = it, onUpdate = onUpdate /* SY --> */, startExpanded /* SY <-- */) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.browse
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
|
||||
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.source.getMainSource
|
||||
import tachiyomi.domain.source.model.EXHSavedSearch
|
||||
|
||||
class SourceFilterSheet(
|
||||
context: Context,
|
||||
// SY -->
|
||||
navigator: Navigator,
|
||||
source: CatalogueSource,
|
||||
searches: List<EXHSavedSearch> = emptyList(),
|
||||
// SY <--
|
||||
private val onFilterClicked: () -> Unit,
|
||||
private val onResetClicked: () -> Unit,
|
||||
// EXH -->
|
||||
private val onSaveClicked: () -> Unit,
|
||||
var onSavedSearchClicked: (Long) -> Unit = {},
|
||||
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> },
|
||||
// EXH <--
|
||||
) : BaseBottomSheetDialog(context) {
|
||||
|
||||
private var filterNavView: FilterNavigationView = FilterNavigationView(
|
||||
context,
|
||||
// SY -->
|
||||
searches = searches,
|
||||
source = source,
|
||||
navigator = navigator,
|
||||
dismissSheet = ::dismiss,
|
||||
// SY <--
|
||||
)
|
||||
|
||||
override fun createView(inflater: LayoutInflater): View {
|
||||
filterNavView.onFilterClicked = {
|
||||
onFilterClicked()
|
||||
this.dismiss()
|
||||
}
|
||||
filterNavView.onResetClicked = onResetClicked
|
||||
|
||||
// EXH -->
|
||||
filterNavView.onSaveClicked = onSaveClicked
|
||||
|
||||
filterNavView.onSavedSearchClicked = onSavedSearchClicked
|
||||
|
||||
filterNavView.onSavedSearchDeleteClicked = onSavedSearchDeleteClicked
|
||||
// EXH <--
|
||||
|
||||
return filterNavView
|
||||
}
|
||||
|
||||
fun setFilters(items: List<IFlexible<*>>) {
|
||||
filterNavView.adapter.updateDataSet(items)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun setSavedSearches(searches: List<EXHSavedSearch>) {
|
||||
filterNavView.setSavedSearches(searches)
|
||||
}
|
||||
|
||||
fun hideFilterButton() {
|
||||
filterNavView.hideFilterButton()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
class FilterNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
// SY -->
|
||||
searches: List<EXHSavedSearch> = emptyList(),
|
||||
source: CatalogueSource? = null,
|
||||
navigator: Navigator? = null,
|
||||
dismissSheet: (() -> Unit)? = null,
|
||||
// SY <--
|
||||
) :
|
||||
SimpleNavigationView(context, attrs) {
|
||||
|
||||
var onFilterClicked = {}
|
||||
var onResetClicked = {}
|
||||
|
||||
// SY -->
|
||||
var onSaveClicked = {}
|
||||
|
||||
var onSavedSearchClicked: (Long) -> Unit = {}
|
||||
|
||||
var onSavedSearchDeleteClicked: (Long, String) -> Unit = { _, _ -> }
|
||||
|
||||
private val savedSearchesAdapter = SavedSearchesAdapter(getSavedSearchesChips(searches))
|
||||
// SY <--
|
||||
|
||||
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
|
||||
.setDisplayHeadersAtStartUp(true)
|
||||
|
||||
private val binding = SourceFilterSheetBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
null,
|
||||
false,
|
||||
)
|
||||
|
||||
init {
|
||||
// SY -->
|
||||
recycler.adapter = ConcatAdapter(
|
||||
listOfNotNull(
|
||||
navigator?.let {
|
||||
source?.getMainSource<MangaDex>()
|
||||
?.let {
|
||||
MangaDexFabHeaderAdapter(navigator, it) {
|
||||
dismissSheet?.invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
savedSearchesAdapter,
|
||||
adapter,
|
||||
),
|
||||
)
|
||||
// SY <--
|
||||
recycler.setHasFixedSize(true)
|
||||
(binding.root.getChildAt(1) as ViewGroup).addView(recycler)
|
||||
addView(binding.root)
|
||||
// SY -->
|
||||
binding.saveSearchBtn.setOnClickListener { onSaveClicked() }
|
||||
// SY <--
|
||||
binding.filterBtn.setOnClickListener { onFilterClicked() }
|
||||
binding.resetBtn.setOnClickListener { onResetClicked() }
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun setSavedSearches(searches: List<EXHSavedSearch>) {
|
||||
savedSearchesAdapter.chips = getSavedSearchesChips(searches)
|
||||
savedSearchesAdapter.notifyItemChanged(0)
|
||||
}
|
||||
|
||||
private fun getSavedSearchesChips(searches: List<EXHSavedSearch>): List<Chip> {
|
||||
return searches
|
||||
.map { search ->
|
||||
Chip(context).apply {
|
||||
text = search.name
|
||||
setOnClickListener { onSavedSearchClicked(search.id) }
|
||||
setOnLongClickListener {
|
||||
onSavedSearchDeleteClicked(search.id, search.name); true
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.text.toString() })
|
||||
}
|
||||
|
||||
fun hideFilterButton() {
|
||||
binding.filterBtn.isVisible = false
|
||||
}
|
||||
// EXH <--
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -3,10 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.source.feed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.copyFrom
|
||||
import eu.kanade.domain.manga.model.toDomainManga
|
||||
@ -17,11 +18,16 @@ import eu.kanade.domain.source.interactor.GetExhSavedSearch
|
||||
import eu.kanade.domain.source.interactor.GetFeedSavedSearchBySourceId
|
||||
import eu.kanade.domain.source.interactor.GetSavedSearchBySourceIdFeed
|
||||
import eu.kanade.domain.source.interactor.InsertFeedSavedSearch
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.browse.SourceFeedUI
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.toItems
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import exh.source.getMainSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@ -32,16 +38,20 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.interactor.GetManga
|
||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||
import tachiyomi.domain.source.model.EXHSavedSearch
|
||||
import tachiyomi.domain.source.model.FeedSavedSearch
|
||||
import tachiyomi.domain.source.model.SavedSearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -52,6 +62,7 @@ import tachiyomi.domain.manga.model.Manga as DomainManga
|
||||
|
||||
open class SourceFeedScreenModel(
|
||||
val sourceId: Long,
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||
@ -66,11 +77,20 @@ open class SourceFeedScreenModel(
|
||||
|
||||
val source = sourceManager.getOrStub(sourceId) as CatalogueSource
|
||||
|
||||
val sourceIsMangaDex = sourceId in mangaDexSourceIds
|
||||
|
||||
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||
|
||||
val startExpanded by uiPreferences.expandFilters().asState(coroutineScope)
|
||||
|
||||
init {
|
||||
setFilters(source.getFilterList())
|
||||
|
||||
coroutineScope.launchIO {
|
||||
val searches = loadSearches()
|
||||
mutableState.update { it.copy(savedSearches = searches) }
|
||||
}
|
||||
|
||||
getFeedSavedSearchBySourceId.subscribe(source.id)
|
||||
.onEach {
|
||||
val items = getSourcesToGetFeed(it)
|
||||
@ -88,7 +108,7 @@ open class SourceFeedScreenModel(
|
||||
mutableState.update { it.copy(filters = filters) }
|
||||
}
|
||||
|
||||
suspend fun hasTooManyFeeds(): Boolean {
|
||||
private suspend fun hasTooManyFeeds(): Boolean {
|
||||
return countFeedSavedSearchBySourceId.await(source.id) > 10
|
||||
}
|
||||
|
||||
@ -213,16 +233,84 @@ open class SourceFeedScreenModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSearch(searchId: Long) =
|
||||
getExhSavedSearch.awaitOne(searchId, source::getFilterList)
|
||||
|
||||
suspend fun loadSearches() =
|
||||
private suspend fun loadSearches() =
|
||||
getExhSavedSearch.await(source.id, source::getFilterList)
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, EXHSavedSearch::name))
|
||||
|
||||
fun onFilter(onBrowseClick: (query: String?, filters: String?) -> Unit) {
|
||||
coroutineScope.launchIO {
|
||||
val allDefault = state.value.filters == source.getFilterList()
|
||||
dismissDialog()
|
||||
if (allDefault) {
|
||||
onBrowseClick(
|
||||
state.value.searchQuery?.nullIfBlank(),
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
onBrowseClick(
|
||||
state.value.searchQuery?.nullIfBlank(),
|
||||
Json.encodeToString(filterSerializer.serialize(state.value.filters)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSavedSearch(
|
||||
search: EXHSavedSearch,
|
||||
onBrowseClick: (query: String?, searchId: Long) -> Unit,
|
||||
onToast: (Int) -> Unit,
|
||||
) {
|
||||
coroutineScope.launchIO {
|
||||
if (search.filterList == null && state.value.filters.isNotEmpty()) {
|
||||
withUIContext {
|
||||
onToast(R.string.save_search_invalid)
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
|
||||
val allDefault = search.filterList != null && search.filterList == source.getFilterList()
|
||||
dismissDialog()
|
||||
|
||||
if (!allDefault) {
|
||||
onBrowseClick(
|
||||
state.value.searchQuery?.nullIfBlank(),
|
||||
search.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSavedSearchAddToFeed(
|
||||
search: EXHSavedSearch,
|
||||
onToast: (Int) -> Unit,
|
||||
) {
|
||||
coroutineScope.launchIO {
|
||||
if (hasTooManyFeeds()) {
|
||||
withUIContext {
|
||||
onToast(R.string.too_many_in_feed)
|
||||
}
|
||||
return@launchIO
|
||||
}
|
||||
openAddFeed(search.id, search.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaDexRandom(onRandomFound: (String) -> Unit) {
|
||||
coroutineScope.launchIO {
|
||||
val random = source.getMainSource<MangaDex>()?.fetchRandomMangaUrl()
|
||||
?: return@launchIO
|
||||
onRandomFound(random)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String?) {
|
||||
mutableState.update { it.copy(searchQuery = query) }
|
||||
}
|
||||
|
||||
fun openFilterSheet() {
|
||||
mutableState.update { it.copy(dialog = Dialog.Filter) }
|
||||
}
|
||||
|
||||
fun openDeleteFeed(feed: FeedSavedSearch) {
|
||||
mutableState.update { it.copy(dialog = Dialog.DeleteFeed(feed)) }
|
||||
}
|
||||
@ -231,18 +319,14 @@ open class SourceFeedScreenModel(
|
||||
mutableState.update { it.copy(dialog = Dialog.AddFeed(feedId, name)) }
|
||||
}
|
||||
|
||||
fun openFailedToLoadSavedSearch() {
|
||||
mutableState.update { it.copy(dialog = Dialog.FailedToLoadSavedSearch) }
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
object Filter : Dialog()
|
||||
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
|
||||
data class AddFeed(val feedId: Long, val name: String) : Dialog()
|
||||
object FailedToLoadSavedSearch : Dialog()
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
@ -256,10 +340,9 @@ data class SourceFeedState(
|
||||
val searchQuery: String? = null,
|
||||
val items: List<SourceFeedUI> = emptyList(),
|
||||
val filters: FilterList = FilterList(),
|
||||
val savedSearches: List<EXHSavedSearch> = emptyList(),
|
||||
val dialog: SourceFeedScreenModel.Dialog? = null,
|
||||
) {
|
||||
val filterItems: List<IFlexible<*>> by lazy { filters.toItems() }
|
||||
|
||||
val isLoading
|
||||
get() = items.isEmpty()
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
|
||||
import exh.log.xLogD
|
||||
|
||||
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_autocomplete
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
holder.text.text = "${filter.name}: "
|
||||
|
||||
holder.autoComplete.hint = filter.hint
|
||||
holder.autoComplete.setAdapter(
|
||||
AutoCompleteAdapter(
|
||||
holder.itemView.context,
|
||||
android.R.layout.simple_dropdown_item_1line,
|
||||
filter.values,
|
||||
filter.validPrefixes,
|
||||
),
|
||||
)
|
||||
holder.autoComplete.threshold = 3
|
||||
|
||||
// select from auto complete
|
||||
holder.autoComplete.setOnItemClickListener { adapterView, _, chipPosition, _ ->
|
||||
val tag = (adapterView.getItemAtPosition(chipPosition) as String).trim()
|
||||
val tagNoPrefix = filter.validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
|
||||
if (tagNoPrefix !in filter.skipAutoFillTags) {
|
||||
holder.autoComplete.text = null
|
||||
addTag(tag, holder)
|
||||
}
|
||||
}
|
||||
|
||||
// done keyboard button is pressed
|
||||
holder.autoComplete.setOnEditorActionListener { textView, actionId, _ ->
|
||||
if (actionId != EditorInfo.IME_ACTION_DONE) return@setOnEditorActionListener false
|
||||
val tag = textView.text.toString().trim()
|
||||
val tagNoPrefix = filter.validPrefixes.find { tag.startsWith(it) }?.let { tag.removePrefix(it).trim() } ?: tag
|
||||
if (tagNoPrefix !in filter.skipAutoFillTags) {
|
||||
textView.text = null
|
||||
addTag(tag, holder)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// space or comma is detected
|
||||
holder.autoComplete.addTextChangedListener {
|
||||
if (it == null || it.isEmpty()) {
|
||||
return@addTextChangedListener
|
||||
}
|
||||
|
||||
if (it.last() == ',') {
|
||||
val name = it.toString().dropLast(1).trim()
|
||||
addTag(name, holder)
|
||||
|
||||
holder.autoComplete.text = null
|
||||
}
|
||||
}
|
||||
|
||||
holder.mainTagChipGroup.removeAllViews()
|
||||
filter.state.forEach {
|
||||
addChipToGroup(it, holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as AutoComplete).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
private fun addTag(name: String, holder: Holder) {
|
||||
if (name.isNotEmpty() && !filter.state.contains(name)) {
|
||||
addChipToGroup(name, holder)
|
||||
filter.state += name
|
||||
} else {
|
||||
xLogD("Invalid tag: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addChipToGroup(name: String, holder: Holder) {
|
||||
val chip = Chip(holder.itemView.context)
|
||||
chip.text = name
|
||||
|
||||
chip.isClickable = true
|
||||
chip.isCheckable = false
|
||||
chip.isCloseIconVisible = true
|
||||
|
||||
holder.mainTagChipGroup.addView(chip)
|
||||
|
||||
chip.setOnCloseIconClickListener {
|
||||
holder.mainTagChipGroup.removeView(chip)
|
||||
filter.state -= name
|
||||
}
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
||||
val autoComplete: AutoCompleteTextView = itemView.findViewById(R.id.nav_view_item)
|
||||
val mainTagChipGroup: ChipGroup = itemView.findViewById(R.id.chip_group)
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_checkbox
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.check
|
||||
view.text = filter.name
|
||||
view.isChecked = filter.state
|
||||
holder.itemView.setOnClickListener {
|
||||
view.toggle()
|
||||
filter.state = view.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as CheckboxItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.ExpandableViewHolder
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
|
||||
|
||||
init {
|
||||
// --> EH
|
||||
isExpanded = Injekt.get<UiPreferences>().expandFilters().get()
|
||||
// <-- EH
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_group
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 101
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.title.text = filter.name
|
||||
|
||||
holder.icon.setVectorCompat(
|
||||
if (isExpanded) {
|
||||
R.drawable.ic_expand_less_24dp
|
||||
} else {
|
||||
R.drawable.ic_expand_more_24dp
|
||||
},
|
||||
)
|
||||
|
||||
holder.itemView.setOnClickListener(holder)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as GroupItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
|
||||
val title: TextView = itemView.findViewById(R.id.title)
|
||||
val icon: ImageView = itemView.findViewById(R.id.expand_icon)
|
||||
|
||||
override fun shouldNotifyParentOnClick(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.design_navigation_item_subheader
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.itemView as TextView
|
||||
view.text = filter.name
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as HeaderItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TriStateSectionItem
|
||||
if (head != other.head) return false
|
||||
return filter == other.filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<TextItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TextSectionItem
|
||||
if (head != other.head) return false
|
||||
return filter == other.filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISectionable<CheckboxItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CheckboxSectionItem
|
||||
if (head != other.head) return false
|
||||
return filter == other.filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISectionable<SelectItem.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SelectSectionItem
|
||||
if (head != other.head) return false
|
||||
return filter == other.filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode() + (head?.hashCode() ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
class AutoCompleteSectionItem(filter: Filter.AutoComplete) : AutoComplete(filter), ISectionable<AutoComplete.Holder, GroupItem> {
|
||||
|
||||
private var head: GroupItem? = null
|
||||
|
||||
override fun getHeader(): GroupItem? = head
|
||||
|
||||
override fun setHeader(header: GroupItem?) {
|
||||
head = header
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as AutoCompleteSectionItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
}
|
||||
// SY <--
|
@ -1,59 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.listener.IgnoreFirstSpinnerListener
|
||||
|
||||
open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_spinner
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.text.text = filter.name + ": "
|
||||
|
||||
val spinner = holder.spinner
|
||||
spinner.prompt = filter.name
|
||||
spinner.adapter = ArrayAdapter<Any>(
|
||||
holder.itemView.context,
|
||||
android.R.layout.simple_spinner_item,
|
||||
filter.values,
|
||||
).apply {
|
||||
setDropDownViewResource(R.layout.common_spinner_item)
|
||||
}
|
||||
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
|
||||
filter.state = pos
|
||||
}
|
||||
spinner.setSelection(filter.state)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SelectItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
|
||||
val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.design_navigation_item_separator
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SeparatorItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter)
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
||||
|
||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
|
||||
|
||||
init {
|
||||
isExpanded = false
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_group
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 100
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.title.text = filter.name
|
||||
|
||||
holder.icon.setVectorCompat(
|
||||
if (isExpanded) {
|
||||
R.drawable.ic_expand_less_24dp
|
||||
} else {
|
||||
R.drawable.ic_expand_more_24dp
|
||||
},
|
||||
)
|
||||
|
||||
holder.itemView.setOnClickListener(holder)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as SortGroup).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : GroupItem.Holder(view, adapter)
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_checkedtext
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 102
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.text
|
||||
view.text = name
|
||||
val filter = group.filter
|
||||
|
||||
val i = filter.values.indexOf(name)
|
||||
|
||||
fun getIcon() = when (filter.state) {
|
||||
Filter.Sort.Selection(i, false) ->
|
||||
AppCompatResources.getDrawable(view.context, R.drawable.ic_arrow_down_white_32dp)
|
||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||
Filter.Sort.Selection(i, true) ->
|
||||
AppCompatResources.getDrawable(view.context, R.drawable.ic_arrow_up_white_32dp)
|
||||
?.apply { setTint(view.context.getResourceColor(R.attr.colorAccent)) }
|
||||
else -> AppCompatResources.getDrawable(view.context, R.drawable.empty_drawable_32dp)
|
||||
}
|
||||
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
holder.itemView.setOnClickListener {
|
||||
val pre = filter.state?.index ?: i
|
||||
if (pre != i) {
|
||||
filter.state = Filter.Sort.Selection(i, false)
|
||||
} else {
|
||||
filter.state = Filter.Sort.Selection(i, filter.state?.ascending == false)
|
||||
}
|
||||
|
||||
group.subItems.forEach { adapter.notifyItemChanged(adapter.getGlobalPositionOf(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as SortItem
|
||||
return name == other.name && group == other.group
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + group.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.navigation_view_text
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
holder.wrapper.hint = filter.name
|
||||
holder.edit.setText(filter.state)
|
||||
holder.edit.doOnTextChanged { text, _, _, _ ->
|
||||
filter.state = text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TextItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
|
||||
val edit: EditText = itemView.findViewById(R.id.nav_view_item)
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.browse.source.filter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.R
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.R as TR
|
||||
|
||||
open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriStateItem.Holder>() {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return TR.layout.navigation_view_checkedtext
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return 103
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
|
||||
return Holder(view, adapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
|
||||
val view = holder.text
|
||||
view.text = filter.name
|
||||
|
||||
fun getIcon() = AppCompatResources.getDrawable(
|
||||
view.context,
|
||||
when (filter.state) {
|
||||
Filter.TriState.STATE_IGNORE -> TR.drawable.ic_check_box_outline_blank_24dp
|
||||
Filter.TriState.STATE_INCLUDE -> TR.drawable.ic_check_box_24dp
|
||||
Filter.TriState.STATE_EXCLUDE -> TR.drawable.ic_check_box_x_24dp
|
||||
else -> throw Exception("Unknown state")
|
||||
},
|
||||
)?.apply {
|
||||
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
|
||||
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
|
||||
} else {
|
||||
view.context.getResourceColor(R.attr.colorPrimary)
|
||||
}
|
||||
|
||||
setTint(color)
|
||||
}
|
||||
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
holder.itemView.setOnClickListener {
|
||||
filter.state = (filter.state + 1) % 3
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(getIcon(), null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return filter == (other as TriStateItem).filter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return filter.hashCode()
|
||||
}
|
||||
|
||||
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
|
||||
val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
|
||||
|
||||
init {
|
||||
// Align with native checkbox
|
||||
text.updatePadding(left = 4.dpToPx)
|
||||
text.compoundDrawablePadding = 20.dpToPx
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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<*>?) {
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package exh.md
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
|
||||
import exh.md.follows.MangaDexFollowsScreen
|
||||
import tachiyomi.core.util.lang.launchUI
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
||||
class MangaDexFabHeaderAdapter(val navigator: Navigator, val source: CatalogueSource, val onClick: () -> Unit) :
|
||||
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
|
||||
|
||||
private lateinit var binding: SourceFilterMangadexHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
|
||||
binding = SourceFilterMangadexHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SavedSearchesViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
|
||||
holder.bind()
|
||||
}
|
||||
|
||||
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
fun bind() {
|
||||
binding.mangadexFollows.setOnClickListener {
|
||||
navigator.replace(MangaDexFollowsScreen(source.id))
|
||||
onClick()
|
||||
}
|
||||
binding.mangadexRandom.setOnClickListener {
|
||||
launchUI {
|
||||
val randomMangaUrl = withIOContext {
|
||||
(source as? RandomMangaSource)?.fetchRandomMangaUrl()
|
||||
}
|
||||
navigator.replace(
|
||||
BrowseSourceScreen(
|
||||
source.id,
|
||||
"id:$randomMangaUrl",
|
||||
),
|
||||
)
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<size
|
||||
android:width="32dp"
|
||||
android:height="32dp" />
|
||||
</shape>
|
@ -1,15 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<group
|
||||
android:pivotX="32"
|
||||
android:pivotY="32"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z" />
|
||||
</group>
|
||||
</vector>
|
@ -1,15 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<group
|
||||
android:pivotX="32"
|
||||
android:pivotY="32"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z" />
|
||||
</group>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M10.6,16.2 L17.65,9.15 16.25,7.75 10.6,13.4 7.75,10.55 6.35,11.95ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21Z"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z" />
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
|
||||
</vector>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nav_view_item_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
tools:text="Filter:" />
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/nav_view_item"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:completionThreshold="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textCapWords" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chip_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:chipSpacingVertical="4dp"
|
||||
style="@style/Widget.Tachiyomi.Chip.Action"/>
|
||||
</LinearLayout>
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="false"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
tools:text="Title" />
|
||||
|
||||
</LinearLayout>
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
tools:text="Title" />
|
||||
|
||||
</LinearLayout>
|
@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Tachiyomi.SectionHeader"
|
||||
tools:text="Header" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/expand_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:tint="?attr/colorOnBackground"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</LinearLayout>
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="false"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
</LinearLayout>
|
@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nav_view_item_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingEnd="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Filter:" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
|
||||
</LinearLayout>
|
@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:baselineAligned="false"
|
||||
android:focusable="true"
|
||||
android:paddingStart="?attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nav_view_item_wrapper"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/mangadex_random"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/random"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/mangadex_follows"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/mangadex_follows"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@drawable/transparent_tabs_background"
|
||||
android:elevation="2dp"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="?attr/listPreferredItemPaddingStart">
|
||||
|
||||
<Button
|
||||
android:id="@+id/reset_btn"
|
||||
style="?attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_reset"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/save_search_btn"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:contentDescription="@string/action_save"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/filter_btn"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/reset_btn"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_save_black_24dp"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/filter_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_filter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_weight="1"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
</LinearLayout>
|
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/saved_searches_title"
|
||||
style="@style/Widget.Tachiyomi.Chip.Action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="visible"
|
||||
android:text="@string/saved_searches" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/saved_searches"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:chipSpacingHorizontal="4dp" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,59 @@
|
||||
package tachiyomi.presentation.core.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
|
||||
@Composable
|
||||
fun CollapsibleBox(
|
||||
heading: String,
|
||||
// SY -->
|
||||
startExpanded: Boolean = false,
|
||||
// SY <--
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(/* SY --> */startExpanded/* SY <-- */) }
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = heading,
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package tachiyomi.presentation.core.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
|
||||
object SettingsItemsPaddings {
|
||||
val Horizontal = 24.dp
|
||||
val Vertical = 10.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeadingItem(
|
||||
@StringRes labelRes: Int,
|
||||
) {
|
||||
HeadingItem(stringResource(labelRes))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeadingItem(
|
||||
text: String,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.header,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SortItem(
|
||||
label: String,
|
||||
sortDescending: Boolean?,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val arrowIcon = when (sortDescending) {
|
||||
true -> Icons.Default.ArrowDownward
|
||||
false -> Icons.Default.ArrowUpward
|
||||
null -> null
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
if (arrowIcon != null) {
|
||||
Icon(
|
||||
imageVector = arrowIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckboxItem(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextItem(
|
||||
label: String,
|
||||
value: String,
|
||||
onChange: (String) -> Unit,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 4.dp),
|
||||
label = { Text(text = label) },
|
||||
value = value,
|
||||
onValueChange = onChange,
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@Composable
|
||||
fun IconItem(
|
||||
label: String,
|
||||
icon: Painter,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = label,
|
||||
tint = if (selected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
// SY <--
|
Loading…
x
Reference in New Issue
Block a user