Last commit merged: 12e7ee9d0c
This commit is contained in:
LuftVerbot 2023-11-11 17:58:02 +01:00
parent 61256a22fd
commit e7b1066e3c
82 changed files with 1486 additions and 1496 deletions

View file

@ -3,24 +3,24 @@ package eu.kanade.domain.entries.anime.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.anime.model.Anime
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
val Anime.downloadedFilter: TriStateFilter
val Anime.downloadedFilter: TriState
get() {
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
if (forceDownloaded()) return TriState.ENABLED_IS
return when (downloadedFilterRaw) {
Anime.EPISODE_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
Anime.EPISODE_SHOW_DOWNLOADED -> TriState.ENABLED_IS
Anime.EPISODE_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
}
fun Anime.episodesFiltered(): Boolean {
return unseenFilter != TriStateFilter.DISABLED ||
downloadedFilter != TriStateFilter.DISABLED ||
bookmarkedFilter != TriStateFilter.DISABLED
return unseenFilter != TriState.DISABLED ||
downloadedFilter != TriState.DISABLED ||
bookmarkedFilter != TriState.DISABLED
}
fun Anime.forceDownloaded(): Boolean {
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()

View file

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

View file

@ -37,9 +37,6 @@ class SourcePreferences(
fun animeExtensionUpdatesCount() = preferenceStore.getInt("animeext_updates_count", 0)
fun mangaExtensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun searchPinnedAnimeSourcesOnly() = preferenceStore.getBoolean("search_pinned_anime_sources_only", false)
fun searchPinnedMangaSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
fun hideInAnimeLibraryItems() = preferenceStore.getBoolean("browse_hide_in_anime_library_items", false)
fun hideInMangaLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)

View file

@ -61,12 +61,12 @@ fun BrowseAnimeSourceContent(
if (animeList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh),
actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite,
)
when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> animeList.refresh()
SnackbarResult.ActionPerformed -> animeList.retry()
}
}
}

View file

@ -1,8 +1,23 @@
package eu.kanade.presentation.browse.anime
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.Divider
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -17,7 +32,8 @@ import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchState
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearchScreenModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.material.Scaffold
@ -25,10 +41,13 @@ import tachiyomi.presentation.core.components.material.padding
@Composable
fun GlobalAnimeSearchScreen(
state: GlobalAnimeSearchState,
state: GlobalAnimeSearchScreenModel.State,
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getAnime: @Composable (Anime) -> State<Anime>,
onClickSource: (AnimeCatalogueSource) -> Unit,
onClickItem: (Anime) -> Unit,
@ -36,6 +55,7 @@ fun GlobalAnimeSearchScreen(
) {
Scaffold(
topBar = { scrollBehavior ->
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
GlobalSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
@ -45,10 +65,68 @@ fun GlobalAnimeSearchScreen(
onSearch = onSearch,
scrollBehavior = scrollBehavior,
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
FilterChip(
selected = state.sourceFilter == AnimeSourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(AnimeSourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.pinned_sources))
},
)
FilterChip(
selected = state.sourceFilter == AnimeSourceFilter.All,
onClick = { onChangeSearchFilter(AnimeSourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.all))
},
)
FilterChip(
selected = state.onlyShowHasResults,
onClick = { onToggleResults() },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.has_results))
},
)
}
Divider()
}
},
) { paddingValues ->
GlobalAnimeSearchContent(
items = state.items,
items = items,
contentPadding = paddingValues,
getAnime = getAnime,
onClickSource = onClickSource,

View file

@ -61,12 +61,12 @@ fun BrowseSourceContent(
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
val result = snackbarHostState.showSnackbar(
message = getErrorMessage(errorState),
actionLabel = context.getString(R.string.action_webview_refresh),
actionLabel = context.getString(R.string.action_retry),
duration = SnackbarDuration.Indefinite,
)
when (result) {
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
SnackbarResult.ActionPerformed -> mangaList.refresh()
SnackbarResult.ActionPerformed -> mangaList.retry()
}
}
}

View file

@ -1,8 +1,23 @@
package eu.kanade.presentation.browse.manga
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.DoneAll
import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material3.Divider
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -16,8 +31,9 @@ import eu.kanade.presentation.browse.GlobalSearchToolbar
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchState
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold
@ -25,10 +41,13 @@ import tachiyomi.presentation.core.components.material.padding
@Composable
fun GlobalMangaSearchScreen(
state: GlobalMangaSearchState,
state: GlobalMangaSearchScreenModel.State,
items: Map<CatalogueSource, MangaSearchItemResult>,
navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit,
onChangeSearchFilter: (MangaSourceFilter) -> Unit,
onToggleResults: () -> Unit,
getManga: @Composable (Manga) -> State<Manga>,
onClickSource: (CatalogueSource) -> Unit,
onClickItem: (Manga) -> Unit,
@ -36,6 +55,7 @@ fun GlobalMangaSearchScreen(
) {
Scaffold(
topBar = { scrollBehavior ->
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
GlobalSearchToolbar(
searchQuery = state.searchQuery,
progress = state.progress,
@ -45,10 +65,68 @@ fun GlobalMangaSearchScreen(
onSearch = onSearch,
scrollBehavior = scrollBehavior,
)
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = MaterialTheme.padding.small),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
// TODO: make this UX better; it only applies when triggering a new search
FilterChip(
selected = state.sourceFilter == MangaSourceFilter.PinnedOnly,
onClick = { onChangeSearchFilter(MangaSourceFilter.PinnedOnly) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.PushPin,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.pinned_sources))
},
)
FilterChip(
selected = state.sourceFilter == MangaSourceFilter.All,
onClick = { onChangeSearchFilter(MangaSourceFilter.All) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.DoneAll,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.all))
},
)
FilterChip(
selected = state.onlyShowHasResults,
onClick = { onToggleResults() },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.FilterList,
contentDescription = null,
modifier = Modifier
.size(FilterChipDefaults.IconSize),
)
},
label = {
Text(text = stringResource(id = R.string.has_results))
},
)
}
Divider()
}
},
) { paddingValues ->
GlobalSearchContent(
items = state.items,
items = items,
contentPadding = paddingValues,
getManga = getManga,
onClickSource = onClickSource,

View file

@ -1,242 +0,0 @@
package eu.kanade.presentation.components
import android.view.MotionEvent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.foundation.layout.widthIn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddCircle
import androidx.compose.material.icons.outlined.RemoveCircle
import androidx.compose.material.icons.rounded.CheckBox
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
import androidx.compose.material.icons.rounded.DisabledByDefault
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.presentation.core.components.SettingsItemsPaddings
@Composable
fun TriStateItem(
label: String,
state: TriStateFilter,
enabled: Boolean = true,
onClick: ((TriStateFilter) -> Unit)?,
) {
Row(
modifier = Modifier
.clickable(
enabled = enabled && onClick != null,
onClick = {
when (state) {
TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT)
TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED)
}
},
)
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
Icon(
imageVector = when (state) {
TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox
TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
},
contentDescription = null,
tint = if (!enabled || state == TriStateFilter.DISABLED) {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
} else {
when (onClick) {
null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled)
else -> MaterialTheme.colorScheme.primary
}
},
)
Text(
text = label,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha),
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
fun <T> SelectItem(
label: String,
options: Array<T>,
selectedIndex: Int,
modifier: Modifier = Modifier,
onSelect: (Int) -> Unit,
toString: (T) -> String = { it.toString() },
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = modifier,
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
label = { Text(text = label) },
value = toString(options[selectedIndex]),
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded,
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(
modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
expanded = expanded,
onDismissRequest = { expanded = false },
) {
options.forEachIndexed { index, option ->
DropdownMenuItem(
text = { Text(toString(option)) },
onClick = {
onSelect(index)
expanded = false
},
)
}
}
}
}
@Composable
fun RepeatingIconButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
maxDelayMillis: Long = 750,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .25f,
content: @Composable () -> Unit,
) {
val currentClickListener by rememberUpdatedState(onClick)
var pressed by remember { mutableStateOf(false) }
IconButton(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
content = content,
)
LaunchedEffect(pressed, enabled) {
var currentDelayMillis = maxDelayMillis
while (enabled && pressed) {
currentClickListener()
delay(currentDelayMillis)
currentDelayMillis =
(currentDelayMillis - (currentDelayMillis * delayDecayFactor))
.toLong().coerceAtLeast(minDelayMillis)
}
}
}
@Composable
fun OutlinedNumericChooser(
label: String,
placeholder: String,
suffix: String,
value: Int,
step: Int,
min: Int? = null,
onValueChanged: (Int) -> Unit,
) {
var currentValue = value
val updateValue: (Boolean) -> Unit = {
currentValue += if (it) step else -step
if (min != null) currentValue = if (currentValue < min) min else currentValue
onValueChanged(currentValue)
}
Row(verticalAlignment = Alignment.CenterVertically) {
RepeatingIconButton(
onClick = { updateValue(false) },
) { Icon(imageVector = Icons.Outlined.RemoveCircle, contentDescription = null) }
OutlinedTextField(
value = "%d".format(currentValue),
modifier = Modifier.widthIn(min = 140.dp),
onValueChange = {
// Don't allow multiple decimal points, non-numeric characters, or leading zeros
currentValue = it.trim().replace(Regex("[^-\\d.]"), "").toIntOrNull()
?: currentValue
onValueChanged(currentValue)
},
label = { Text(text = label) },
placeholder = { Text(text = placeholder) },
suffix = { Text(text = suffix) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
RepeatingIconButton(
onClick = { updateValue(true) },
) { Icon(imageVector = Icons.Outlined.AddCircle, contentDescription = null) }
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
@ -46,6 +47,7 @@ fun TabbedDialog(
onOverflowMenuClicked: (() -> Unit)? = null,
overflowIcon: ImageVector? = null,
hideSystemBars: Boolean = false,
pagerState: PagerState = rememberPagerState { tabTitles.size },
content: @Composable (Int) -> Unit,
) {
AdaptiveSheet(
@ -53,7 +55,6 @@ fun TabbedDialog(
onDismissRequest = onDismissRequest,
) {
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState { tabTitles.size }
Column {
Row {

View file

@ -26,20 +26,20 @@ import eu.kanade.domain.entries.anime.model.downloadedFilter
import eu.kanade.domain.entries.anime.model.forceDownloaded
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
import eu.kanade.tachiyomi.R
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.core.preference.TriState
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
@Composable
fun EpisodeSettingsDialog(
onDismissRequest: () -> Unit,
anime: Anime? = null,
onDownloadFilterChanged: (TriStateFilter) -> Unit,
onUnseenFilterChanged: (TriStateFilter) -> Unit,
onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
onDownloadFilterChanged: (TriState) -> Unit,
onUnseenFilterChanged: (TriState) -> Unit,
onBookmarkedFilterChanged: (TriState) -> Unit,
onSortModeChanged: (Long) -> Unit,
onDisplayModeChanged: (Long) -> Unit,
onSetAsDefault: (applyToExistingAnime: Boolean) -> Unit,
@ -77,11 +77,11 @@ fun EpisodeSettingsDialog(
when (page) {
0 -> {
FilterPage(
downloadFilter = anime?.downloadedFilter ?: TriStateFilter.DISABLED,
downloadFilter = anime?.downloadedFilter ?: TriState.DISABLED,
onDownloadFilterChanged = onDownloadFilterChanged.takeUnless { anime?.forceDownloaded() == true },
unseenFilter = anime?.unseenFilter ?: TriStateFilter.DISABLED,
unseenFilter = anime?.unseenFilter ?: TriState.DISABLED,
onUnseenFilterChanged = onUnseenFilterChanged,
bookmarkedFilter = anime?.bookmarkedFilter ?: TriStateFilter.DISABLED,
bookmarkedFilter = anime?.bookmarkedFilter ?: TriState.DISABLED,
onBookmarkedFilterChanged = onBookmarkedFilterChanged,
)
}
@ -105,12 +105,12 @@ fun EpisodeSettingsDialog(
@Composable
private fun FilterPage(
downloadFilter: TriStateFilter,
onDownloadFilterChanged: ((TriStateFilter) -> Unit)?,
unseenFilter: TriStateFilter,
onUnseenFilterChanged: (TriStateFilter) -> Unit,
bookmarkedFilter: TriStateFilter,
onBookmarkedFilterChanged: (TriStateFilter) -> Unit,
downloadFilter: TriState,
onDownloadFilterChanged: ((TriState) -> Unit)?,
unseenFilter: TriState,
onUnseenFilterChanged: (TriState) -> Unit,
bookmarkedFilter: TriState,
onBookmarkedFilterChanged: (TriState) -> Unit,
) {
TriStateItem(
label = stringResource(R.string.label_downloaded),

View file

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

View file

@ -7,21 +7,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibrarySettingsScreenModel
import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.library.anime.model.AnimeLibrarySort
import tachiyomi.domain.library.anime.model.sort
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -31,6 +28,7 @@ import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
@Composable
fun AnimeLibrarySettingsDialog(
@ -76,7 +74,7 @@ private fun ColumnScope.FilterPage(
TriStateItem(
label = stringResource(R.string.label_downloaded),
state = if (downloadedOnly) {
TriStateFilter.ENABLED_IS
TriState.ENABLED_IS
} else {
filterDownloaded
},

View file

@ -7,21 +7,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.components.TriStateItem
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.manga.MangaLibrarySettingsScreenModel
import tachiyomi.core.preference.TriState
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.library.manga.model.MangaLibrarySort
import tachiyomi.domain.library.manga.model.sort
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -31,6 +28,7 @@ import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.components.SortItem
import tachiyomi.presentation.core.components.TriStateItem
@Composable
fun MangaLibrarySettingsDialog(
@ -76,7 +74,7 @@ private fun ColumnScope.FilterPage(
TriStateItem(
label = stringResource(R.string.label_downloaded),
state = if (downloadedOnly) {
TriStateFilter.ENABLED_IS
TriState.ENABLED_IS
} else {
filterDownloaded
},

View file

@ -87,7 +87,7 @@ internal fun PreferenceItem(
min = item.min,
max = item.max,
value = item.value,
valueText = item.value.toString(),
valueText = item.subtitle.takeUnless { it.isNullOrEmpty() } ?: item.value.toString(),
onChange = {
scope.launch {
item.onValueChanged(it)

View file

@ -29,14 +29,6 @@ object SettingsBrowseScreen : SearchableSettings {
Preference.PreferenceGroup(
title = stringResource(R.string.label_sources),
preferenceItems = listOf(
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.searchPinnedAnimeSourcesOnly(),
title = stringResource(R.string.pref_search_pinned_anime_sources_only),
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.searchPinnedMangaSourcesOnly(),
title = stringResource(R.string.pref_search_pinned_manga_sources_only),
),
Preference.PreferenceItem.SwitchPreference(
pref = sourcePreferences.hideInAnimeLibraryItems(),
title = stringResource(R.string.pref_hide_in_anime_library_items),

View file

@ -394,7 +394,7 @@ object SettingsLibraryScreen : SearchableSettings {
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterStartAction(),
title = stringResource(R.string.pref_chapter_swipe_end),
title = stringResource(R.string.pref_chapter_swipe_start),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
@ -404,7 +404,7 @@ object SettingsLibraryScreen : SearchableSettings {
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeChapterEndAction(),
title = stringResource(R.string.pref_chapter_swipe_start),
title = stringResource(R.string.pref_chapter_swipe_end),
entries = mapOf(
LibraryPreferences.ChapterSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.ChapterSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark),
@ -425,7 +425,7 @@ object SettingsLibraryScreen : SearchableSettings {
preferenceItems = listOf(
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeStartAction(),
title = stringResource(R.string.pref_episode_swipe_end),
title = stringResource(R.string.pref_episode_swipe_start),
entries = mapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark_episode),
@ -435,7 +435,7 @@ object SettingsLibraryScreen : SearchableSettings {
),
Preference.PreferenceItem.ListPreference(
pref = libraryPreferences.swipeEpisodeEndAction(),
title = stringResource(R.string.pref_episode_swipe_start),
title = stringResource(R.string.pref_episode_swipe_end),
entries = mapOf(
LibraryPreferences.EpisodeSwipeAction.Disabled to stringResource(R.string.action_disable),
LibraryPreferences.EpisodeSwipeAction.ToggleBookmark to stringResource(R.string.action_bookmark_episode),

View file

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.NumberFormat
object SettingsReaderScreen : SearchableSettings {
@ -252,11 +253,15 @@ object SettingsReaderScreen : SearchableSettings {
@Composable
private fun getWebtoonGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
val numberFormat = remember { NumberFormat.getPercentInstance() }
val navModePref = readerPreferences.navigationModeWebtoon()
val dualPageSplitPref = readerPreferences.dualPageSplitWebtoon()
val webtoonSidePaddingPref = readerPreferences.webtoonSidePadding()
val navMode by navModePref.collectAsState()
val dualPageSplit by dualPageSplitPref.collectAsState()
val webtoonSidePadding by webtoonSidePaddingPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(R.string.webtoon_viewer),
@ -279,17 +284,16 @@ object SettingsReaderScreen : SearchableSettings {
),
enabled = navMode != 5,
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.webtoonSidePadding(),
Preference.PreferenceItem.SliderPreference(
value = webtoonSidePadding,
title = stringResource(R.string.pref_webtoon_side_padding),
entries = mapOf(
0 to stringResource(R.string.webtoon_side_padding_0),
5 to stringResource(R.string.webtoon_side_padding_5),
10 to stringResource(R.string.webtoon_side_padding_10),
15 to stringResource(R.string.webtoon_side_padding_15),
20 to stringResource(R.string.webtoon_side_padding_20),
25 to stringResource(R.string.webtoon_side_padding_25),
),
subtitle = numberFormat.format(webtoonSidePadding / 100f),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
onValueChanged = {
webtoonSidePaddingPref.set(it)
true
},
),
Preference.PreferenceItem.ListPreference(
pref = readerPreferences.readerHideThreshold(),

View file

@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.SelectItem
import eu.kanade.tachiyomi.R
import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.SelectItem
@Composable
fun SelectStorageCategory(

View file

@ -0,0 +1,158 @@
package eu.kanade.presentation.reader.settings
import android.os.Build
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.core.preference.getAndSet
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.SelectItem
import tachiyomi.presentation.core.components.SliderItem
@Composable
internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel) {
val colorFilterModes = buildList {
addAll(
listOf(
R.string.label_default,
R.string.filter_mode_multiply,
R.string.filter_mode_screen,
),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
addAll(
listOf(
R.string.filter_mode_overlay,
R.string.filter_mode_lighten,
R.string.filter_mode_darken,
),
)
}
}.map { stringResource(it) }
val customBrightness by screenModel.preferences.customBrightness().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_custom_brightness),
checked = customBrightness,
onClick = {
screenModel.togglePreference(ReaderPreferences::customBrightness)
},
)
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
*/
if (customBrightness) {
val customBrightnessValue by screenModel.preferences.customBrightnessValue().collectAsState()
SliderItem(
label = stringResource(R.string.pref_custom_brightness),
min = -75,
max = 100,
value = customBrightnessValue,
valueText = customBrightnessValue.toString(),
onChange = { screenModel.preferences.customBrightnessValue().set(it) },
)
}
val colorFilter by screenModel.preferences.colorFilter().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_custom_color_filter),
checked = colorFilter,
onClick = {
screenModel.togglePreference(ReaderPreferences::colorFilter)
},
)
if (colorFilter) {
val colorFilterValue by screenModel.preferences.colorFilterValue().collectAsState()
SliderItem(
label = stringResource(R.string.color_filter_r_value),
max = 255,
value = colorFilterValue.red,
valueText = colorFilterValue.red.toString(),
onChange = { newRValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
},
)
SliderItem(
label = stringResource(R.string.color_filter_g_value),
max = 255,
value = colorFilterValue.green,
valueText = colorFilterValue.green.toString(),
onChange = { newGValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newGValue, GREEN_MASK, 8)
}
},
)
SliderItem(
label = stringResource(R.string.color_filter_b_value),
max = 255,
value = colorFilterValue.blue,
valueText = colorFilterValue.blue.toString(),
onChange = { newBValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newBValue, BLUE_MASK, 0)
}
},
)
SliderItem(
label = stringResource(R.string.color_filter_a_value),
max = 255,
value = colorFilterValue.alpha,
valueText = colorFilterValue.alpha.toString(),
onChange = { newAValue ->
screenModel.preferences.colorFilterValue().getAndSet {
getColorValue(it, newAValue, ALPHA_MASK, 24)
}
},
)
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
SelectItem(
label = stringResource(R.string.pref_color_filter_mode),
options = colorFilterModes.toTypedArray(),
selectedIndex = colorFilterMode,
) {
screenModel.preferences.colorFilterMode().set(it)
}
}
val grayscale by screenModel.preferences.grayscale().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_grayscale),
checked = grayscale,
onClick = {
screenModel.togglePreference(ReaderPreferences::grayscale)
},
)
val invertedColors by screenModel.preferences.invertedColors().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_inverted_colors),
checked = invertedColors,
onClick = {
screenModel.togglePreference(ReaderPreferences::invertedColors)
},
)
}
private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int {
return (color shl bitShift) or (currentColor and mask.inv().toInt())
}
private const val ALPHA_MASK: Long = 0xFF000000
private const val RED_MASK: Long = 0x00FF0000
private const val GREEN_MASK: Long = 0x0000FF00
private const val BLUE_MASK: Long = 0x000000FF

View file

@ -0,0 +1,96 @@
package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.RadioItem
@Composable
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
// TODO: show this in a nicer way
HeadingItem(R.string.pref_reader_theme)
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
listOf(
R.string.black_background to 1,
R.string.gray_background to 2,
R.string.white_background to 0,
R.string.automatic_background to 3,
).map { (titleRes, theme) ->
RadioItem(
label = stringResource(titleRes),
selected = readerTheme == theme,
onClick = { screenModel.preferences.readerTheme().set(theme) },
)
}
val showPageNumber by screenModel.preferences.showPageNumber().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_show_page_number),
checked = showPageNumber,
onClick = {
screenModel.togglePreference(ReaderPreferences::showPageNumber)
},
)
val fullscreen by screenModel.preferences.fullscreen().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_fullscreen),
checked = fullscreen,
onClick = {
screenModel.togglePreference(ReaderPreferences::fullscreen)
},
)
// TODO: hide if there's no cutout
val cutoutShort by screenModel.preferences.cutoutShort().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_cutout_short),
checked = cutoutShort,
onClick = {
screenModel.togglePreference(ReaderPreferences::cutoutShort)
},
)
val keepScreenOn by screenModel.preferences.keepScreenOn().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_keep_screen_on),
checked = keepScreenOn,
onClick = {
screenModel.togglePreference(ReaderPreferences::keepScreenOn)
},
)
val readWithLongTap by screenModel.preferences.readWithLongTap().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_read_with_long_tap),
checked = readWithLongTap,
onClick = {
screenModel.togglePreference(ReaderPreferences::readWithLongTap)
},
)
val alwaysShowChapterTransition by screenModel.preferences.alwaysShowChapterTransition().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_always_show_chapter_transition),
checked = alwaysShowChapterTransition,
onClick = {
screenModel.togglePreference(ReaderPreferences::alwaysShowChapterTransition)
},
)
val pageTransitions by screenModel.preferences.pageTransitions().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_page_transitions),
checked = pageTransitions,
onClick = {
screenModel.togglePreference(ReaderPreferences::pageTransitions)
},
)
}

View file

@ -0,0 +1,66 @@
package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.DialogWindowProvider
import eu.kanade.presentation.components.TabbedDialog
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
@Composable
fun ReaderSettingsDialog(
onDismissRequest: () -> Unit,
onShowMenus: () -> Unit,
onHideMenus: () -> Unit,
screenModel: ReaderSettingsScreenModel,
) {
// TODO: undimming doesn't seem to work
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
val tabTitles = listOf(
stringResource(R.string.pref_category_reading_mode),
stringResource(R.string.pref_category_general),
stringResource(R.string.custom_filter),
)
val pagerState = rememberPagerState { tabTitles.size }
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage == 2) {
window?.setDimAmount(0f)
onHideMenus()
} else {
window?.setDimAmount(0.75f)
onShowMenus()
}
}
TabbedDialog(
onDismissRequest = {
onDismissRequest()
onShowMenus()
},
tabTitles = tabTitles,
pagerState = pagerState,
) { page ->
Column(
modifier = Modifier
.padding(vertical = TabbedDialogPaddings.Vertical)
.verticalScroll(rememberScrollState()),
) {
when (page) {
0 -> ReadingModePage(screenModel)
1 -> GeneralPage(screenModel)
2 -> ColorFilterPage(screenModel)
}
}
}
}

View file

@ -0,0 +1,179 @@
package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.presentation.core.components.CheckboxItem
import tachiyomi.presentation.core.components.HeadingItem
import tachiyomi.presentation.core.components.SliderItem
import java.text.NumberFormat
@Composable
internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pref_category_for_this_series)
// Reading mode
// Rotation type
// if (pager)
PagerViewerSettings(screenModel)
WebtoonViewerSettings(screenModel)
}
@Composable
private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenModel) {
HeadingItem(R.string.pager_viewer)
// Tap zones
// Invert tap zones
// Scale type
// Zoom start position
val cropBorders by screenModel.preferences.cropBorders().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_crop_borders),
checked = cropBorders,
onClick = {
screenModel.togglePreference(ReaderPreferences::cropBorders)
},
)
val landscapeZoom by screenModel.preferences.landscapeZoom().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_landscape_zoom),
checked = landscapeZoom,
onClick = {
screenModel.togglePreference(ReaderPreferences::landscapeZoom)
},
)
val navigateToPan by screenModel.preferences.navigateToPan().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_navigate_pan),
checked = navigateToPan,
onClick = {
screenModel.togglePreference(ReaderPreferences::navigateToPan)
},
)
val dualPageSplitPaged by screenModel.preferences.dualPageSplitPaged().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_dual_page_split),
checked = dualPageSplitPaged,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageSplitPaged)
},
)
if (dualPageSplitPaged) {
val dualPageInvertPaged by screenModel.preferences.dualPageInvertPaged().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_dual_page_invert),
checked = dualPageInvertPaged,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageInvertPaged)
},
)
}
val dualPageRotateToFit by screenModel.preferences.dualPageRotateToFit().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_page_rotate),
checked = dualPageRotateToFit,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageRotateToFit)
},
)
if (dualPageRotateToFit) {
val dualPageRotateToFitInvert by screenModel.preferences.dualPageRotateToFitInvert().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_page_rotate_invert),
checked = dualPageRotateToFitInvert,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageRotateToFitInvert)
},
)
}
}
@Composable
private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenModel) {
val numberFormat = remember { NumberFormat.getPercentInstance() }
HeadingItem(R.string.webtoon_viewer)
// TODO: Tap zones
// TODO: Invert tap zones
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
SliderItem(
label = stringResource(R.string.pref_webtoon_side_padding),
min = ReaderPreferences.WEBTOON_PADDING_MIN,
max = ReaderPreferences.WEBTOON_PADDING_MAX,
value = webtoonSidePadding,
valueText = numberFormat.format(webtoonSidePadding / 100f),
onChange = {
screenModel.preferences.webtoonSidePadding().set(it)
},
)
val cropBordersWebtoon by screenModel.preferences.cropBordersWebtoon().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_crop_borders),
checked = cropBordersWebtoon,
onClick = {
screenModel.togglePreference(ReaderPreferences::cropBordersWebtoon)
},
)
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_dual_page_split),
checked = dualPageSplitWebtoon,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageSplitWebtoon)
},
)
if (dualPageSplitWebtoon) {
val dualPageInvertWebtoon by screenModel.preferences.dualPageInvertWebtoon()
.collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_dual_page_invert),
checked = dualPageInvertWebtoon,
onClick = {
screenModel.togglePreference(ReaderPreferences::dualPageInvertWebtoon)
},
)
}
if (!isReleaseBuildType) {
val longStripSplitWebtoon by screenModel.preferences.longStripSplitWebtoon()
.collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_long_strip_split),
checked = longStripSplitWebtoon,
onClick = {
screenModel.togglePreference(ReaderPreferences::longStripSplitWebtoon)
},
)
}
val webtoonDoubleTapZoomEnabled by screenModel.preferences.webtoonDoubleTapZoomEnabled().collectAsState()
CheckboxItem(
label = stringResource(R.string.pref_double_tap_zoom),
checked = webtoonDoubleTapZoomEnabled,
onClick = {
screenModel.togglePreference(ReaderPreferences::webtoonDoubleTapZoomEnabled)
},
)
}

View file

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

View file

@ -41,7 +41,12 @@ internal object AnimeExtensionLoader {
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 15
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
}
// jmir1's key
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
@ -49,7 +54,7 @@ internal object AnimeExtensionLoader {
/**
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
/**
* Return a list of all the installed extensions initialized concurrently.
@ -59,7 +64,6 @@ internal object AnimeExtensionLoader {
fun loadExtensions(context: Context): List<AnimeLoadResult> {
val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
@ -135,7 +139,7 @@ internal object AnimeExtensionLoader {
return AnimeLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
val signatureHash = getSignatureHash(context, pkgInfo)
if (signatureHash == null) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
@ -221,12 +225,8 @@ internal object AnimeExtensionLoader {
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
}
}

View file

@ -41,7 +41,12 @@ internal object MangaExtensionLoader {
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.5
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
private val PACKAGE_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNING_CERTIFICATES
} else {
@Suppress("DEPRECATION")
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
}
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
@ -49,7 +54,7 @@ internal object MangaExtensionLoader {
/**
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
var trustedSignatures = mutableSetOf(officialSignature) + preferences.trustedSignatures().get()
/**
* Return a list of all the installed extensions initialized concurrently.
@ -59,7 +64,6 @@ internal object MangaExtensionLoader {
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
val pkgManager = context.packageManager
@Suppress("DEPRECATION")
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
} else {
@ -135,7 +139,7 @@ internal object MangaExtensionLoader {
return MangaLoadResult.Error
}
val signatureHash = getSignatureHash(pkgInfo)
val signatureHash = getSignatureHash(context, pkgInfo)
if (signatureHash == null) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
@ -221,12 +225,8 @@ internal object MangaExtensionLoader {
*
* @param pkgInfo The package info of the application.
*/
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
private fun getSignatureHash(context: Context, pkgInfo: PackageInfo): String? {
val signatures = PackageInfoCompat.getSignatures(context.packageManager, pkgInfo.packageName)
return signatures.firstOrNull()?.let { Hash.sha256(it.toByteArray()) }
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -313,6 +314,7 @@ internal class MigrateAnimeDialogScreenModel(
)
}
@Immutable
data class State(
val isMigrating: Boolean = false,
)

View file

@ -10,6 +10,7 @@ import eu.kanade.presentation.browse.anime.MigrateAnimeSearchScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic
class MigrateAnimeSearchScreen(private val animeId: Long) : Screen() {
@Composable

View file

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

View file

@ -68,16 +68,7 @@ abstract class AnimeSearchScreenModel<T>(
val enabledSources = getEnabledSources()
if (filter.isEmpty()) {
val shouldSearchPinnedOnly = sourcePreferences.searchPinnedAnimeSourcesOnly().get()
val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
return enabledSources.filter {
if (shouldSearchPinnedOnly) {
"${it.id}" in pinnedSources
} else {
true
}
}
return enabledSources
}
return extensionManager.installedExtensionsFlow.value
@ -136,6 +127,11 @@ abstract class AnimeSearchScreenModel<T>(
}
}
enum class AnimeSourceFilter {
All,
PinnedOnly,
}
sealed class AnimeSearchItemResult {
object Loading : AnimeSearchItemResult()

View file

@ -35,6 +35,7 @@ class GlobalAnimeSearchScreen(
var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
}
val filteredSources by screenModel.searchPagerFlow.collectAsState()
if (showSingleLoadingScreen) {
LoadingScreen()
@ -57,10 +58,13 @@ class GlobalAnimeSearchScreen(
} else {
GlobalAnimeSearchScreen(
state = state,
items = filteredSources,
navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = screenModel::search,
getAnime = { screenModel.getAnime(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onClickSource = {
if (!screenModel.incognitoMode.get()) {
screenModel.lastUsedSourceId.set(it.id)

View file

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch
import androidx.compose.runtime.Immutable
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import uy.kohesive.injekt.Injekt
@ -15,8 +20,8 @@ class GlobalAnimeSearchScreenModel(
preferences: BasePreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: AnimeSourceManager = Injekt.get(),
) : AnimeSearchScreenModel<GlobalAnimeSearchState>(
GlobalAnimeSearchState(
) : AnimeSearchScreenModel<GlobalAnimeSearchScreenModel.State>(
State(
searchQuery = initialQuery,
),
) {
@ -24,6 +29,13 @@ class GlobalAnimeSearchScreenModel(
val incognitoMode = preferences.incognitoMode()
val lastUsedSourceId = sourcePreferences.lastUsedAnimeSource()
val searchPagerFlow = state.map { Pair(it.onlyShowHasResults, it.items) }
.distinctUntilChanged()
.map { (onlyShowHasResults, items) ->
items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
.stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items)
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
@ -37,6 +49,7 @@ class GlobalAnimeSearchScreenModel(
val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
return sourceManager.getCatalogueSources()
.filter { mutableState.value.sourceFilter != AnimeSourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
.filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
@ -57,15 +70,29 @@ class GlobalAnimeSearchScreenModel(
override fun getItems(): Map<AnimeCatalogueSource, AnimeSearchItemResult> {
return mutableState.value.items
}
}
@Immutable
data class GlobalAnimeSearchState(
fun setSourceFilter(filter: AnimeSourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
private fun AnimeSearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean {
return !onlyShowHasResults || (this is AnimeSearchItemResult.Success && !this.isEmpty)
}
@Immutable
data class State(
val searchQuery: String? = null,
val sourceFilter: AnimeSourceFilter = AnimeSourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<AnimeCatalogueSource, AnimeSearchItemResult> = emptyMap(),
) {
) {
val progress: Int = items.count { it.value !is AnimeSearchItemResult.Loading }
val total: Int = items.size
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -313,6 +314,7 @@ internal class MigrateMangaDialogScreenModel(
)
}
@Immutable
data class State(
val isMigrating: Boolean = false,
)

View file

@ -10,6 +10,7 @@ import eu.kanade.presentation.browse.manga.MigrateMangaSearchScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic
class MigrateSearchScreen(private val mangaId: Long) : Screen() {
@Composable

View file

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

View file

@ -35,6 +35,7 @@ class GlobalMangaSearchScreen(
var showSingleLoadingScreen by remember {
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
}
val filteredSources by screenModel.searchPagerFlow.collectAsState()
if (showSingleLoadingScreen) {
LoadingScreen()
@ -57,10 +58,13 @@ class GlobalMangaSearchScreen(
} else {
GlobalMangaSearchScreen(
state = state,
items = filteredSources,
navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = screenModel::search,
getManga = { screenModel.getManga(it) },
onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults,
onClickSource = {
if (!screenModel.incognitoMode.get()) {
screenModel.lastUsedSourceId.set(it.id)

View file

@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch
import androidx.compose.runtime.Immutable
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope
import eu.kanade.tachiyomi.source.CatalogueSource
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.Injekt
@ -15,8 +20,8 @@ class GlobalMangaSearchScreenModel(
preferences: BasePreferences = Injekt.get(),
private val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: MangaSourceManager = Injekt.get(),
) : MangaSearchScreenModel<GlobalMangaSearchState>(
GlobalMangaSearchState(
) : MangaSearchScreenModel<GlobalMangaSearchScreenModel.State>(
State(
searchQuery = initialQuery,
),
) {
@ -24,6 +29,13 @@ class GlobalMangaSearchScreenModel(
val incognitoMode = preferences.incognitoMode()
val lastUsedSourceId = sourcePreferences.lastUsedMangaSource()
val searchPagerFlow = state.map { Pair(it.onlyShowHasResults, it.items) }
.distinctUntilChanged()
.map { (onlyShowHasResults, items) ->
items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
.stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items)
init {
extensionFilter = initialExtensionFilter
if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
@ -37,6 +49,7 @@ class GlobalMangaSearchScreenModel(
val pinnedSources = sourcePreferences.pinnedMangaSources().get()
return sourceManager.getCatalogueSources()
.filter { mutableState.value.sourceFilter != MangaSourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
.filter { it.lang in enabledLanguages }
.filterNot { "${it.id}" in disabledSources }
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
@ -57,15 +70,29 @@ class GlobalMangaSearchScreenModel(
override fun getItems(): Map<CatalogueSource, MangaSearchItemResult> {
return mutableState.value.items
}
}
@Immutable
data class GlobalMangaSearchState(
fun setSourceFilter(filter: MangaSourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
private fun MangaSearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean {
return !onlyShowHasResults || (this is MangaSearchItemResult.Success && !this.isEmpty)
}
@Immutable
data class State(
val searchQuery: String? = null,
val sourceFilter: MangaSourceFilter = MangaSourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, MangaSearchItemResult> = emptyMap(),
) {
) {
val progress: Int = items.count { it.value !is MangaSearchItemResult.Loading }
val total: Int = items.size
}
}

View file

@ -68,16 +68,7 @@ abstract class MangaSearchScreenModel<T>(
val enabledSources = getEnabledSources()
if (filter.isEmpty()) {
val shouldSearchPinnedOnly = sourcePreferences.searchPinnedMangaSourcesOnly().get()
val pinnedSources = sourcePreferences.pinnedMangaSources().get()
return enabledSources.filter {
if (shouldSearchPinnedOnly) {
"${it.id}" in pinnedSources
} else {
true
}
}
return enabledSources
}
return extensionManager.installedExtensionsFlow.value
@ -136,6 +127,11 @@ abstract class MangaSearchScreenModel<T>(
}
}
enum class MangaSourceFilter {
All,
PinnedOnly,
}
sealed class MangaSearchItemResult {
object Loading : MangaSearchItemResult()

View file

@ -50,6 +50,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.preference.CheckboxState
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.mapAsCheckboxState
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
@ -60,7 +61,6 @@ import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
import tachiyomi.domain.category.anime.interactor.SetAnimeCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
@ -161,7 +161,8 @@ class AnimeInfoScreenModel(
combine(
getAnimeAndEpisodes.subscribe(animeId).distinctUntilChanged(),
downloadCache.changes,
) { animeAndEpisodes, _ -> animeAndEpisodes }
downloadManager.queueState,
) { animeAndEpisodes, _, _ -> animeAndEpisodes }
.collectLatest { (anime, episodes) ->
updateSuccessState {
it.copy(
@ -766,13 +767,13 @@ class AnimeInfoScreenModel(
* Sets the seen filter and requests an UI update.
* @param state whether to display only unseen episodes or all episodes.
*/
fun setUnseenFilter(state: TriStateFilter) {
fun setUnseenFilter(state: TriState) {
val anime = successState?.anime ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Anime.SHOW_ALL
TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_UNSEEN
TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_SEEN
TriState.DISABLED -> Anime.SHOW_ALL
TriState.ENABLED_IS -> Anime.EPISODE_SHOW_UNSEEN
TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_SEEN
}
coroutineScope.launchNonCancellable {
setAnimeEpisodeFlags.awaitSetUnseenFilter(anime, flag)
@ -783,13 +784,13 @@ class AnimeInfoScreenModel(
* Sets the download filter and requests an UI update.
* @param state whether to display only downloaded episodes or all episodes.
*/
fun setDownloadedFilter(state: TriStateFilter) {
fun setDownloadedFilter(state: TriState) {
val anime = successState?.anime ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Anime.SHOW_ALL
TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_DOWNLOADED
TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_DOWNLOADED
TriState.DISABLED -> Anime.SHOW_ALL
TriState.ENABLED_IS -> Anime.EPISODE_SHOW_DOWNLOADED
TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_DOWNLOADED
}
coroutineScope.launchNonCancellable {
@ -801,13 +802,13 @@ class AnimeInfoScreenModel(
* Sets the bookmark filter and requests an UI update.
* @param state whether to display only bookmarked episodes or all episodes.
*/
fun setBookmarkedFilter(state: TriStateFilter) {
fun setBookmarkedFilter(state: TriState) {
val anime = successState?.anime ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Anime.SHOW_ALL
TriStateFilter.ENABLED_IS -> Anime.EPISODE_SHOW_BOOKMARKED
TriStateFilter.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_BOOKMARKED
TriState.DISABLED -> Anime.SHOW_ALL
TriState.ENABLED_IS -> Anime.EPISODE_SHOW_BOOKMARKED
TriState.ENABLED_NOT -> Anime.EPISODE_SHOW_NOT_BOOKMARKED
}
coroutineScope.launchNonCancellable {

View file

@ -20,6 +20,7 @@ import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -269,6 +270,7 @@ data class AnimeTrackInfoDialogHomeScreen(
.filter { (it.service as? EnhancedAnimeTrackService)?.accept(source) ?: true }
}
@Immutable
data class State(
val trackItems: List<AnimeTrackItem> = emptyList(),
)
@ -318,6 +320,7 @@ private data class TrackStatusSelectorScreen(
}
}
@Immutable
data class State(
val selection: Int,
)
@ -374,6 +377,7 @@ private data class TrackEpisodeSelectorScreen(
}
}
@Immutable
data class State(
val selection: Int,
)
@ -424,6 +428,7 @@ private data class TrackScoreSelectorScreen(
}
}
@Immutable
data class State(
val selection: String,
)
@ -731,6 +736,7 @@ data class TrackServiceSearchScreen(
mutableState.update { it.copy(selected = selected) }
}
@Immutable
data class State(
val queryResult: Result<List<AnimeTrackSearch>>? = null,
val selected: AnimeTrackSearch? = null,

View file

@ -47,6 +47,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.preference.CheckboxState
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.mapAsCheckboxState
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
@ -57,7 +58,6 @@ import tachiyomi.domain.category.manga.interactor.GetMangaCategories
import tachiyomi.domain.category.manga.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
@ -156,7 +156,8 @@ class MangaInfoScreenModel(
combine(
getMangaAndChapters.subscribe(mangaId).distinctUntilChanged(),
downloadCache.changes,
) { mangaAndChapters, _ -> mangaAndChapters }
downloadManager.queueState,
) { mangaAndChapters, _, _ -> mangaAndChapters }
.collectLatest { (manga, chapters) ->
updateSuccessState {
it.copy(
@ -756,13 +757,13 @@ class MangaInfoScreenModel(
* Sets the read filter and requests an UI update.
* @param state whether to display only unread chapters or all chapters.
*/
fun setUnreadFilter(state: TriStateFilter) {
fun setUnreadFilter(state: TriState) {
val manga = successState?.manga ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Manga.SHOW_ALL
TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD
TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ
TriState.DISABLED -> Manga.SHOW_ALL
TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_UNREAD
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_READ
}
coroutineScope.launchNonCancellable {
setMangaChapterFlags.awaitSetUnreadFilter(manga, flag)
@ -773,13 +774,13 @@ class MangaInfoScreenModel(
* Sets the download filter and requests an UI update.
* @param state whether to display only downloaded chapters or all chapters.
*/
fun setDownloadedFilter(state: TriStateFilter) {
fun setDownloadedFilter(state: TriState) {
val manga = successState?.manga ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Manga.SHOW_ALL
TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED
TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED
TriState.DISABLED -> Manga.SHOW_ALL
TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_DOWNLOADED
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_DOWNLOADED
}
coroutineScope.launchNonCancellable {
@ -791,13 +792,13 @@ class MangaInfoScreenModel(
* Sets the bookmark filter and requests an UI update.
* @param state whether to display only bookmarked chapters or all chapters.
*/
fun setBookmarkedFilter(state: TriStateFilter) {
fun setBookmarkedFilter(state: TriState) {
val manga = successState?.manga ?: return
val flag = when (state) {
TriStateFilter.DISABLED -> Manga.SHOW_ALL
TriStateFilter.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED
TriStateFilter.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED
TriState.DISABLED -> Manga.SHOW_ALL
TriState.ENABLED_IS -> Manga.CHAPTER_SHOW_BOOKMARKED
TriState.ENABLED_NOT -> Manga.CHAPTER_SHOW_NOT_BOOKMARKED
}
coroutineScope.launchNonCancellable {

View file

@ -20,6 +20,7 @@ import androidx.compose.material3.SelectableDates
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -268,6 +269,7 @@ data class MangaTrackInfoDialogHomeScreen(
.filter { (it.service as? EnhancedMangaTrackService)?.accept(source) ?: true }
}
@Immutable
data class State(
val trackItems: List<MangaTrackItem> = emptyList(),
)
@ -317,6 +319,7 @@ private data class TrackStatusSelectorScreen(
}
}
@Immutable
data class State(
val selection: Int,
)
@ -373,6 +376,7 @@ private data class TrackChapterSelectorScreen(
}
}
@Immutable
data class State(
val selection: Int,
)
@ -423,6 +427,7 @@ private data class TrackScoreSelectorScreen(
}
}
@Immutable
data class State(
val selection: String,
)
@ -730,6 +735,7 @@ data class TrackServiceSearchScreen(
mutableState.update { it.copy(selected = selected) }
}
@Immutable
data class State(
val queryResult: Result<List<MangaTrackSearch>>? = null,
val selected: MangaTrackSearch? = null,

View file

@ -41,13 +41,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import tachiyomi.core.preference.CheckboxState
import tachiyomi.core.preference.TriState
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.category.anime.interactor.GetVisibleAnimeCategories
import tachiyomi.domain.category.anime.interactor.SetAnimeCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate
@ -154,7 +154,7 @@ class AnimeLibraryScreenModel(
prefs.filterBookmarked,
prefs.filterCompleted,
) + trackFilter.values
).any { it != TriStateFilter.DISABLED }
).any { it != TriState.DISABLED }
}
.distinctUntilChanged()
.onEach {
@ -170,12 +170,12 @@ class AnimeLibraryScreenModel(
*/
private suspend fun AnimeLibraryMap.applyFilters(
trackMap: Map<Long, List<Long>>,
loggedInTrackServices: Map<Long, TriStateFilter>,
loggedInTrackServices: Map<Long, TriState>,
): AnimeLibraryMap {
val prefs = getAnimelibItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded
val filterDownloaded =
if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded
if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
val filterUnseen = prefs.filterUnseen
val filterStarted = prefs.filterStarted
val filterBookmarked = prefs.filterBookmarked
@ -183,8 +183,8 @@ class AnimeLibraryScreenModel(
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null }
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (AnimeLibraryItem) -> Boolean = {
@ -315,11 +315,11 @@ class AnimeLibraryScreenModel(
localBadge = it[1] as Boolean,
languageBadge = it[2] as Boolean,
globalFilterDownloaded = it[3] as Boolean,
filterDownloaded = it[4] as TriStateFilter,
filterUnseen = it[5] as TriStateFilter,
filterStarted = it[6] as TriStateFilter,
filterBookmarked = it[7] as TriStateFilter,
filterCompleted = it[8] as TriStateFilter,
filterDownloaded = it[4] as TriState,
filterUnseen = it[5] as TriState,
filterStarted = it[6] as TriState,
filterBookmarked = it[7] as TriState,
filterCompleted = it[8] as TriState,
)
},
)
@ -372,7 +372,7 @@ class AnimeLibraryScreenModel(
*
* @return map of track id with the filter value
*/
private fun getTrackingFilterFlow(): Flow<Map<Long, TriStateFilter>> {
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
val loggedServices = trackManager.services.filter { it.isLogged && it is AnimeTrackService }
return if (loggedServices.isNotEmpty()) {
val prefFlows = loggedServices
@ -677,11 +677,11 @@ class AnimeLibraryScreenModel(
val languageBadge: Boolean,
val globalFilterDownloaded: Boolean,
val filterDownloaded: TriStateFilter,
val filterUnseen: TriStateFilter,
val filterStarted: TriStateFilter,
val filterBookmarked: TriStateFilter,
val filterCompleted: TriStateFilter,
val filterDownloaded: TriState,
val filterUnseen: TriState,
val filterStarted: TriState,
val filterBookmarked: TriState,
val filterCompleted: TriState,
)
@Immutable

View file

@ -6,12 +6,12 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.preference.toggle
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.category.anime.interactor.SetAnimeDisplayMode
import tachiyomi.domain.category.anime.interactor.SetSortModeForAnimeCategory
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.library.anime.model.AnimeLibrarySort
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.service.LibraryPreferences
@ -33,7 +33,7 @@ class AnimeLibrarySettingsScreenModel(
preference(libraryPreferences).toggle()
}
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriStateFilter>) {
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) {
preference(libraryPreferences).getAndSet {
it.next()
}

View file

@ -41,13 +41,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import tachiyomi.core.preference.CheckboxState
import tachiyomi.core.preference.TriState
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.category.manga.interactor.GetVisibleMangaCategories
import tachiyomi.domain.category.manga.interactor.SetMangaCategories
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.model.Manga
@ -154,7 +154,7 @@ class MangaLibraryScreenModel(
prefs.filterBookmarked,
prefs.filterCompleted,
) + trackFilter.values
).any { it != TriStateFilter.DISABLED }
).any { it != TriState.DISABLED }
}
.distinctUntilChanged()
.onEach {
@ -170,12 +170,12 @@ class MangaLibraryScreenModel(
*/
private suspend fun MangaLibraryMap.applyFilters(
trackMap: Map<Long, List<Long>>,
loggedInTrackServices: Map<Long, TriStateFilter>,
loggedInTrackServices: Map<Long, TriState>,
): MangaLibraryMap {
val prefs = getLibraryItemPreferencesFlow().first()
val downloadedOnly = prefs.globalFilterDownloaded
val filterDownloaded =
if (downloadedOnly) TriStateFilter.ENABLED_IS else prefs.filterDownloaded
if (downloadedOnly) TriState.ENABLED_IS else prefs.filterDownloaded
val filterUnread = prefs.filterUnread
val filterStarted = prefs.filterStarted
val filterBookmarked = prefs.filterBookmarked
@ -183,8 +183,8 @@ class MangaLibraryScreenModel(
val isNotLoggedInAnyTrack = loggedInTrackServices.isEmpty()
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriStateFilter.ENABLED_IS) it.key else null }
val excludedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_NOT) it.key else null }
val includedTracks = loggedInTrackServices.mapNotNull { if (it.value == TriState.ENABLED_IS) it.key else null }
val trackFiltersIsIgnored = includedTracks.isEmpty() && excludedTracks.isEmpty()
val filterFnDownloaded: (MangaLibraryItem) -> Boolean = {
@ -309,11 +309,11 @@ class MangaLibraryScreenModel(
localBadge = it[1] as Boolean,
languageBadge = it[2] as Boolean,
globalFilterDownloaded = it[3] as Boolean,
filterDownloaded = it[4] as TriStateFilter,
filterUnread = it[5] as TriStateFilter,
filterStarted = it[6] as TriStateFilter,
filterBookmarked = it[7] as TriStateFilter,
filterCompleted = it[8] as TriStateFilter,
filterDownloaded = it[4] as TriState,
filterUnread = it[5] as TriState,
filterStarted = it[6] as TriState,
filterBookmarked = it[7] as TriState,
filterCompleted = it[8] as TriState,
)
},
)
@ -366,7 +366,7 @@ class MangaLibraryScreenModel(
*
* @return map of track id with the filter value
*/
private fun getTrackingFilterFlow(): Flow<Map<Long, TriStateFilter>> {
private fun getTrackingFilterFlow(): Flow<Map<Long, TriState>> {
val loggedServices = trackManager.services.filter { it.isLogged && it is MangaTrackService }
return if (loggedServices.isNotEmpty()) {
val prefFlows = loggedServices
@ -671,11 +671,11 @@ class MangaLibraryScreenModel(
val languageBadge: Boolean,
val globalFilterDownloaded: Boolean,
val filterDownloaded: TriStateFilter,
val filterUnread: TriStateFilter,
val filterStarted: TriStateFilter,
val filterBookmarked: TriStateFilter,
val filterCompleted: TriStateFilter,
val filterDownloaded: TriState,
val filterUnread: TriState,
val filterStarted: TriState,
val filterBookmarked: TriState,
val filterCompleted: TriState,
)
@Immutable

View file

@ -6,12 +6,12 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.util.preference.toggle
import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getAndSet
import tachiyomi.core.util.lang.launchIO
import tachiyomi.domain.category.manga.interactor.SetMangaDisplayMode
import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.library.manga.model.MangaLibrarySort
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.domain.library.service.LibraryPreferences
@ -33,7 +33,7 @@ class MangaLibrarySettingsScreenModel(
preference(libraryPreferences).toggle()
}
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriStateFilter>) {
fun toggleFilter(preference: (LibraryPreferences) -> Preference<TriState>) {
preference(libraryPreferences).getAndSet {
it.next()
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.player
import android.app.Application
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -719,6 +720,7 @@ class PlayerViewModel(
mutableState.update { it.copy(dialog = null, sheet = null) }
}
@Immutable
data class State(
val episodeList: List<Episode> = emptyList(),
val episode: Episode? = null,

View file

@ -15,11 +15,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.presentation.components.OutlinedNumericChooser
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import `is`.xyz.mpv.MPVLib
import tachiyomi.presentation.core.components.OutlinedNumericChooser
import tachiyomi.presentation.core.components.material.padding
@Composable

View file

@ -32,12 +32,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.yubyf.truetypeparser.TTFFile
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.OutlinedNumericChooser
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.player.settings.PlayerPreferences
import eu.kanade.tachiyomi.ui.player.settings.PlayerSettingsScreenModel
import `is`.xyz.mpv.MPVLib
import tachiyomi.presentation.core.components.OutlinedNumericChooser
import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.padding
import java.io.File

View file

@ -31,6 +31,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.core.graphics.ColorUtils
@ -50,6 +51,7 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.entries.manga.model.orientationType
import eu.kanade.presentation.reader.ChapterNavigator
import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -65,8 +67,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderColorFilterDialog
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
@ -392,6 +394,8 @@ class ReaderActivity : BaseActivity() {
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
val settingsScreenModel = remember { ReaderSettingsScreenModel() }
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
@ -409,14 +413,12 @@ class ReaderActivity : BaseActivity() {
},
)
}
is ReaderViewModel.Dialog.ColorFilter -> {
setMenuVisibility(false)
ReaderColorFilterDialog(
onDismissRequest = {
onDismissRequest()
setMenuVisibility(true)
},
readerPreferences = viewModel.readerPreferences,
is ReaderViewModel.Dialog.Settings -> {
ReaderSettingsDialog(
onDismissRequest = onDismissRequest,
onShowMenus = { setMenuVisibility(true) },
onHideMenus = { setMenuVisibility(false) },
screenModel = settingsScreenModel,
)
}
is ReaderViewModel.Dialog.PageActions -> {
@ -549,7 +551,7 @@ class ReaderActivity : BaseActivity() {
}
// Settings sheet
with(binding.actionSettings) {
with(binding.actionSettingsLegacy) {
setTooltip(R.string.action_settings)
var readerSettingSheet: ReaderSettingsSheet? = null
@ -559,13 +561,11 @@ class ReaderActivity : BaseActivity() {
readerSettingSheet = ReaderSettingsSheet(this@ReaderActivity).apply { show() }
}
}
// Color filter sheet
with(binding.actionColorSettings) {
setTooltip(R.string.custom_filter)
with(binding.actionSettings) {
setTooltip(R.string.action_settings)
setOnClickListener {
viewModel.openColorFilterDialog()
viewModel.openSettingsDialog()
}
}
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -704,8 +705,8 @@ class ReaderViewModel(
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
}
fun openColorFilterDialog() {
mutableState.update { it.copy(dialog = Dialog.ColorFilter) }
fun openSettingsDialog() {
mutableState.update { it.copy(dialog = Dialog.Settings) }
}
fun closeDialog() {
@ -809,16 +810,12 @@ class ReaderViewModel(
}
}
/**
* Results of the set as cover feature.
*/
enum class SetAsCoverResult {
Success, AddToLibraryFirst, Error
Success,
AddToLibraryFirst,
Error,
}
/**
* Results of the save image feature.
*/
sealed class SaveImageResult {
class Success(val uri: Uri) : SaveImageResult()
class Error(val error: Throwable) : SaveImageResult()
@ -893,6 +890,7 @@ class ReaderViewModel(
}
}
@Immutable
data class State(
val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null,
@ -912,7 +910,7 @@ class ReaderViewModel(
sealed class Dialog {
object Loading : Dialog()
object ColorFilter : Dialog()
object Settings : Dialog()
data class PageActions(val page: ReaderPage) : Dialog()
}

View file

@ -1,164 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.PreferenceScreen
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.getAndSet
@Composable
fun ReaderColorFilterDialog(
onDismissRequest: () -> Unit,
readerPreferences: ReaderPreferences,
) {
val colorFilterModes = buildList {
addAll(
listOf(
R.string.label_default,
R.string.filter_mode_multiply,
R.string.filter_mode_screen,
),
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
addAll(
listOf(
R.string.filter_mode_overlay,
R.string.filter_mode_lighten,
R.string.filter_mode_darken,
),
)
}
}.map { stringResource(it) }
val customBrightness by readerPreferences.customBrightness().collectAsState()
val customBrightnessValue by readerPreferences.customBrightnessValue().collectAsState()
val colorFilter by readerPreferences.colorFilter().collectAsState()
val colorFilterValue by readerPreferences.colorFilterValue().collectAsState()
val colorFilterMode by readerPreferences.colorFilterMode().collectAsState()
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
(LocalView.current.parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
CompositionLocalProvider(
LocalPreferenceMinHeight provides 48.dp,
) {
PreferenceScreen(
items = listOfNotNull(
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.customBrightness(),
title = stringResource(R.string.pref_custom_brightness),
),
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
*/
Preference.PreferenceItem.SliderPreference(
value = customBrightnessValue,
title = stringResource(R.string.pref_custom_brightness),
min = -75,
max = 100,
onValueChanged = {
readerPreferences.customBrightnessValue().set(it)
true
},
).takeIf { customBrightness },
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.colorFilter(),
title = stringResource(R.string.pref_custom_color_filter),
),
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.red,
title = stringResource(R.string.color_filter_r_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, RED_MASK, 16)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.green,
title = stringResource(R.string.color_filter_g_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, GREEN_MASK, 8)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.blue,
title = stringResource(R.string.color_filter_b_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, BLUE_MASK, 0)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SliderPreference(
value = colorFilterValue.alpha,
title = stringResource(R.string.color_filter_a_value),
max = 255,
onValueChanged = { newRValue ->
readerPreferences.colorFilterValue().getAndSet {
getColorValue(it, newRValue, ALPHA_MASK, 24)
}
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.BasicListPreference(
value = colorFilterMode.toString(),
title = stringResource(R.string.pref_color_filter_mode),
entries = colorFilterModes
.mapIndexed { index, mode -> index.toString() to mode }
.toMap(),
onValueChanged = { newValue ->
readerPreferences.colorFilterMode().set(newValue.toInt())
true
},
).takeIf { colorFilter },
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.grayscale(),
title = stringResource(R.string.pref_grayscale),
),
Preference.PreferenceItem.SwitchPreference(
pref = readerPreferences.invertedColors(),
title = stringResource(R.string.pref_inverted_colors),
),
),
)
}
}
}
private fun getColorValue(currentColor: Int, color: Int, mask: Long, bitShift: Int): Int {
return (color shl bitShift) or (currentColor and mask.inv().toInt())
}
private const val ALPHA_MASK: Long = 0xFF000000
private const val RED_MASK: Long = 0x00FF0000
private const val GREEN_MASK: Long = 0x0000FF00
private const val BLUE_MASK: Long = 0x000000FF

View file

@ -1,53 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderGeneralSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.preference.bindToPreference
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
/**
* Sheet to show reader and viewer preferences.
*/
class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestedScrollView(context, attrs) {
private val readerPreferences: ReaderPreferences by injectLazy()
private val binding = ReaderGeneralSettingsBinding.inflate(LayoutInflater.from(context), this, false)
init {
addView(binding.root)
initGeneralPreferences()
}
/**
* Init general reader preferences.
*/
private fun initGeneralPreferences() {
binding.backgroundColor.bindToIntPreference(readerPreferences.readerTheme(), R.array.reader_themes_values)
binding.showPageNumber.bindToPreference(readerPreferences.showPageNumber())
binding.fullscreen.bindToPreference(readerPreferences.fullscreen())
readerPreferences.fullscreen().changes()
.onEach {
// If the preference is explicitly disabled, that means the setting was configured since there is a cutout
binding.cutoutShort.isVisible = it && ((context as ReaderActivity).hasCutout || !readerPreferences.cutoutShort().get())
binding.cutoutShort.bindToPreference(readerPreferences.cutoutShort())
}
.launchIn((context as ReaderActivity).lifecycleScope)
binding.keepscreen.bindToPreference(readerPreferences.keepScreenOn())
binding.longTap.bindToPreference(readerPreferences.readWithLongTap())
binding.alwaysShowChapterTransition.bindToPreference(readerPreferences.alwaysShowChapterTransition())
binding.pageTransitions.bindToPreference(readerPreferences.pageTransitions())
}
}

View file

@ -54,7 +54,7 @@ class ReaderPreferences(
fun cropBordersWebtoon() = preferenceStore.getBoolean("crop_borders_webtoon", false)
fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", 0)
fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", WEBTOON_PADDING_MIN)
fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", ReaderHideThreshold.LOW)
@ -137,4 +137,9 @@ class ReaderPreferences(
LOW(31),
LOWEST(47),
}
companion object {
const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25
}
}

View file

@ -1,145 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import eu.kanade.domain.entries.manga.model.orientationType
import eu.kanade.domain.entries.manga.model.readingModeType
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.util.preference.bindToPreference
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
/**
* Sheet to show reader and viewer preferences.
*/
class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestedScrollView(context, attrs) {
private val readerPreferences: ReaderPreferences by injectLazy()
private val binding = ReaderReadingModeSettingsBinding.inflate(LayoutInflater.from(context), this, false)
init {
addView(binding.root)
initGeneralPreferences()
when ((context as ReaderActivity).viewModel.state.value.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}
}
/**
* Init general reader preferences.
*/
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = { position ->
val readingModeType = ReadingModeType.fromSpinner(position)
(context as ReaderActivity).viewModel.setMangaReadingMode(readingModeType.flagValue)
val mangaViewer = (context as ReaderActivity).viewModel.getMangaReadingMode()
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
initWebtoonPreferences()
} else {
initPagerPreferences()
}
}
binding.viewer.setSelection((context as ReaderActivity).viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
binding.rotationMode.onItemSelectedListener = { position ->
val rotationType = OrientationType.fromSpinner(position)
(context as ReaderActivity).viewModel.setMangaOrientationType(rotationType.flagValue)
}
binding.rotationMode.setSelection((context as ReaderActivity).viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue)
}
/**
* Init the preferences for the pager reader.
*/
private fun initPagerPreferences() {
binding.webtoonPrefsGroup.root.isVisible = false
binding.pagerPrefsGroup.root.isVisible = true
binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.pagerPrefsGroup.navigatePan.bindToPreference(readerPreferences.navigateToPan())
binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager())
readerPreferences.navigationModePager().changes()
.onEach {
val isTappingEnabled = it != 5
binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled
binding.pagerPrefsGroup.navigatePan.isVisible = isTappingEnabled
}
.launchIn((context as ReaderActivity).lifecycleScope)
// Makes so that landscape zoom gets hidden away when image scale type is not fit screen
binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1)
readerPreferences.imageScaleType().changes()
.onEach { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 }
.launchIn((context as ReaderActivity).lifecycleScope)
binding.pagerPrefsGroup.landscapeZoom.bindToPreference(readerPreferences.landscapeZoom())
binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1)
binding.pagerPrefsGroup.cropBorders.bindToPreference(readerPreferences.cropBorders())
binding.pagerPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitPaged())
readerPreferences.dualPageSplitPaged().changes()
.onEach {
binding.pagerPrefsGroup.dualPageInvert.isVisible = it
if (it) {
binding.pagerPrefsGroup.dualPageRotateToFit.isChecked = false
}
}
.launchIn((context as ReaderActivity).lifecycleScope)
binding.pagerPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertPaged())
binding.pagerPrefsGroup.dualPageRotateToFit.bindToPreference(readerPreferences.dualPageRotateToFit())
readerPreferences.dualPageRotateToFit().changes()
.onEach {
binding.pagerPrefsGroup.dualPageRotateToFitInvert.isVisible = it
if (it) {
binding.pagerPrefsGroup.dualPageSplit.isChecked = false
}
}
.launchIn((context as ReaderActivity).lifecycleScope)
binding.pagerPrefsGroup.dualPageRotateToFitInvert.bindToPreference(readerPreferences.dualPageRotateToFitInvert())
}
/**
* Init the preferences for the webtoon reader.
*/
private fun initWebtoonPreferences() {
binding.pagerPrefsGroup.root.isVisible = false
binding.webtoonPrefsGroup.root.isVisible = true
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon())
readerPreferences.navigationModeWebtoon().changes()
.onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 }
.launchIn((context as ReaderActivity).lifecycleScope)
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(readerPreferences.cropBordersWebtoon())
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(readerPreferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
binding.webtoonPrefsGroup.dualPageSplit.bindToPreference(readerPreferences.dualPageSplitWebtoon())
// Makes it so that dual page invert gets hidden away when dual page split is turned off
readerPreferences.dualPageSplitWebtoon().changes()
.onEach { binding.webtoonPrefsGroup.dualPageInvert.isVisible = it }
.launchIn((context as ReaderActivity).lifecycleScope)
binding.webtoonPrefsGroup.dualPageInvert.bindToPreference(readerPreferences.dualPageInvertWebtoon())
binding.webtoonPrefsGroup.longStripSplit.isVisible = !isReleaseBuildType
binding.webtoonPrefsGroup.longStripSplit.bindToPreference(readerPreferences.longStripSplitWebtoon())
binding.webtoonPrefsGroup.doubleTapZoom.bindToPreference(readerPreferences.webtoonDoubleTapZoomEnabled())
}
}

View file

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.reader.setting
import cafe.adriel.voyager.core.model.ScreenModel
import eu.kanade.tachiyomi.util.preference.toggle
import tachiyomi.core.preference.Preference
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel(
val preferences: ReaderPreferences = Injekt.get(),
) : ScreenModel {
fun togglePreference(preference: (ReaderPreferences) -> Preference<Boolean>) {
preference(preferences).toggle()
}
}

View file

@ -1,55 +1,89 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.CommonTabbedSheetBinding
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialog
import eu.kanade.domain.entries.manga.model.orientationType
import eu.kanade.domain.entries.manga.model.readingModeType
import eu.kanade.tachiyomi.databinding.ReaderReadingModeSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
class ReaderSettingsSheet(
private val activity: ReaderActivity,
) : BaseBottomSheetDialog(activity) {
) : BottomSheetDialog(activity) {
private val tabs = listOf(
ReaderReadingModeSettings(activity) to R.string.pref_category_reading_mode,
ReaderGeneralSettings(activity) to R.string.pref_category_general,
)
private val readerPreferences: ReaderPreferences by injectLazy()
private lateinit var binding: CommonTabbedSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = CommonTabbedSheetBinding.inflate(activity.layoutInflater)
val adapter = Adapter()
binding.pager.adapter = adapter
binding.tabs.setupWithViewPager(binding.pager)
return binding.root
}
private lateinit var binding: ReaderReadingModeSettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
behavior.isFitToContents = false
behavior.halfExpandedRatio = 0.25f
binding = ReaderReadingModeSettingsBinding.inflate(activity.layoutInflater)
setContentView(binding.root)
initGeneralPreferences()
when (activity.viewModel.state.value.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}
}
private inner class Adapter : ViewPagerAdapter() {
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = { position ->
val readingModeType = ReadingModeType.fromSpinner(position)
activity.viewModel.setMangaReadingMode(readingModeType.flagValue)
override fun createView(container: ViewGroup, position: Int): View {
return tabs[position].first
val mangaViewer = activity.viewModel.getMangaReadingMode()
if (mangaViewer == ReadingModeType.WEBTOON.flagValue || mangaViewer == ReadingModeType.CONTINUOUS_VERTICAL.flagValue) {
initWebtoonPreferences()
} else {
initPagerPreferences()
}
}
binding.viewer.setSelection(activity.viewModel.manga?.readingModeType?.let { ReadingModeType.fromPreference(it.toInt()).prefValue } ?: ReadingModeType.DEFAULT.prefValue)
binding.rotationMode.onItemSelectedListener = { position ->
val rotationType = OrientationType.fromSpinner(position)
activity.viewModel.setMangaOrientationType(rotationType.flagValue)
}
binding.rotationMode.setSelection(activity.viewModel.manga?.orientationType?.let { OrientationType.fromPreference(it.toInt()).prefValue } ?: OrientationType.DEFAULT.prefValue)
}
override fun getCount(): Int {
return tabs.size
private fun initPagerPreferences() {
binding.webtoonPrefsGroup.root.isVisible = false
binding.pagerPrefsGroup.root.isVisible = true
binding.pagerPrefsGroup.tappingInverted.bindToPreference(readerPreferences.pagerNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.pagerPrefsGroup.pagerNav.bindToPreference(readerPreferences.navigationModePager())
readerPreferences.navigationModePager().changes()
.onEach {
val isTappingEnabled = it != 5
binding.pagerPrefsGroup.tappingInverted.isVisible = isTappingEnabled
}
.launchIn(activity.lifecycleScope)
binding.pagerPrefsGroup.scaleType.bindToPreference(readerPreferences.imageScaleType(), 1)
binding.pagerPrefsGroup.zoomStart.bindToPreference(readerPreferences.zoomStart(), 1)
}
override fun getPageTitle(position: Int): CharSequence {
return activity.resources!!.getString(tabs[position].second)
}
private fun initWebtoonPreferences() {
binding.pagerPrefsGroup.root.isVisible = false
binding.webtoonPrefsGroup.root.isVisible = true
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(readerPreferences.webtoonNavInverted(), ReaderPreferences.TappingInvertMode::class.java)
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(readerPreferences.navigationModeWebtoon())
readerPreferences.navigationModeWebtoon().changes()
.onEach { binding.webtoonPrefsGroup.tappingInverted.isVisible = it != 5 }
.launchIn(activity.lifecycleScope)
}
}

View file

@ -134,7 +134,6 @@ private fun Context.defaultBrowserPackageName(): String? {
val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.resolveActivity(browserIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
} else {
@Suppress("DEPRECATION")
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
}
return resolveInfo

View file

@ -5,10 +5,7 @@ import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.view.Display
import android.view.View
import android.view.WindowManager
import androidx.core.content.getSystemService
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import uy.kohesive.injekt.Injekt
@ -67,14 +64,6 @@ fun Context.isNightMode(): Boolean {
return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
val Context.displayCompat: Display?
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
} else {
@Suppress("DEPRECATION")
getSystemService<WindowManager>()?.defaultDisplay
}
val Resources.isLTR
get() = configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR

View file

@ -7,7 +7,6 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.MenuItem
import android.widget.FrameLayout
import androidx.annotation.ArrayRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
@ -99,17 +98,6 @@ class MaterialSpinnerView @JvmOverloads constructor(context: Context, attrs: Att
}
}
fun bindToIntPreference(pref: Preference<Int>, @ArrayRes intValuesResource: Int, block: ((Int) -> Unit)? = null) {
val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() }
setSelection(intValues.indexOf(pref.get()))
popup = makeSettingsPopup(pref, intValues, block)
setOnTouchListener(popup?.dragToOpenListener)
setOnClickListener {
popup?.show()
}
}
private fun <T : Enum<T>> makeSettingsPopup(preference: Preference<T>, clazz: Class<T>): PopupMenu {
return createPopupMenu { pos ->
onItemSelectedListener?.invoke(pos)

View file

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.widget.sheet
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.getElevation
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.displayCompat
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
abstract class BaseBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
abstract fun createView(inflater: LayoutInflater): View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = createView(layoutInflater)
setContentView(rootView)
// Enforce max width for tablets
val width = context.resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
if (width > 0) {
behavior.maxWidth = width
}
// Set peek height to 50% display height
context.displayCompat?.let {
val metrics = DisplayMetrics()
it.getRealMetrics(metrics)
behavior.peekHeight = metrics.heightPixels / 2
}
// Set navbar color to transparent for edge-to-edge bottom sheet if we can use light navigation bar
// TODO Replace deprecated systemUiVisibility when material-components uses new API to modify status bar icons
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.setNavigationBarTransparentCompat(context, behavior.getElevation())
val bottomSheet = rootView.parent as ViewGroup
var flags = bottomSheet.systemUiVisibility
flags = if (context.isNightMode()) {
flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
}
bottomSheet.systemUiVisibility = flags
}
}
}

View file

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.widget.sheet
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.viewpager.widget.ViewPager
import java.lang.reflect.Field
/**
* From https://github.com/kafumi/android-bottomsheet-viewpager
*/
class BottomSheetViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : ViewPager(context, attrs) {
private val positionField: Field = LayoutParams::class.java.getDeclaredField("position").also {
it.isAccessible = true
}
override fun getChildAt(index: Int): View {
val currentView = getCurrentView() ?: return super.getChildAt(index)
return if (index == 0) {
currentView
} else {
var view = super.getChildAt(index)
if (view == currentView) {
view = super.getChildAt(0)
}
return view
}
}
private fun getCurrentView(): View? {
for (i in 0 until childCount) {
val child = super.getChildAt(i)
val lp = child.layoutParams as? LayoutParams
if (lp != null) {
val position = positionField.getInt(lp)
if (!lp.isDecor && currentItem == position) {
return child
}
}
}
return null
}
init {
addOnPageChangeListener(
object : SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
requestLayout()
}
},
)
}
}

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z" />
</vector>

View file

@ -1,51 +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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/transparent_tabs_background">
<!-- Remove background color so rounded sheet corners work -->
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabMaxWidth="0dp"
app:tabGravity="fill"
app:tabMode="fixed" />
<ImageButton
android:id="@+id/menu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_menu"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_overflow_24dp"
app:tint="?attr/colorOnSurface"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<eu.kanade.tachiyomi.widget.sheet.BottomSheetViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -120,29 +120,29 @@
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_settings"
android:id="@+id/action_settings_legacy"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@+id/action_color_settings"
app:layout_constraintEnd_toStartOf="@+id/action_settings"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_color_settings"
android:id="@+id/action_settings"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/custom_filter"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_settings"
app:layout_constraintStart_toEndOf="@id/action_settings_legacy"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_brightness_5_24dp"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/background_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/reader_themes"
app:title="@string/pref_reader_theme" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/show_page_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_show_page_number"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/fullscreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_fullscreen"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/cutout_short"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_cutout_short"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/keepscreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_keep_screen_on"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/long_tap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_read_with_long_tap"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/always_show_chapter_transition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_always_show_chapter_transition"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/page_transitions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_page_transitions"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,7 +1,6 @@
<?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">
@ -45,77 +44,4 @@
android:entries="@array/zoom_start"
app:title="@string/pref_zoom_start" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/crop_borders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_crop_borders"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/landscape_zoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_landscape_zoom"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/navigate_pan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_navigate_pan"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_split"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_dual_page_split"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_invert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_dual_page_invert"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_rotate_to_fit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_page_rotate"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_rotate_to_fit_invert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_page_rotate_invert"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/tapping_prefs_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="pager_nav,tapping_inverted,dual_page_split,dual_page_invert,dual_page_rotate_to_fit,dual_page_rotate_to_fit_invert" />
</LinearLayout>

View file

@ -36,7 +36,6 @@
android:entries="@array/rotation_type"
app:title="@string/rotation_type" />
<!-- Pager preferences -->
<include
android:id="@+id/pager_prefs_group"
layout="@layout/reader_pager_settings"
@ -45,7 +44,6 @@
android:visibility="gone"
tools:visibility="visible" />
<!-- Webtoon preferences -->
<include
android:id="@+id/webtoon_prefs_group"
layout="@layout/reader_webtoon_settings"

View file

@ -1,7 +1,6 @@
<?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">
@ -30,64 +29,4 @@
android:entries="@array/invert_tapping_mode"
app:title="@string/pref_read_with_tapping_inverted" />
<eu.kanade.tachiyomi.widget.MaterialSpinnerView
android:id="@+id/webtoon_side_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/webtoon_side_padding"
app:title="@string/pref_webtoon_side_padding" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/crop_borders_webtoon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_crop_borders"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_split"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_dual_page_split"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/dual_page_invert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:text="@string/pref_dual_page_invert"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/long_strip_split"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/pref_long_strip_split"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/double_tap_zoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/pref_double_tap_zoom"
android:textColor="?android:attr/textColorSecondary" />
<androidx.constraintlayout.widget.Group
android:id="@+id/tapping_prefs_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="webtoon_nav,tapping_inverted,dual_page_split,dual_page_invert" />
</LinearLayout>

View file

@ -1,5 +1,3 @@
<resources>
<dimen name="bottom_sheet_width">480dp</dimen>
<dimen name="screen_edge_margin">24dp</dimen>
</resources>

View file

@ -9,20 +9,6 @@
<item>@string/vertical_plus_viewer</item>
</string-array>
<string-array name="reader_themes">
<item>@string/black_background</item>
<item>@string/gray_background</item>
<item>@string/white_background</item>
<item>@string/automatic_background</item>
</string-array>
<string-array name="reader_themes_values">
<item>1</item>
<item>2</item>
<item>0</item>
<item>3</item>
</string-array>
<string-array name="image_scale_type">
<item>@string/scale_type_fit_screen</item>
<item>@string/scale_type_stretch</item>
@ -32,24 +18,6 @@
<item>@string/scale_type_smart_fit</item>
</string-array>
<string-array name="webtoon_side_padding">
<item>@string/webtoon_side_padding_0</item>
<item>@string/webtoon_side_padding_5</item>
<item>@string/webtoon_side_padding_10</item>
<item>@string/webtoon_side_padding_15</item>
<item>@string/webtoon_side_padding_20</item>
<item>@string/webtoon_side_padding_25</item>
</string-array>
<string-array name="webtoon_side_padding_values">
<item>0</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>20</item>
<item>25</item>
</string-array>
<string-array name="zoom_start">
<item>@string/zoom_start_automatic</item>
<item>@string/zoom_start_left</item>

View file

@ -1,6 +1,4 @@
<resources>
<dimen name="bottom_sheet_width">0dp</dimen>
<dimen name="dialog_radius">8dp</dimen>
<dimen name="screen_edge_margin">16dp</dimen>

View file

@ -44,6 +44,8 @@ class NetworkHelper(
builder.addNetworkInterceptor(httpLoggingInterceptor)
}
builder.addInterceptor(cloudflareInterceptor)
when (preferences.dohProvider().get()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
@ -64,12 +66,12 @@ class NetworkHelper(
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
/**
* @deprecated Since extension-lib 1.5
*/
@Deprecated("The regular client handles Cloudflare by default")
@Suppress("UNUSED")
val cloudflareClient by lazy {
client.newBuilder()
.addInterceptor(cloudflareInterceptor)
.build()
}
val cloudflareClient = client
fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim()
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package tachiyomi.domain.entries.anime.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.core.preference.TriState
import java.io.Serializable
import kotlin.math.pow
@ -54,18 +54,18 @@ data class Anime(
val nextEpisodeAiringAt: Long
get() = (viewerFlags and ANIME_AIRING_TIME_MASK).removeHexZeros(zeros = 6)
val unseenFilter: TriStateFilter
val unseenFilter: TriState
get() = when (unseenFilterRaw) {
EPISODE_SHOW_UNSEEN -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_SEEN -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
EPISODE_SHOW_UNSEEN -> TriState.ENABLED_IS
EPISODE_SHOW_SEEN -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
val bookmarkedFilter: TriStateFilter
val bookmarkedFilter: TriState
get() = when (bookmarkedFilterRaw) {
EPISODE_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
EPISODE_SHOW_BOOKMARKED -> TriState.ENABLED_IS
EPISODE_SHOW_NOT_BOOKMARKED -> TriState.ENABLED_NOT
else -> TriState.DISABLED
}
fun sortDescending(): Boolean {

View file

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

View file

@ -1,8 +1,8 @@
package tachiyomi.domain.library.service
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.TriState
import tachiyomi.core.preference.getEnum
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.library.anime.model.AnimeLibrarySort
@ -110,60 +110,60 @@ class LibraryPreferences(
// Mixture Filter
fun filterDownloadedAnime() =
preferenceStore.getEnum("pref_filter_animelib_downloaded_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_downloaded_v2", TriState.DISABLED)
fun filterDownloadedManga() =
preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_downloaded_v2", TriState.DISABLED)
fun filterUnseen() =
preferenceStore.getEnum("pref_filter_animelib_unread_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_unread_v2", TriState.DISABLED)
fun filterUnread() =
preferenceStore.getEnum("pref_filter_library_unread_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_unread_v2", TriState.DISABLED)
fun filterStartedAnime() =
preferenceStore.getEnum("pref_filter_animelib_started_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_started_v2", TriState.DISABLED)
fun filterStartedManga() =
preferenceStore.getEnum("pref_filter_library_started_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_started_v2", TriState.DISABLED)
fun filterBookmarkedAnime() =
preferenceStore.getEnum("pref_filter_animelib_bookmarked_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_bookmarked_v2", TriState.DISABLED)
fun filterBookmarkedManga() =
preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_bookmarked_v2", TriState.DISABLED)
fun filterCompletedAnime() =
preferenceStore.getEnum("pref_filter_animelib_completed_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_completed_v2", TriState.DISABLED)
fun filterCompletedManga() =
preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_completed_v2", TriState.DISABLED)
fun filterIntervalCustomAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_custom", TriStateFilter.DISABLED)
fun filterIntervalCustomAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_custom", TriState.DISABLED)
fun filterIntervalCustomManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_custom", TriStateFilter.DISABLED)
fun filterIntervalCustomManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_custom", TriState.DISABLED)
fun filterIntervalLongAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_long", TriStateFilter.DISABLED)
fun filterIntervalLongAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_long", TriState.DISABLED)
fun filterIntervalLongManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_long", TriStateFilter.DISABLED)
fun filterIntervalLongManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_long", TriState.DISABLED)
fun filterIntervalLateAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_late", TriStateFilter.DISABLED)
fun filterIntervalLateAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_late", TriState.DISABLED)
fun filterIntervalLateManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_late", TriStateFilter.DISABLED)
fun filterIntervalLateManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_late", TriState.DISABLED)
fun filterIntervalDroppedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_dropped", TriStateFilter.DISABLED)
fun filterIntervalDroppedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_dropped", TriState.DISABLED)
fun filterIntervalDroppedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_dropped", TriStateFilter.DISABLED)
fun filterIntervalDroppedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_dropped", TriState.DISABLED)
fun filterIntervalPassedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_passed", TriStateFilter.DISABLED)
fun filterIntervalPassedAnime() = preferenceStore.getEnum("pref_filter_anime_library_interval_passed", TriState.DISABLED)
fun filterIntervalPassedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_passed", TriStateFilter.DISABLED)
fun filterIntervalPassedManga() = preferenceStore.getEnum("pref_filter_manga_library_interval_passed", TriState.DISABLED)
fun filterTrackedAnime(id: Int) =
preferenceStore.getEnum("pref_filter_animelib_tracked_${id}_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_animelib_tracked_${id}_v2", TriState.DISABLED)
fun filterTrackedManga(id: Int) =
preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriStateFilter.DISABLED)
preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriState.DISABLED)
// Mixture Update Count

View file

@ -373,7 +373,7 @@
<string name="tapping_inverted_vertical">Vertical</string>
<string name="tapping_inverted_both">Both</string>
<string name="pref_reader_actions">Actions</string>
<string name="pref_read_with_long_tap">Show on long tap</string>
<string name="pref_read_with_long_tap">Show on actions long tap</string>
<string name="pref_create_folder_per_manga">Save pages into separate folders</string>
<string name="pref_create_folder_per_manga_summary">Creates folders according to entries\' title</string>
<string name="pref_reader_theme">Background color</string>
@ -431,12 +431,6 @@
<string name="pref_category_reading_mode">Reading mode</string>
<string name="pref_category_reading">Reading</string>
<string name="pref_webtoon_side_padding">Side padding</string>
<string name="webtoon_side_padding_0">None</string>
<string name="webtoon_side_padding_5">5%</string>
<string name="webtoon_side_padding_10">10%</string>
<string name="webtoon_side_padding_15">15%</string>
<string name="webtoon_side_padding_20">20%</string>
<string name="webtoon_side_padding_25">25%</string>
<string name="pref_hide_threshold">Sensitivity for hiding menu on scroll</string>
<string name="pref_highest">Highest</string>
<string name="pref_high">High</string>
@ -479,7 +473,6 @@
<string name="action_track">Track</string>
<!-- Browse section -->
<string name="pref_search_pinned_sources_only">Only search pinned sources in global search</string>
<string name="pref_hide_in_library_items">Hide entries already in library</string>
<!-- Backup section -->
@ -620,6 +613,7 @@
<string name="latest">Latest</string>
<string name="popular">Popular</string>
<string name="browse">Browse</string>
<string name="has_results">Has results</string>
<string name="local_source_help_guide">Local source guide</string>
<string name="no_pinned_sources">You have no pinned sources</string>
<string name="chapter_not_found">Chapter not found</string>

View file

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

View file

@ -1,7 +1,9 @@
package tachiyomi.presentation.core.components
import android.view.MotionEvent
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -10,22 +12,44 @@ 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.foundation.layout.widthIn
import androidx.compose.foundation.text.KeyboardOptions
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.outlined.AddCircle
import androidx.compose.material.icons.outlined.RemoveCircle
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.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import tachiyomi.core.preference.TriState
import tachiyomi.presentation.core.theme.header
object SettingsItemsPaddings {
@ -174,6 +198,274 @@ fun SliderItem(
}
}
@Composable
fun SelectItem(
label: String,
options: Array<out Any?>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
label = { Text(text = label) },
value = options[selectedIndex].toString(),
onValueChange = {},
enabled = false,
readOnly = true,
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded,
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
),
)
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
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
@Composable
fun TriStateItem(
label: String,
state: TriState,
enabled: Boolean = true,
onClick: ((TriState) -> Unit)?,
) {
Row(
modifier = Modifier
.clickable(
enabled = enabled && onClick != null,
onClick = {
when (state) {
TriState.DISABLED -> onClick?.invoke(TriState.ENABLED_IS)
TriState.ENABLED_IS -> onClick?.invoke(TriState.ENABLED_NOT)
TriState.ENABLED_NOT -> onClick?.invoke(TriState.DISABLED)
}
},
)
.fillMaxWidth()
.padding(horizontal = SettingsItemsPaddings.Horizontal, vertical = SettingsItemsPaddings.Vertical),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
Icon(
imageVector = when (state) {
TriState.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
TriState.ENABLED_IS -> Icons.Rounded.CheckBox
TriState.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
},
contentDescription = null,
tint = if (!enabled || state == TriState.DISABLED) {
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
} else {
when (onClick) {
null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled)
else -> MaterialTheme.colorScheme.primary
}
},
)
Text(
text = label,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha),
style = MaterialTheme.typography.bodyMedium,
)
}
}
@Composable
fun <T> SelectItem(
label: String,
options: Array<T>,
selectedIndex: Int,
modifier: Modifier = Modifier,
onSelect: (Int) -> Unit,
toString: (T) -> String = { it.toString() },
) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = modifier,
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
.padding(
horizontal = SettingsItemsPaddings.Horizontal,
vertical = SettingsItemsPaddings.Vertical,
),
label = { Text(text = label) },
value = toString(options[selectedIndex]),
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded,
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(
modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
expanded = expanded,
onDismissRequest = { expanded = false },
) {
options.forEachIndexed { index, option ->
DropdownMenuItem(
text = { Text(toString(option)) },
onClick = {
onSelect(index)
expanded = false
},
)
}
}
}
}
@Composable
fun RepeatingIconButton(
modifier: Modifier = Modifier,
onClick: () -> Unit,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
maxDelayMillis: Long = 750,
minDelayMillis: Long = 5,
delayDecayFactor: Float = .25f,
content: @Composable () -> Unit,
) {
val currentClickListener by rememberUpdatedState(onClick)
var pressed by remember { mutableStateOf(false) }
IconButton(
modifier = modifier.pointerInteropFilter {
pressed = when (it.action) {
MotionEvent.ACTION_DOWN -> true
else -> false
}
true
},
onClick = {},
enabled = enabled,
interactionSource = interactionSource,
content = content,
)
LaunchedEffect(pressed, enabled) {
var currentDelayMillis = maxDelayMillis
while (enabled && pressed) {
currentClickListener()
delay(currentDelayMillis)
currentDelayMillis =
(currentDelayMillis - (currentDelayMillis * delayDecayFactor))
.toLong().coerceAtLeast(minDelayMillis)
}
}
}
@Composable
fun OutlinedNumericChooser(
label: String,
placeholder: String,
suffix: String,
value: Int,
step: Int,
min: Int? = null,
onValueChanged: (Int) -> Unit,
) {
var currentValue = value
val updateValue: (Boolean) -> Unit = {
currentValue += if (it) step else -step
if (min != null) currentValue = if (currentValue < min) min else currentValue
onValueChanged(currentValue)
}
Row(verticalAlignment = Alignment.CenterVertically) {
RepeatingIconButton(
onClick = { updateValue(false) },
) { Icon(imageVector = Icons.Outlined.RemoveCircle, contentDescription = null) }
OutlinedTextField(
value = "%d".format(currentValue),
modifier = Modifier.widthIn(min = 140.dp),
onValueChange = {
// Don't allow multiple decimal points, non-numeric characters, or leading zeros
currentValue = it.trim().replace(Regex("[^-\\d.]"), "").toIntOrNull()
?: currentValue
onValueChanged(currentValue)
},
label = { Text(text = label) },
placeholder = { Text(text = placeholder) },
suffix = { Text(text = suffix) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
RepeatingIconButton(
onClick = { updateValue(true) },
) { Icon(imageVector = Icons.Outlined.AddCircle, contentDescription = null) }
}
}
@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,
)
}
@Composable
private fun BaseSettingsItem(
label: String,
@ -195,20 +487,3 @@ private fun BaseSettingsItem(
)
}
}
@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,
)
}

View file

@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.SecondaryIndicator
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -51,8 +51,8 @@ fun TabIndicator(
currentTabPosition: TabPosition,
currentPageOffsetFraction: Float,
) {
TabRowDefaults.Indicator(
Modifier
SecondaryIndicator(
modifier = Modifier
.tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction)
.padding(horizontal = 8.dp)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),