mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
parent
096276cd68
commit
4709cbedba
54 changed files with 887 additions and 800 deletions
|
@ -61,18 +61,6 @@ fun GlobalSearchResultItem(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchEmptyResultItem() {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchLoadingResultItem() {
|
||||
Box(
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchToolbar(
|
||||
searchQuery: String?,
|
||||
progress: Int,
|
||||
total: Int,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
) {
|
||||
Box {
|
||||
SearchToolbar(
|
||||
searchQuery = searchQuery,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
if (progress in 1 until total) {
|
||||
LinearProgressIndicator(
|
||||
progress = progress / total.toFloat(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.anime.components.BaseAnimeSourceItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesState
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.AnimeSourcesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||
|
@ -40,7 +40,7 @@ import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
|||
|
||||
@Composable
|
||||
fun AnimeSourcesScreen(
|
||||
state: AnimeSourcesState,
|
||||
state: AnimeSourcesScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (AnimeSource, Listing) -> Unit,
|
||||
onClickPin: (AnimeSource) -> Unit,
|
||||
|
|
|
@ -1,35 +1,14 @@
|
|||
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
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchToolbar
|
||||
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.AnimeSourceFilter
|
||||
|
@ -37,12 +16,10 @@ import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.GlobalAnimeSearch
|
|||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun GlobalAnimeSearchScreen(
|
||||
state: GlobalAnimeSearchScreenModel.State,
|
||||
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
|
@ -55,78 +32,23 @@ fun GlobalAnimeSearchScreen(
|
|||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
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()
|
||||
}
|
||||
GlobalAnimeSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
GlobalAnimeSearchContent(
|
||||
items = items,
|
||||
GlobalSearchContent(
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getAnime = getAnime,
|
||||
onClickSource = onClickSource,
|
||||
|
@ -137,7 +59,8 @@ fun GlobalAnimeSearchScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun GlobalAnimeSearchContent(
|
||||
internal fun GlobalSearchContent(
|
||||
fromSourceId: Long? = null,
|
||||
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getAnime: @Composable (Anime) -> State<Anime>,
|
||||
|
@ -151,7 +74,7 @@ private fun GlobalAnimeSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = source.name,
|
||||
title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
|
@ -160,18 +83,6 @@ private fun GlobalAnimeSearchContent(
|
|||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is AnimeSearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalAnimeSearchCardRow(
|
||||
titles = result.result,
|
||||
getAnime = getAnime,
|
||||
|
|
|
@ -1,49 +1,47 @@
|
|||
package eu.kanade.presentation.browse.anime
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.GlobalSearchEmptyResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchCardRow
|
||||
import eu.kanade.presentation.browse.anime.components.GlobalAnimeSearchToolbar
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.migration.search.MigrateAnimeSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.migration.search.MigrateAnimeSearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun MigrateAnimeSearchScreen(
|
||||
state: MigrateAnimeSearchScreenModel.State,
|
||||
navigateUp: () -> Unit,
|
||||
state: MigrateAnimeSearchState,
|
||||
getAnime: @Composable (Anime) -> State<Anime>,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
|
||||
onToggleResults: () -> Unit,
|
||||
getAnime: @Composable (Anime) -> State<Anime>,
|
||||
onClickSource: (AnimeCatalogueSource) -> Unit,
|
||||
onClickItem: (Anime) -> Unit,
|
||||
onLongClickItem: (Anime) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
GlobalAnimeSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MigrateAnimeSearchContent(
|
||||
sourceId = state.anime?.source ?: -1,
|
||||
items = state.items,
|
||||
GlobalSearchContent(
|
||||
fromSourceId = state.anime?.source ?: -1,
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getAnime = getAnime,
|
||||
onClickSource = onClickSource,
|
||||
|
@ -52,50 +50,3 @@ fun MigrateAnimeSearchScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrateAnimeSearchContent(
|
||||
sourceId: Long,
|
||||
items: Map<AnimeCatalogueSource, AnimeSearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getAnime: @Composable (Anime) -> State<Anime>,
|
||||
onClickSource: (AnimeCatalogueSource) -> Unit,
|
||||
onClickItem: (Anime) -> Unit,
|
||||
onLongClickItem: (Anime) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = if (source.id == sourceId) "▶ ${source.name}" else source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
AnimeSearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is AnimeSearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
GlobalSearchEmptyResultItem()
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalAnimeSearchCardRow(
|
||||
titles = result.result,
|
||||
getAnime = getAnime,
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is AnimeSearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
package eu.kanade.presentation.browse.anime.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.presentation.browse.GlobalSearchCard
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
import eu.kanade.presentation.library.EntryComfortableGridItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.AnimeCover
|
||||
import tachiyomi.domain.entries.anime.model.asAnimeCover
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
|
@ -20,13 +31,18 @@ fun GlobalAnimeSearchCardRow(
|
|||
onClick: (Anime) -> Unit,
|
||||
onLongClick: (Anime) -> Unit,
|
||||
) {
|
||||
if (titles.isEmpty()) {
|
||||
EmptyResultItem()
|
||||
return
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getAnime(it)
|
||||
GlobalSearchCard(
|
||||
AnimeItem(
|
||||
title = title.title,
|
||||
cover = title.asAnimeCover(),
|
||||
isFavorite = title.favorite,
|
||||
|
@ -36,3 +52,37 @@ fun GlobalAnimeSearchCardRow(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimeItem(
|
||||
title: String,
|
||||
cover: AnimeCover,
|
||||
isFavorite: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.width(96.dp)) {
|
||||
EntryComfortableGridItem(
|
||||
title = title,
|
||||
coverData = cover,
|
||||
coverBadgeStart = {
|
||||
InLibraryBadge(enabled = isFavorite)
|
||||
},
|
||||
coverAlpha = if (isFavorite) CommonEntryItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyResultItem() {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package eu.kanade.presentation.browse.anime.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
|
||||
import tachiyomi.presentation.core.components.material.VerticalDivider
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun GlobalAnimeSearchToolbar(
|
||||
searchQuery: String?,
|
||||
progress: Int,
|
||||
total: Int,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
sourceFilter: AnimeSourceFilter,
|
||||
onChangeSearchFilter: (AnimeSourceFilter) -> Unit,
|
||||
onlyShowHasResults: Boolean,
|
||||
onToggleResults: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
) {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
Box {
|
||||
SearchToolbar(
|
||||
searchQuery = searchQuery,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
if (progress in 1 until total) {
|
||||
LinearProgressIndicator(
|
||||
progress = progress / total.toFloat(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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 = 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))
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
FilterChip(
|
||||
selected = 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()
|
||||
}
|
||||
}
|
|
@ -1,35 +1,14 @@
|
|||
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
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.GlobalMangaSearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult
|
||||
|
@ -37,12 +16,10 @@ 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
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun GlobalMangaSearchScreen(
|
||||
state: GlobalMangaSearchScreenModel.State,
|
||||
items: Map<CatalogueSource, MangaSearchItemResult>,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
|
@ -55,78 +32,23 @@ fun GlobalMangaSearchScreen(
|
|||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
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()
|
||||
}
|
||||
GlobalMangaSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
GlobalSearchContent(
|
||||
items = items,
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
|
@ -137,7 +59,8 @@ fun GlobalMangaSearchScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun GlobalSearchContent(
|
||||
internal fun GlobalSearchContent(
|
||||
fromSourceId: Long? = null,
|
||||
items: Map<CatalogueSource, MangaSearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
|
@ -151,7 +74,7 @@ private fun GlobalSearchContent(
|
|||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = source.name,
|
||||
title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
|
@ -160,18 +83,6 @@ private fun GlobalSearchContent(
|
|||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is MangaSearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalMangaSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = getManga,
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesState
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.manga.model.Pin
|
||||
|
@ -40,7 +40,7 @@ import tachiyomi.source.local.entries.manga.LocalMangaSource
|
|||
|
||||
@Composable
|
||||
fun MangaSourcesScreen(
|
||||
state: MangaSourcesState,
|
||||
state: MangaSourcesScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source, Listing) -> Unit,
|
||||
onClickPin: (Source) -> Unit,
|
||||
|
|
|
@ -1,49 +1,47 @@
|
|||
package eu.kanade.presentation.browse.manga
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.GlobalSearchEmptyResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchCardRow
|
||||
import eu.kanade.presentation.browse.manga.components.GlobalMangaSearchToolbar
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.migration.search.MigrateMangaSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.migration.search.MigrateSearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaSearchScreen(
|
||||
state: MigrateSearchScreenModel.State,
|
||||
navigateUp: () -> Unit,
|
||||
state: MigrateMangaSearchState,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onChangeSearchFilter: (MangaSourceFilter) -> Unit,
|
||||
onToggleResults: () -> Unit,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
GlobalMangaSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MigrateMangaSearchContent(
|
||||
sourceId = state.manga?.source ?: -1,
|
||||
items = state.items,
|
||||
GlobalSearchContent(
|
||||
fromSourceId = state.manga?.source,
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
|
@ -52,50 +50,3 @@ fun MigrateMangaSearchScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaSearchContent(
|
||||
sourceId: Long,
|
||||
items: Map<CatalogueSource, MangaSearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = if (source.id == sourceId) "▶ ${source.name}" else source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
MangaSearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is MangaSearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
GlobalSearchEmptyResultItem()
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalMangaSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = getManga,
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is MangaSearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
package eu.kanade.presentation.browse.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.presentation.browse.GlobalSearchCard
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.library.CommonEntryItemDefaults
|
||||
import eu.kanade.presentation.library.EntryComfortableGridItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.MangaCover
|
||||
import tachiyomi.domain.entries.manga.model.asMangaCover
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
|
@ -20,13 +31,18 @@ fun GlobalMangaSearchCardRow(
|
|||
onClick: (Manga) -> Unit,
|
||||
onLongClick: (Manga) -> Unit,
|
||||
) {
|
||||
if (titles.isEmpty()) {
|
||||
EmptyResultItem()
|
||||
return
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getManga(it)
|
||||
GlobalSearchCard(
|
||||
MangaItem(
|
||||
title = title.title,
|
||||
cover = title.asMangaCover(),
|
||||
isFavorite = title.favorite,
|
||||
|
@ -36,3 +52,37 @@ fun GlobalMangaSearchCardRow(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MangaItem(
|
||||
title: String,
|
||||
cover: MangaCover,
|
||||
isFavorite: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.width(96.dp)) {
|
||||
EntryComfortableGridItem(
|
||||
title = title,
|
||||
coverData = cover,
|
||||
coverBadgeStart = {
|
||||
InLibraryBadge(enabled = isFavorite)
|
||||
},
|
||||
coverAlpha = if (isFavorite) CommonEntryItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyResultItem() {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package eu.kanade.presentation.browse.manga.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
|
||||
import tachiyomi.presentation.core.components.material.VerticalDivider
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun GlobalMangaSearchToolbar(
|
||||
searchQuery: String?,
|
||||
progress: Int,
|
||||
total: Int,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
sourceFilter: MangaSourceFilter,
|
||||
onChangeSearchFilter: (MangaSourceFilter) -> Unit,
|
||||
onlyShowHasResults: Boolean,
|
||||
onToggleResults: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
) {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
Box {
|
||||
SearchToolbar(
|
||||
searchQuery = searchQuery,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
if (progress in 1 until total) {
|
||||
LinearProgressIndicator(
|
||||
progress = progress / total.toFloat(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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 = 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))
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
FilterChip(
|
||||
selected = 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()
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
@ -72,6 +73,7 @@ fun NavigatorAdaptiveSheet(
|
|||
*/
|
||||
@Composable
|
||||
fun AdaptiveSheet(
|
||||
modifier: Modifier = Modifier,
|
||||
hideSystemBars: Boolean = false,
|
||||
tonalElevation: Dp = 1.dp,
|
||||
enableSwipeDismiss: Boolean = true,
|
||||
|
@ -91,6 +93,7 @@ fun AdaptiveSheet(
|
|||
}
|
||||
}
|
||||
AdaptiveSheetImpl(
|
||||
modifier = modifier,
|
||||
isTabletUi = isTabletUi,
|
||||
tonalElevation = tonalElevation,
|
||||
enableSwipeDismiss = enableSwipeDismiss,
|
||||
|
|
|
@ -41,6 +41,7 @@ object TabbedDialogPaddings {
|
|||
|
||||
@Composable
|
||||
fun TabbedDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
onDismissRequest: () -> Unit,
|
||||
tabTitles: List<String>,
|
||||
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
|
||||
|
@ -52,6 +53,7 @@ fun TabbedDialog(
|
|||
) {
|
||||
AdaptiveSheet(
|
||||
hideSystemBars = hideSystemBars,
|
||||
modifier = modifier,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
|
@ -27,7 +27,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
|
|||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.HeadingItem
|
||||
import tachiyomi.presentation.core.components.SettingsFlowRow
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
|
@ -182,7 +182,7 @@ private fun ColumnScope.DisplayPage(
|
|||
screenModel: AnimeLibrarySettingsScreenModel,
|
||||
) {
|
||||
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
|
||||
SettingsFlowRow(R.string.action_display_mode) {
|
||||
SettingsChipRow(R.string.action_display_mode) {
|
||||
displayModes.map { (titleRes, mode) ->
|
||||
FilterChip(
|
||||
selected = displayMode == mode,
|
||||
|
|
|
@ -27,7 +27,7 @@ import tachiyomi.domain.library.model.LibraryDisplayMode
|
|||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.HeadingItem
|
||||
import tachiyomi.presentation.core.components.SettingsFlowRow
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import tachiyomi.presentation.core.components.SortItem
|
||||
import tachiyomi.presentation.core.components.TriStateItem
|
||||
|
@ -181,7 +181,7 @@ private fun ColumnScope.DisplayPage(
|
|||
screenModel: MangaLibrarySettingsScreenModel,
|
||||
) {
|
||||
val displayMode by screenModel.libraryPreferences.libraryDisplayMode().collectAsState()
|
||||
SettingsFlowRow(R.string.action_display_mode) {
|
||||
SettingsChipRow(R.string.action_display_mode) {
|
||||
displayModes.map { (titleRes, mode) ->
|
||||
FilterChip(
|
||||
selected = displayMode == mode,
|
||||
|
|
|
@ -17,7 +17,7 @@ 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.SettingsFlowRow
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
|
||||
@Composable
|
||||
|
@ -124,7 +124,7 @@ internal fun ColumnScope.ColorFilterPage(screenModel: ReaderSettingsScreenModel)
|
|||
)
|
||||
|
||||
val colorFilterMode by screenModel.preferences.colorFilterMode().collectAsState()
|
||||
SettingsFlowRow(R.string.pref_color_filter_mode) {
|
||||
SettingsChipRow(R.string.pref_color_filter_mode) {
|
||||
colorFilterModes.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = colorFilterMode == index,
|
||||
|
|
|
@ -11,7 +11,7 @@ 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.SettingsFlowRow
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
|
||||
private val themes = listOf(
|
||||
R.string.black_background to 1,
|
||||
|
@ -23,7 +23,7 @@ private val themes = listOf(
|
|||
@Composable
|
||||
internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
||||
val readerTheme by screenModel.preferences.readerTheme().collectAsState()
|
||||
SettingsFlowRow(R.string.pref_reader_theme) {
|
||||
SettingsChipRow(R.string.pref_reader_theme) {
|
||||
themes.map { (labelRes, value) ->
|
||||
FilterChip(
|
||||
selected = readerTheme == value,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
@ -30,35 +32,38 @@ fun ReaderSettingsDialog(
|
|||
)
|
||||
val pagerState = rememberPagerState { tabTitles.size }
|
||||
|
||||
TabbedDialog(
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
onShowMenus()
|
||||
},
|
||||
tabTitles = tabTitles,
|
||||
pagerState = pagerState,
|
||||
) { page ->
|
||||
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage == 2) {
|
||||
window?.setDimAmount(0f)
|
||||
onHideMenus()
|
||||
} else {
|
||||
window?.setDimAmount(0.5f)
|
||||
BoxWithConstraints {
|
||||
TabbedDialog(
|
||||
modifier = Modifier.heightIn(max = maxHeight * 0.75f),
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
onShowMenus()
|
||||
}
|
||||
}
|
||||
},
|
||||
tabTitles = tabTitles,
|
||||
pagerState = pagerState,
|
||||
) { page ->
|
||||
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ReadingModePage(screenModel)
|
||||
1 -> GeneralPage(screenModel)
|
||||
2 -> ColorFilterPage(screenModel)
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage == 2) {
|
||||
window?.setDimAmount(0f)
|
||||
onHideMenus()
|
||||
} else {
|
||||
window?.setDimAmount(0.5f)
|
||||
onShowMenus()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TabbedDialogPaddings.Vertical)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ReadingModePage(screenModel)
|
||||
1 -> GeneralPage(screenModel)
|
||||
2 -> ColorFilterPage(screenModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package eu.kanade.presentation.reader.settings
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -18,7 +20,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
|
|||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.presentation.core.components.CheckboxItem
|
||||
import tachiyomi.presentation.core.components.HeadingItem
|
||||
import tachiyomi.presentation.core.components.SelectItem
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.SliderItem
|
||||
import java.text.NumberFormat
|
||||
|
||||
|
@ -32,21 +34,25 @@ internal fun ColumnScope.ReadingModePage(screenModel: ReaderSettingsScreenModel)
|
|||
val manga by screenModel.mangaFlow.collectAsState()
|
||||
|
||||
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_category_reading_mode),
|
||||
options = readingModeOptions.map { stringResource(it.first) }.toTypedArray(),
|
||||
selectedIndex = readingModeOptions.indexOfFirst { it.second == readingMode },
|
||||
) {
|
||||
screenModel.onChangeReadingMode(readingModeOptions[it].second)
|
||||
SettingsChipRow(R.string.pref_category_reading_mode) {
|
||||
readingModeOptions.map { (stringRes, it) ->
|
||||
FilterChip(
|
||||
selected = it == readingMode,
|
||||
onClick = { screenModel.onChangeReadingMode(it) },
|
||||
label = { Text(stringResource(stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
|
||||
SelectItem(
|
||||
label = stringResource(R.string.rotation_type),
|
||||
options = orientationTypeOptions.map { stringResource(it.first) }.toTypedArray(),
|
||||
selectedIndex = orientationTypeOptions.indexOfFirst { it.second == orientationType },
|
||||
) {
|
||||
screenModel.onChangeOrientation(orientationTypeOptions[it].second)
|
||||
SettingsChipRow(R.string.rotation_type) {
|
||||
orientationTypeOptions.map { (stringRes, it) ->
|
||||
FilterChip(
|
||||
selected = it == orientationType,
|
||||
onClick = { screenModel.onChangeOrientation(it) },
|
||||
label = { Text(stringResource(stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val viewer by screenModel.viewerFlow.collectAsState()
|
||||
|
@ -62,40 +68,35 @@ private fun ColumnScope.PagerViewerSettings(screenModel: ReaderSettingsScreenMod
|
|||
HeadingItem(R.string.pager_viewer)
|
||||
|
||||
val navigationModePager by screenModel.preferences.navigationModePager().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_viewer_nav),
|
||||
options = ReaderPreferences.TapZones.map { stringResource(it) }.toTypedArray(),
|
||||
selectedIndex = navigationModePager,
|
||||
onSelect = { screenModel.preferences.navigationModePager().set(it) },
|
||||
val pagerNavInverted by screenModel.preferences.pagerNavInverted().collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModePager,
|
||||
onSelect = screenModel.preferences.navigationModePager()::set,
|
||||
invertMode = pagerNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.pagerNavInverted()::set,
|
||||
)
|
||||
|
||||
if (navigationModePager != 5) {
|
||||
val pagerNavInverted by screenModel.preferences.pagerNavInverted().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_read_with_tapping_inverted),
|
||||
options = tappingInvertModeOptions.map { stringResource(it.first) }.toTypedArray(),
|
||||
selectedIndex = tappingInvertModeOptions.indexOfFirst { it.second == pagerNavInverted },
|
||||
onSelect = {
|
||||
screenModel.preferences.pagerNavInverted().set(tappingInvertModeOptions[it].second)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val imageScaleType by screenModel.preferences.imageScaleType().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_image_scale_type),
|
||||
options = ReaderPreferences.ImageScaleType.map { stringResource(it) }.toTypedArray(),
|
||||
selectedIndex = imageScaleType - 1,
|
||||
onSelect = { screenModel.preferences.imageScaleType().set(it + 1) },
|
||||
)
|
||||
SettingsChipRow(R.string.pref_image_scale_type) {
|
||||
ReaderPreferences.ImageScaleType.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = imageScaleType == index + 1,
|
||||
onClick = { screenModel.preferences.imageScaleType().set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val zoomStart by screenModel.preferences.zoomStart().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_zoom_start),
|
||||
options = ReaderPreferences.ZoomStart.map { stringResource(it) }.toTypedArray(),
|
||||
selectedIndex = zoomStart - 1,
|
||||
onSelect = { screenModel.preferences.zoomStart().set(it + 1) },
|
||||
)
|
||||
SettingsChipRow(R.string.pref_zoom_start) {
|
||||
ReaderPreferences.ZoomStart.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = zoomStart == index + 1,
|
||||
onClick = { screenModel.preferences.zoomStart().set(index + 1) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val cropBorders by screenModel.preferences.cropBorders().collectAsState()
|
||||
CheckboxItem(
|
||||
|
@ -172,25 +173,14 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||
HeadingItem(R.string.webtoon_viewer)
|
||||
|
||||
val navigationModeWebtoon by screenModel.preferences.navigationModeWebtoon().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_viewer_nav),
|
||||
options = ReaderPreferences.TapZones.map { stringResource(it) }.toTypedArray(),
|
||||
selectedIndex = navigationModeWebtoon,
|
||||
onSelect = { screenModel.preferences.navigationModeWebtoon().set(it) },
|
||||
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted().collectAsState()
|
||||
TapZonesItems(
|
||||
selected = navigationModeWebtoon,
|
||||
onSelect = screenModel.preferences.navigationModeWebtoon()::set,
|
||||
invertMode = webtoonNavInverted,
|
||||
onSelectInvertMode = screenModel.preferences.webtoonNavInverted()::set,
|
||||
)
|
||||
|
||||
if (navigationModeWebtoon != 5) {
|
||||
val webtoonNavInverted by screenModel.preferences.webtoonNavInverted().collectAsState()
|
||||
SelectItem(
|
||||
label = stringResource(R.string.pref_read_with_tapping_inverted),
|
||||
options = tappingInvertModeOptions.map { stringResource(it.first) }.toTypedArray(),
|
||||
selectedIndex = tappingInvertModeOptions.indexOfFirst { it.second == webtoonNavInverted },
|
||||
onSelect = {
|
||||
screenModel.preferences.webtoonNavInverted().set(tappingInvertModeOptions[it].second)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val webtoonSidePadding by screenModel.preferences.webtoonSidePadding().collectAsState()
|
||||
SliderItem(
|
||||
label = stringResource(R.string.pref_webtoon_side_padding),
|
||||
|
@ -254,3 +244,33 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.TapZonesItems(
|
||||
selected: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
invertMode: ReaderPreferences.TappingInvertMode,
|
||||
onSelectInvertMode: (ReaderPreferences.TappingInvertMode) -> Unit,
|
||||
) {
|
||||
SettingsChipRow(R.string.pref_viewer_nav) {
|
||||
ReaderPreferences.TapZones.mapIndexed { index, it ->
|
||||
FilterChip(
|
||||
selected = selected == index,
|
||||
onClick = { onSelect(index) },
|
||||
label = { Text(stringResource(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selected != 5) {
|
||||
SettingsChipRow(R.string.pref_read_with_tapping_inverted) {
|
||||
tappingInvertModeOptions.map { (stringRes, mode) ->
|
||||
FilterChip(
|
||||
selected = mode == invertMode,
|
||||
onClick = { onSelectInvertMode(mode) },
|
||||
label = { Text(stringResource(stringRes)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
object BackupConst {
|
||||
|
||||
private const val NAME = "BackupRestoreServices"
|
||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||
|
||||
// Filter options
|
||||
internal const val BACKUP_CATEGORY = 0x1
|
||||
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||
internal const val BACKUP_CHAPTER = 0x2
|
||||
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||
internal const val BACKUP_HISTORY = 0x4
|
||||
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||
internal const val BACKUP_TRACK = 0x8
|
||||
internal const val BACKUP_TRACK_MASK = 0x8
|
||||
internal const val BACKUP_PREFS = 0x10
|
||||
internal const val BACKUP_PREFS_MASK = 0x10
|
||||
internal const val BACKUP_EXT_PREFS = 0x20
|
||||
internal const val BACKUP_EXT_PREFS_MASK = 0x20
|
||||
internal const val BACKUP_EXTENSIONS = 0x40
|
||||
internal const val BACKUP_EXTENSIONS_MASK = 0x40
|
||||
internal const val BACKUP_ALL = 0x7F
|
||||
// Filter options
|
||||
internal object BackupConst {
|
||||
const val BACKUP_CATEGORY = 0x1
|
||||
const val BACKUP_CATEGORY_MASK = 0x1
|
||||
const val BACKUP_CHAPTER = 0x2
|
||||
const val BACKUP_CHAPTER_MASK = 0x2
|
||||
const val BACKUP_HISTORY = 0x4
|
||||
const val BACKUP_HISTORY_MASK = 0x4
|
||||
const val BACKUP_TRACK = 0x8
|
||||
const val BACKUP_TRACK_MASK = 0x8
|
||||
const val BACKUP_PREFS = 0x10
|
||||
const val BACKUP_PREFS_MASK = 0x10
|
||||
const val BACKUP_EXT_PREFS = 0x20
|
||||
const val BACKUP_EXT_PREFS_MASK = 0x20
|
||||
const val BACKUP_EXTENSIONS = 0x40
|
||||
const val BACKUP_EXTENSIONS_MASK = 0x40
|
||||
const val BACKUP_ALL = 0x7F
|
||||
}
|
||||
|
|
|
@ -76,7 +76,9 @@ import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
|||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
|
||||
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
|
@ -102,6 +104,8 @@ class BackupManager(
|
|||
private val getAnimeCategories: GetAnimeCategories = Injekt.get()
|
||||
private val getMangaFavorites: GetMangaFavorites = Injekt.get()
|
||||
private val getAnimeFavorites: GetAnimeFavorites = Injekt.get()
|
||||
private val getMangaHistory: GetMangaHistory = Injekt.get()
|
||||
private val getAnimeHistory: GetAnimeHistory = Injekt.get()
|
||||
|
||||
internal val parser = ProtoBuf
|
||||
|
||||
|
@ -289,11 +293,11 @@ class BackupManager(
|
|||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyByMangaId = mangaHandler.awaitList(true) { historyQueries.getHistoryByMangaId(manga.id) }
|
||||
val historyByMangaId = getMangaHistory.await(manga.id)
|
||||
if (historyByMangaId.isNotEmpty()) {
|
||||
val history = historyByMangaId.map { history ->
|
||||
val chapter = mangaHandler.awaitOne { chaptersQueries.getChapterById(history.chapter_id) }
|
||||
BackupHistory(chapter.url, history.last_read?.time ?: 0L, history.time_read)
|
||||
val chapter = mangaHandler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
mangaObject.history = history
|
||||
|
@ -343,11 +347,11 @@ class BackupManager(
|
|||
|
||||
// Check if user wants history information in backup
|
||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||
val historyByAnimeId = animeHandler.awaitList(true) { animehistoryQueries.getHistoryByAnimeId(anime.id) }
|
||||
val historyByAnimeId = getAnimeHistory.await(anime.id)
|
||||
if (historyByAnimeId.isNotEmpty()) {
|
||||
val history = historyByAnimeId.map { history ->
|
||||
val episode = animeHandler.awaitOne { episodesQueries.getEpisodeById(history.episode_id) }
|
||||
BackupAnimeHistory(episode.url, history.last_seen?.time ?: 0L)
|
||||
val episode = animeHandler.awaitOne { episodesQueries.getEpisodeById(history.episodeId) }
|
||||
BackupAnimeHistory(episode.url, history.seenAt?.time ?: 0L)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
animeObject.history = history
|
||||
|
|
|
@ -6,15 +6,18 @@ import eu.kanade.domain.track.anime.model.toDbTrack
|
|||
import eu.kanade.domain.track.anime.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.anime.AnimeTrack
|
||||
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||
import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack as DomainAnimeTrack
|
||||
|
||||
interface AnimeTrackService {
|
||||
|
@ -76,6 +79,21 @@ interface AnimeTrackService {
|
|||
)
|
||||
setRemoteLastEpisodeSeen(updatedTrack.toDbTrack(), latestLocalSeenEpisodeNumber.toInt())
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetAnimeHistory>().await(animeId)
|
||||
.sortedBy { it.seenAt }
|
||||
.firstOrNull()
|
||||
?.seenAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this is EnhancedAnimeTrackService) {
|
||||
|
|
|
@ -6,15 +6,18 @@ import eu.kanade.domain.track.manga.model.toDbTrack
|
|||
import eu.kanade.domain.track.manga.model.toDomainTrack
|
||||
import eu.kanade.tachiyomi.data.database.models.manga.MangaTrack
|
||||
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.history.manga.interactor.GetMangaHistory
|
||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||
import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack as DomainTrack
|
||||
|
||||
interface MangaTrackService {
|
||||
|
@ -76,6 +79,21 @@ interface MangaTrackService {
|
|||
)
|
||||
setRemoteLastChapterRead(updatedTrack.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
}
|
||||
|
||||
if (track.startDate <= 0) {
|
||||
val firstReadChapterDate = Injekt.get<GetMangaHistory>().await(mangaId)
|
||||
.sortedBy { it.readAt }
|
||||
.firstOrNull()
|
||||
?.readAt
|
||||
|
||||
firstReadChapterDate?.let {
|
||||
val startDate = firstReadChapterDate.time.convertEpochMillisZone(
|
||||
ZoneOffset.systemDefault(),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
setRemoteStartDate(track.toDbTrack(), startDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this is EnhancedMangaTrackService) {
|
||||
|
|
|
@ -127,7 +127,7 @@ internal class AnimeExtensionGithubApi {
|
|||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.map(extensionAnimeSourceMapper).orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ internal class MangaExtensionGithubApi {
|
|||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
|
||||
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,23 +21,22 @@ class MigrateAnimeSearchScreen(private val animeId: Long) : Screen() {
|
|||
val state by screenModel.state.collectAsState()
|
||||
|
||||
MigrateAnimeSearchScreen(
|
||||
navigateUp = navigator::pop,
|
||||
state = state,
|
||||
getAnime = { screenModel.getAnime(it) },
|
||||
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)
|
||||
}
|
||||
navigator.push(AnimeSourceSearchScreen(state.anime!!, it.id, state.searchQuery))
|
||||
},
|
||||
onClickItem = { screenModel.setDialog(MigrateAnimeSearchDialog.Migrate(it)) },
|
||||
onClickItem = { screenModel.setDialog((MigrateAnimeSearchScreenModel.Dialog.Migrate(it))) },
|
||||
onLongClickItem = { navigator.push(AnimeScreen(it.id, true)) },
|
||||
)
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is MigrateAnimeSearchDialog.Migrate -> {
|
||||
is MigrateAnimeSearchScreenModel.Dialog.Migrate -> {
|
||||
MigrateAnimeDialog(
|
||||
oldAnime = state.anime!!,
|
||||
newAnime = dialog.anime,
|
||||
|
|
|
@ -2,27 +2,22 @@ package eu.kanade.tachiyomi.ui.browse.anime.migration.search
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
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.AnimeSearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.globalsearch.AnimeSourceFilter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MigrateAnimeSearchScreenModel(
|
||||
val animeId: Long,
|
||||
initialExtensionFilter: String = "",
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
private val getAnime: GetAnime = Injekt.get(),
|
||||
) : AnimeSearchScreenModel<MigrateAnimeSearchState>(MigrateAnimeSearchState()) {
|
||||
getAnime: GetAnime = Injekt.get(),
|
||||
) : AnimeSearchScreenModel<MigrateAnimeSearchScreenModel.State>(State()) {
|
||||
|
||||
init {
|
||||
extensionFilter = initialExtensionFilter
|
||||
|
@ -37,19 +32,16 @@ class MigrateAnimeSearchScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
val incognitoMode = preferences.incognitoMode()
|
||||
val lastUsedSourceId = sourcePreferences.lastUsedAnimeSource()
|
||||
|
||||
override fun getEnabledSources(): List<AnimeCatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledAnimeSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in enabledLanguages }
|
||||
.filterNot { "${it.id}" in disabledSources }
|
||||
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
|
||||
.sortedByDescending { it.id == state.value.anime!!.source }
|
||||
return super.getEnabledSources()
|
||||
.filter { mutableState.value.sourceFilter != AnimeSourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.id != state.value.anime!!.source },
|
||||
{ "${it.id}" !in pinnedSources },
|
||||
{ "${it.name.lowercase()} (${it.lang})" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateSearchQuery(query: String?) {
|
||||
|
@ -68,26 +60,38 @@ class MigrateAnimeSearchScreenModel(
|
|||
return mutableState.value.items
|
||||
}
|
||||
|
||||
fun setDialog(dialog: MigrateAnimeSearchDialog?) {
|
||||
override fun setSourceFilter(filter: AnimeSourceFilter) {
|
||||
mutableState.update { it.copy(sourceFilter = filter) }
|
||||
}
|
||||
|
||||
override fun toggleFilterResults() {
|
||||
mutableState.update {
|
||||
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update {
|
||||
it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MigrateAnimeSearchDialog {
|
||||
data class Migrate(val anime: Anime) : MigrateAnimeSearchDialog()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class MigrateAnimeSearchState(
|
||||
val anime: Anime? = null,
|
||||
val searchQuery: String? = null,
|
||||
val items: Map<AnimeCatalogueSource, AnimeSearchItemResult> = emptyMap(),
|
||||
val dialog: MigrateAnimeSearchDialog? = null,
|
||||
) {
|
||||
|
||||
val progress: Int = items.count { it.value !is AnimeSearchItemResult.Loading }
|
||||
|
||||
val total: Int = items.size
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val anime: Anime? = null,
|
||||
val dialog: Dialog? = null,
|
||||
|
||||
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
|
||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class Migrate(val anime: Anime) : Dialog()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class AnimeSourcesScreenModel(
|
|||
private val getEnabledAnimeSources: GetEnabledAnimeSources = Injekt.get(),
|
||||
private val toggleSource: ToggleAnimeSource = Injekt.get(),
|
||||
private val toggleSourcePin: ToggleAnimeSourcePin = Injekt.get(),
|
||||
) : StateScreenModel<AnimeSourcesState>(AnimeSourcesState()) {
|
||||
) : StateScreenModel<AnimeSourcesScreenModel.State>(State()) {
|
||||
|
||||
private val _events = Channel<Event>(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
@ -83,12 +83,6 @@ class AnimeSourcesScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onOpenSource(source: AnimeSource) {
|
||||
if (!preferences.incognitoMode().get()) {
|
||||
sourcePreferences.lastUsedAnimeSource().set(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSource(source: AnimeSource) {
|
||||
toggleSource.await(source)
|
||||
}
|
||||
|
@ -110,13 +104,13 @@ class AnimeSourcesScreenModel(
|
|||
}
|
||||
|
||||
data class Dialog(val source: AnimeSource)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class AnimeSourcesState(
|
||||
val dialog: AnimeSourcesScreenModel.Dialog? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<AnimeSourceUiModel> = emptyList(),
|
||||
) {
|
||||
val isEmpty = items.isEmpty()
|
||||
@Immutable
|
||||
data class State(
|
||||
val dialog: Dialog? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<AnimeSourceUiModel> = emptyList(),
|
||||
) {
|
||||
val isEmpty = items.isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ fun Screen.animeSourcesTab(): TabContent {
|
|||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = { source, listing ->
|
||||
screenModel.onOpenSource(source)
|
||||
navigator.push(BrowseAnimeSourceScreen(source.id, listing.query))
|
||||
},
|
||||
onClickPin = screenModel::togglePin,
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.paging.map
|
|||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.anime.model.toDomainAnime
|
||||
import eu.kanade.domain.items.episode.interactor.SyncEpisodesWithTrackServiceTwoWay
|
||||
|
@ -67,6 +68,7 @@ class BrowseAnimeSourceScreenModel(
|
|||
listingQuery: String?,
|
||||
sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val coverCache: AnimeCoverCache = Injekt.get(),
|
||||
private val getRemoteAnime: GetRemoteAnime = Injekt.get(),
|
||||
|
@ -106,6 +108,10 @@ class BrowseAnimeSourceScreenModel(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!basePreferences.incognitoMode().get()) {
|
||||
sourcePreferences.lastUsedAnimeSource().set(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,16 +10,19 @@ import eu.kanade.domain.source.service.SourcePreferences
|
|||
import eu.kanade.presentation.util.ioCoroutineScope
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.Executors
|
||||
|
@ -27,6 +30,7 @@ import java.util.concurrent.Executors
|
|||
abstract class AnimeSearchScreenModel<T>(
|
||||
initialState: T,
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
private val extensionManager: AnimeExtensionManager = Injekt.get(),
|
||||
private val networkToLocalAnime: NetworkToLocalAnime = Injekt.get(),
|
||||
private val getAnime: GetAnime = Injekt.get(),
|
||||
|
@ -34,12 +38,13 @@ abstract class AnimeSearchScreenModel<T>(
|
|||
) : StateScreenModel<T>(initialState) {
|
||||
|
||||
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
protected var query: String? = null
|
||||
protected lateinit var extensionFilter: String
|
||||
protected var extensionFilter: String? = null
|
||||
|
||||
private val sources by lazy { getSelectedSources() }
|
||||
private val pinnedSources by lazy { sourcePreferences.pinnedAnimeSources().get() }
|
||||
protected val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
|
||||
|
||||
private val sortComparator = { map: Map<AnimeCatalogueSource, AnimeSearchItemResult> ->
|
||||
compareBy<AnimeCatalogueSource>(
|
||||
|
@ -53,29 +58,41 @@ abstract class AnimeSearchScreenModel<T>(
|
|||
fun getAnime(initialAnime: Anime): State<Anime> {
|
||||
return produceState(initialValue = initialAnime) {
|
||||
getAnime.subscribe(initialAnime.url, initialAnime.source)
|
||||
.filterNotNull()
|
||||
.collectLatest { anime ->
|
||||
if (anime == null) return@collectLatest
|
||||
value = anime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getEnabledSources(): List<AnimeCatalogueSource>
|
||||
open fun getEnabledSources(): List<AnimeCatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledAnimeSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ "${it.id}" !in pinnedSources },
|
||||
{ "${it.name.lowercase()} (${it.lang})" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSelectedSources(): List<AnimeCatalogueSource> {
|
||||
val filter = extensionFilter
|
||||
|
||||
val enabledSources = getEnabledSources()
|
||||
|
||||
if (filter.isEmpty()) {
|
||||
val filter = extensionFilter
|
||||
if (filter.isNullOrEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
return extensionManager.installedExtensionsFlow.value
|
||||
.filter { it.pkgName == filter }
|
||||
.flatMap { it.sources }
|
||||
.filter { it in enabledSources }
|
||||
.filterIsInstance<AnimeCatalogueSource>()
|
||||
.filter { it in enabledSources }
|
||||
}
|
||||
|
||||
abstract fun updateSearchQuery(query: String?)
|
||||
|
@ -88,6 +105,10 @@ abstract class AnimeSearchScreenModel<T>(
|
|||
updateItems(function(getItems()))
|
||||
}
|
||||
|
||||
abstract fun setSourceFilter(filter: AnimeSourceFilter)
|
||||
|
||||
abstract fun toggleFilterResults()
|
||||
|
||||
fun search(query: String) {
|
||||
if (this.query == query) return
|
||||
|
||||
|
@ -95,8 +116,7 @@ abstract class AnimeSearchScreenModel<T>(
|
|||
|
||||
val initialItems = getSelectedSources().associateWith { AnimeSearchItemResult.Loading }
|
||||
updateItems(initialItems)
|
||||
|
||||
ioCoroutineScope.launch {
|
||||
searchJob = ioCoroutineScope.launch {
|
||||
sources
|
||||
.map { source ->
|
||||
async {
|
||||
|
@ -145,4 +165,8 @@ sealed class AnimeSearchItemResult {
|
|||
val isEmpty: Boolean
|
||||
get() = result.isEmpty()
|
||||
}
|
||||
|
||||
fun isVisible(onlyShowHasResults: Boolean): Boolean {
|
||||
return !onlyShowHasResults || (this is Success && !this.isEmpty)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import tachiyomi.presentation.core.screens.LoadingScreen
|
|||
|
||||
class GlobalAnimeSearchScreen(
|
||||
val searchQuery: String = "",
|
||||
private val extensionFilter: String = "",
|
||||
private val extensionFilter: String? = null,
|
||||
) : Screen() {
|
||||
|
||||
@Composable
|
||||
|
@ -33,9 +33,8 @@ class GlobalAnimeSearchScreen(
|
|||
}
|
||||
val state by screenModel.state.collectAsState()
|
||||
var showSingleLoadingScreen by remember {
|
||||
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
|
||||
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
|
||||
}
|
||||
val filteredSources by screenModel.searchPagerFlow.collectAsState()
|
||||
|
||||
if (showSingleLoadingScreen) {
|
||||
LoadingScreen()
|
||||
|
@ -58,7 +57,6 @@ class GlobalAnimeSearchScreen(
|
|||
} else {
|
||||
GlobalAnimeSearchScreen(
|
||||
state = state,
|
||||
items = filteredSources,
|
||||
navigateUp = navigator::pop,
|
||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||
onSearch = screenModel::search,
|
||||
|
@ -66,9 +64,6 @@ class GlobalAnimeSearchScreen(
|
|||
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||
onToggleResults = screenModel::toggleFilterResults,
|
||||
onClickSource = {
|
||||
if (!screenModel.incognitoMode.get()) {
|
||||
screenModel.lastUsedSourceId.set(it.id)
|
||||
}
|
||||
navigator.push(BrowseAnimeSourceScreen(it.id, state.searchQuery))
|
||||
},
|
||||
onClickItem = { navigator.push(AnimeScreen(it.id, true)) },
|
||||
|
|
|
@ -1,58 +1,29 @@
|
|||
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
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GlobalAnimeSearchScreenModel(
|
||||
initialQuery: String = "",
|
||||
initialExtensionFilter: String = "",
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: AnimeSourceManager = Injekt.get(),
|
||||
initialExtensionFilter: String? = null,
|
||||
) : AnimeSearchScreenModel<GlobalAnimeSearchScreenModel.State>(
|
||||
State(
|
||||
searchQuery = initialQuery,
|
||||
),
|
||||
) {
|
||||
|
||||
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()) {
|
||||
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
|
||||
search(initialQuery)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<AnimeCatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledAnimeSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedAnimeSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
return super.getEnabledSources()
|
||||
.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})" }))
|
||||
}
|
||||
|
||||
override fun updateSearchQuery(query: String?) {
|
||||
|
@ -71,20 +42,16 @@ class GlobalAnimeSearchScreenModel(
|
|||
return mutableState.value.items
|
||||
}
|
||||
|
||||
fun setSourceFilter(filter: AnimeSourceFilter) {
|
||||
override fun setSourceFilter(filter: AnimeSourceFilter) {
|
||||
mutableState.update { it.copy(sourceFilter = filter) }
|
||||
}
|
||||
|
||||
fun toggleFilterResults() {
|
||||
override 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,
|
||||
|
@ -94,5 +61,6 @@ class GlobalAnimeSearchScreenModel(
|
|||
) {
|
||||
val progress: Int = items.count { it.value !is AnimeSearchItemResult.Loading }
|
||||
val total: Int = items.size
|
||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,23 +21,22 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
|
|||
val state by screenModel.state.collectAsState()
|
||||
|
||||
MigrateMangaSearchScreen(
|
||||
navigateUp = navigator::pop,
|
||||
state = state,
|
||||
getManga = { screenModel.getManga(it) },
|
||||
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)
|
||||
}
|
||||
navigator.push(MangaSourceSearchScreen(state.manga!!, it.id, state.searchQuery))
|
||||
},
|
||||
onClickItem = { screenModel.setDialog(MigrateMangaSearchDialog.Migrate(it)) },
|
||||
onClickItem = { screenModel.setDialog(MigrateSearchScreenModel.Dialog.Migrate(it)) },
|
||||
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||
)
|
||||
|
||||
when (val dialog = state.dialog) {
|
||||
is MigrateMangaSearchDialog.Migrate -> {
|
||||
is MigrateSearchScreenModel.Dialog.Migrate -> {
|
||||
MigrateMangaDialog(
|
||||
oldManga = state.manga!!,
|
||||
newManga = dialog.manga,
|
||||
|
|
|
@ -2,27 +2,22 @@ package eu.kanade.tachiyomi.ui.browse.manga.migration.search
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchItemResult
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.globalsearch.MangaSourceFilter
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MigrateSearchScreenModel(
|
||||
val mangaId: Long,
|
||||
initialExtensionFilter: String = "",
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
) : MangaSearchScreenModel<MigrateMangaSearchState>(MigrateMangaSearchState()) {
|
||||
getManga: GetManga = Injekt.get(),
|
||||
) : MangaSearchScreenModel<MigrateSearchScreenModel.State>(State()) {
|
||||
|
||||
init {
|
||||
extensionFilter = initialExtensionFilter
|
||||
|
@ -37,19 +32,16 @@ class MigrateSearchScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
val incognitoMode = preferences.incognitoMode()
|
||||
val lastUsedSourceId = sourcePreferences.lastUsedMangaSource()
|
||||
|
||||
override fun getEnabledSources(): List<CatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledMangaSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedMangaSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in enabledLanguages }
|
||||
.filterNot { "${it.id}" in disabledSources }
|
||||
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
|
||||
.sortedByDescending { it.id == state.value.manga!!.source }
|
||||
return super.getEnabledSources()
|
||||
.filter { mutableState.value.sourceFilter != MangaSourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.id != state.value.manga!!.source },
|
||||
{ "${it.id}" !in pinnedSources },
|
||||
{ "${it.name.lowercase()} (${it.lang})" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateSearchQuery(query: String?) {
|
||||
|
@ -68,26 +60,38 @@ class MigrateSearchScreenModel(
|
|||
return mutableState.value.items
|
||||
}
|
||||
|
||||
fun setDialog(dialog: MigrateMangaSearchDialog?) {
|
||||
override fun setSourceFilter(filter: MangaSourceFilter) {
|
||||
mutableState.update { it.copy(sourceFilter = filter) }
|
||||
}
|
||||
|
||||
override fun toggleFilterResults() {
|
||||
mutableState.update {
|
||||
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDialog(dialog: Dialog?) {
|
||||
mutableState.update {
|
||||
it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MigrateMangaSearchDialog {
|
||||
data class Migrate(val manga: Manga) : MigrateMangaSearchDialog()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class MigrateMangaSearchState(
|
||||
val manga: Manga? = null,
|
||||
val searchQuery: String? = null,
|
||||
val items: Map<CatalogueSource, MangaSearchItemResult> = emptyMap(),
|
||||
val dialog: MigrateMangaSearchDialog? = null,
|
||||
) {
|
||||
|
||||
val progress: Int = items.count { it.value !is MangaSearchItemResult.Loading }
|
||||
|
||||
val total: Int = items.size
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val manga: Manga? = null,
|
||||
val dialog: Dialog? = null,
|
||||
|
||||
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
|
||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||
}
|
||||
|
||||
sealed class Dialog {
|
||||
data class Migrate(val manga: Manga) : Dialog()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ class MangaSourcesScreenModel(
|
|||
// SY -->
|
||||
private val toggleExcludeFromMangaDataSaver: ToggleExcludeFromMangaDataSaver = Injekt.get(),
|
||||
// SY <--
|
||||
) : StateScreenModel<MangaSourcesState>(MangaSourcesState()) {
|
||||
) : StateScreenModel<MangaSourcesScreenModel.State>(State()) {
|
||||
|
||||
private val _events = Channel<Event>(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
@ -101,12 +101,6 @@ class MangaSourcesScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onOpenSource(source: Source) {
|
||||
if (!preferences.incognitoMode().get()) {
|
||||
sourcePreferences.lastUsedMangaSource().set(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSource(source: Source) {
|
||||
toggleSource.await(source)
|
||||
}
|
||||
|
@ -134,16 +128,16 @@ class MangaSourcesScreenModel(
|
|||
}
|
||||
|
||||
data class Dialog(val source: Source)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class MangaSourcesState(
|
||||
val dialog: MangaSourcesScreenModel.Dialog? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<MangaSourceUiModel> = emptyList(),
|
||||
// SY -->
|
||||
val dataSaverEnabled: Boolean = false,
|
||||
// SY <--
|
||||
) {
|
||||
val isEmpty = items.isEmpty()
|
||||
@Immutable
|
||||
data class State(
|
||||
val dialog: Dialog? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<MangaSourceUiModel> = emptyList(),
|
||||
// SY -->
|
||||
val dataSaverEnabled: Boolean = false,
|
||||
// SY <--
|
||||
) {
|
||||
val isEmpty = items.isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ fun Screen.mangaSourcesTab(): TabContent {
|
|||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = { source, listing ->
|
||||
screenModel.onOpenSource(source)
|
||||
navigator.push(BrowseMangaSourceScreen(source.id, listing.query))
|
||||
},
|
||||
onClickPin = screenModel::togglePin,
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.paging.map
|
|||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.entries.manga.model.toDomainManga
|
||||
import eu.kanade.domain.items.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
|
@ -67,6 +68,7 @@ class BrowseMangaSourceScreenModel(
|
|||
listingQuery: String?,
|
||||
sourceManager: MangaSourceManager = Injekt.get(),
|
||||
sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
basePreferences: BasePreferences = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val coverCache: MangaCoverCache = Injekt.get(),
|
||||
private val getRemoteManga: GetRemoteManga = Injekt.get(),
|
||||
|
@ -106,6 +108,10 @@ class BrowseMangaSourceScreenModel(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!basePreferences.incognitoMode().get()) {
|
||||
sourcePreferences.lastUsedMangaSource().set(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,7 @@ import tachiyomi.presentation.core.screens.LoadingScreen
|
|||
|
||||
class GlobalMangaSearchScreen(
|
||||
val searchQuery: String = "",
|
||||
private val extensionFilter: String = "",
|
||||
private val extensionFilter: String? = null,
|
||||
) : Screen() {
|
||||
|
||||
@Composable
|
||||
|
@ -33,9 +33,8 @@ class GlobalMangaSearchScreen(
|
|||
}
|
||||
val state by screenModel.state.collectAsState()
|
||||
var showSingleLoadingScreen by remember {
|
||||
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
|
||||
mutableStateOf(searchQuery.isNotEmpty() && !extensionFilter.isNullOrEmpty() && state.total == 1)
|
||||
}
|
||||
val filteredSources by screenModel.searchPagerFlow.collectAsState()
|
||||
|
||||
if (showSingleLoadingScreen) {
|
||||
LoadingScreen()
|
||||
|
@ -58,7 +57,6 @@ class GlobalMangaSearchScreen(
|
|||
} else {
|
||||
GlobalMangaSearchScreen(
|
||||
state = state,
|
||||
items = filteredSources,
|
||||
navigateUp = navigator::pop,
|
||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||
onSearch = screenModel::search,
|
||||
|
@ -66,9 +64,6 @@ class GlobalMangaSearchScreen(
|
|||
onChangeSearchFilter = screenModel::setSourceFilter,
|
||||
onToggleResults = screenModel::toggleFilterResults,
|
||||
onClickSource = {
|
||||
if (!screenModel.incognitoMode.get()) {
|
||||
screenModel.lastUsedSourceId.set(it.id)
|
||||
}
|
||||
navigator.push(BrowseMangaSourceScreen(it.id, state.searchQuery))
|
||||
},
|
||||
onClickItem = { navigator.push(MangaScreen(it.id, true)) },
|
||||
|
|
|
@ -1,58 +1,29 @@
|
|||
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
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GlobalMangaSearchScreenModel(
|
||||
initialQuery: String = "",
|
||||
initialExtensionFilter: String = "",
|
||||
preferences: BasePreferences = Injekt.get(),
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||
initialExtensionFilter: String? = null,
|
||||
) : MangaSearchScreenModel<GlobalMangaSearchScreenModel.State>(
|
||||
State(
|
||||
searchQuery = initialQuery,
|
||||
),
|
||||
) {
|
||||
|
||||
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()) {
|
||||
if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) {
|
||||
search(initialQuery)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<CatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledMangaSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedMangaSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
return super.getEnabledSources()
|
||||
.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})" }))
|
||||
}
|
||||
|
||||
override fun updateSearchQuery(query: String?) {
|
||||
|
@ -71,20 +42,16 @@ class GlobalMangaSearchScreenModel(
|
|||
return mutableState.value.items
|
||||
}
|
||||
|
||||
fun setSourceFilter(filter: MangaSourceFilter) {
|
||||
override fun setSourceFilter(filter: MangaSourceFilter) {
|
||||
mutableState.update { it.copy(sourceFilter = filter) }
|
||||
}
|
||||
|
||||
fun toggleFilterResults() {
|
||||
override 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,
|
||||
|
@ -94,5 +61,6 @@ class GlobalMangaSearchScreenModel(
|
|||
) {
|
||||
val progress: Int = items.count { it.value !is MangaSearchItemResult.Loading }
|
||||
val total: Int = items.size
|
||||
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,16 +10,19 @@ import eu.kanade.domain.source.service.SourcePreferences
|
|||
import eu.kanade.presentation.util.ioCoroutineScope
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.Executors
|
||||
|
@ -27,6 +30,7 @@ import java.util.concurrent.Executors
|
|||
abstract class MangaSearchScreenModel<T>(
|
||||
initialState: T,
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
private val sourceManager: MangaSourceManager = Injekt.get(),
|
||||
private val extensionManager: MangaExtensionManager = Injekt.get(),
|
||||
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
|
||||
private val getManga: GetManga = Injekt.get(),
|
||||
|
@ -34,12 +38,13 @@ abstract class MangaSearchScreenModel<T>(
|
|||
) : StateScreenModel<T>(initialState) {
|
||||
|
||||
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
protected var query: String? = null
|
||||
protected lateinit var extensionFilter: String
|
||||
protected var extensionFilter: String? = null
|
||||
|
||||
private val sources by lazy { getSelectedSources() }
|
||||
private val pinnedSources by lazy { sourcePreferences.pinnedMangaSources().get() }
|
||||
protected val pinnedSources = sourcePreferences.pinnedMangaSources().get()
|
||||
|
||||
private val sortComparator = { map: Map<CatalogueSource, MangaSearchItemResult> ->
|
||||
compareBy<CatalogueSource>(
|
||||
|
@ -53,29 +58,41 @@ abstract class MangaSearchScreenModel<T>(
|
|||
fun getManga(initialManga: Manga): State<Manga> {
|
||||
return produceState(initialValue = initialManga) {
|
||||
getManga.subscribe(initialManga.url, initialManga.source)
|
||||
.filterNotNull()
|
||||
.collectLatest { manga ->
|
||||
if (manga == null) return@collectLatest
|
||||
value = manga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getEnabledSources(): List<CatalogueSource>
|
||||
open fun getEnabledSources(): List<CatalogueSource> {
|
||||
val enabledLanguages = sourcePreferences.enabledLanguages().get()
|
||||
val disabledSources = sourcePreferences.disabledMangaSources().get()
|
||||
val pinnedSources = sourcePreferences.pinnedMangaSources().get()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in enabledLanguages && "${it.id}" !in disabledSources }
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ "${it.id}" !in pinnedSources },
|
||||
{ "${it.name.lowercase()} (${it.lang})" },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSelectedSources(): List<CatalogueSource> {
|
||||
val filter = extensionFilter
|
||||
|
||||
val enabledSources = getEnabledSources()
|
||||
|
||||
if (filter.isEmpty()) {
|
||||
val filter = extensionFilter
|
||||
if (filter.isNullOrEmpty()) {
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
return extensionManager.installedExtensionsFlow.value
|
||||
.filter { it.pkgName == filter }
|
||||
.flatMap { it.sources }
|
||||
.filter { it in enabledSources }
|
||||
.filterIsInstance<CatalogueSource>()
|
||||
.filter { it in enabledSources }
|
||||
}
|
||||
|
||||
abstract fun updateSearchQuery(query: String?)
|
||||
|
@ -88,15 +105,19 @@ abstract class MangaSearchScreenModel<T>(
|
|||
updateItems(function(getItems()))
|
||||
}
|
||||
|
||||
abstract fun setSourceFilter(filter: MangaSourceFilter)
|
||||
|
||||
abstract fun toggleFilterResults()
|
||||
|
||||
fun search(query: String) {
|
||||
if (this.query == query) return
|
||||
|
||||
this.query = query
|
||||
|
||||
searchJob?.cancel()
|
||||
val initialItems = getSelectedSources().associateWith { MangaSearchItemResult.Loading }
|
||||
updateItems(initialItems)
|
||||
|
||||
ioCoroutineScope.launch {
|
||||
searchJob = ioCoroutineScope.launch {
|
||||
sources
|
||||
.map { source ->
|
||||
async {
|
||||
|
@ -145,4 +166,8 @@ sealed class MangaSearchItemResult {
|
|||
val isEmpty: Boolean
|
||||
get() = result.isEmpty()
|
||||
}
|
||||
|
||||
fun isVisible(onlyShowHasResults: Boolean): Boolean {
|
||||
return !onlyShowHasResults || (this is Success && !this.isEmpty)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
|
|||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
@ -83,7 +84,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack as DbAnimeTrack
|
||||
|
@ -540,14 +540,14 @@ private data class TrackDateSelectorScreen(
|
|||
(if (start) track.startDate else track.finishDate)
|
||||
.takeIf { it != 0L }
|
||||
?: Instant.now().toEpochMilli()
|
||||
return convertEpochMillisZone(millis, ZoneOffset.systemDefault(), ZoneOffset.UTC)
|
||||
return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
// In UTC
|
||||
fun setDate(millis: Long) {
|
||||
// Convert to local time
|
||||
val localMillis =
|
||||
convertEpochMillisZone(millis, ZoneOffset.UTC, ZoneOffset.systemDefault())
|
||||
millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault())
|
||||
coroutineScope.launchNonCancellable {
|
||||
if (start) {
|
||||
service.animeService.setRemoteStartDate(track.toDbTrack(), localMillis)
|
||||
|
@ -561,19 +561,6 @@ private data class TrackDateSelectorScreen(
|
|||
navigator.push(TrackDateRemoverScreen(track, service.id, start))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun convertEpochMillisZone(
|
||||
localMillis: Long,
|
||||
from: ZoneId,
|
||||
to: ZoneId,
|
||||
): Long {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(localMillis), from)
|
||||
.atZone(to)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TrackDateRemoverScreen(
|
||||
|
|
|
@ -57,6 +57,7 @@ import eu.kanade.tachiyomi.data.track.MangaTrackService
|
|||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.MangaTrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.flow.catch
|
||||
|
@ -83,7 +84,6 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack as DbMangaTrack
|
||||
|
@ -539,14 +539,14 @@ private data class TrackDateSelectorScreen(
|
|||
(if (start) track.startDate else track.finishDate)
|
||||
.takeIf { it != 0L }
|
||||
?: Instant.now().toEpochMilli()
|
||||
return convertEpochMillisZone(millis, ZoneOffset.systemDefault(), ZoneOffset.UTC)
|
||||
return millis.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
// In UTC
|
||||
fun setDate(millis: Long) {
|
||||
// Convert to local time
|
||||
val localMillis =
|
||||
convertEpochMillisZone(millis, ZoneOffset.UTC, ZoneOffset.systemDefault())
|
||||
millis.convertEpochMillisZone(ZoneOffset.UTC, ZoneOffset.systemDefault())
|
||||
coroutineScope.launchNonCancellable {
|
||||
if (start) {
|
||||
service.mangaService.setRemoteStartDate(track.toDbTrack(), localMillis)
|
||||
|
@ -560,19 +560,6 @@ private data class TrackDateSelectorScreen(
|
|||
navigator.push(TrackDateRemoverScreen(track, service.id, start))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun convertEpochMillisZone(
|
||||
localMillis: Long,
|
||||
from: ZoneId,
|
||||
to: ZoneId,
|
||||
): Long {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(localMillis), from)
|
||||
.atZone(to)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TrackDateRemoverScreen(
|
||||
|
|
|
@ -455,7 +455,7 @@ class MainActivity : BaseActivity() {
|
|||
INTENT_SEARCH -> {
|
||||
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) ?: ""
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
navigator.popUntilRoot()
|
||||
navigator.push(GlobalMangaSearchScreen(query, filter))
|
||||
}
|
||||
|
@ -464,7 +464,7 @@ class MainActivity : BaseActivity() {
|
|||
INTENT_ANIMESEARCH -> {
|
||||
val query = intent.getStringExtra(INTENT_SEARCH_QUERY)
|
||||
if (!query.isNullOrEmpty()) {
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER) ?: ""
|
||||
val filter = intent.getStringExtra(INTENT_SEARCH_FILTER)
|
||||
navigator.popUntilRoot()
|
||||
navigator.push(GlobalAnimeSearchScreen(query, filter))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.util.lang
|
|||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import java.text.DateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
@ -17,6 +20,16 @@ fun Date.toTimestampString(): String {
|
|||
return DateFormat.getTimeInstance(DateFormat.SHORT).format(this)
|
||||
}
|
||||
|
||||
fun Long.convertEpochMillisZone(
|
||||
from: ZoneId,
|
||||
to: ZoneId,
|
||||
): Long {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(this), from)
|
||||
.atZone(to)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date as time key
|
||||
*
|
||||
|
|
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistory
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||
import tachiyomi.domain.history.anime.repository.AnimeHistoryRepository
|
||||
|
@ -24,6 +25,10 @@ class AnimeHistoryRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getHistoryByAnimeId(animeId: Long): List<AnimeHistory> {
|
||||
return handler.awaitList { animehistoryQueries.getHistoryByAnimeId(animeId, animeHistoryMapper) }
|
||||
}
|
||||
|
||||
override suspend fun resetAnimeHistory(historyId: Long) {
|
||||
try {
|
||||
handler.await { animehistoryQueries.resetAnimeHistoryById(historyId) }
|
||||
|
|
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
|
||||
import tachiyomi.domain.history.manga.model.MangaHistory
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository
|
||||
|
@ -28,6 +29,10 @@ class MangaHistoryRepositoryImpl(
|
|||
return handler.awaitOne { historyQueries.getReadDuration() }
|
||||
}
|
||||
|
||||
override suspend fun getHistoryByMangaId(mangaId: Long): List<MangaHistory> {
|
||||
return handler.awaitList { historyQueries.getHistoryByMangaId(mangaId, mangaHistoryMapper) }
|
||||
}
|
||||
|
||||
override suspend fun resetMangaHistory(historyId: Long) {
|
||||
try {
|
||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tachiyomi.domain.history.anime.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistory
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||
import tachiyomi.domain.history.anime.repository.AnimeHistoryRepository
|
||||
|
||||
|
@ -8,6 +9,10 @@ class GetAnimeHistory(
|
|||
private val repository: AnimeHistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(animeId: Long): List<AnimeHistory> {
|
||||
return repository.getHistoryByAnimeId(animeId)
|
||||
}
|
||||
|
||||
fun subscribe(query: String): Flow<List<AnimeHistoryWithRelations>> {
|
||||
return repository.getAnimeHistory(query)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tachiyomi.domain.history.anime.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistory
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryUpdate
|
||||
import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||
|
||||
|
@ -12,6 +13,8 @@ interface AnimeHistoryRepository {
|
|||
|
||||
suspend fun resetAnimeHistory(historyId: Long)
|
||||
|
||||
suspend fun getHistoryByAnimeId(animeId: Long): List<AnimeHistory>
|
||||
|
||||
suspend fun resetHistoryByAnimeId(animeId: Long)
|
||||
|
||||
suspend fun deleteAllAnimeHistory(): Boolean
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tachiyomi.domain.history.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.manga.model.MangaHistory
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||
import tachiyomi.domain.history.manga.repository.MangaHistoryRepository
|
||||
|
||||
|
@ -8,6 +9,10 @@ class GetMangaHistory(
|
|||
private val repository: MangaHistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): List<MangaHistory> {
|
||||
return repository.getHistoryByMangaId(mangaId)
|
||||
}
|
||||
|
||||
fun subscribe(query: String): Flow<List<MangaHistoryWithRelations>> {
|
||||
return repository.getMangaHistory(query)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package tachiyomi.domain.history.manga.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.history.manga.model.MangaHistory
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryUpdate
|
||||
import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||
|
||||
|
@ -12,6 +13,8 @@ interface MangaHistoryRepository {
|
|||
|
||||
suspend fun getTotalReadDuration(): Long
|
||||
|
||||
suspend fun getHistoryByMangaId(mangaId: Long): List<MangaHistory>
|
||||
|
||||
suspend fun resetMangaHistory(historyId: Long)
|
||||
|
||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||
|
|
|
@ -58,6 +58,7 @@ private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
|||
|
||||
@Composable
|
||||
fun AdaptiveSheet(
|
||||
modifier: Modifier = Modifier,
|
||||
isTabletUi: Boolean,
|
||||
tonalElevation: Dp,
|
||||
enableSwipeDismiss: Boolean,
|
||||
|
@ -105,7 +106,8 @@ fun AdaptiveSheet(
|
|||
onClick = {},
|
||||
)
|
||||
.systemBarsPadding()
|
||||
.padding(vertical = 16.dp),
|
||||
.padding(vertical = 16.dp)
|
||||
.then(modifier),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = tonalElevation,
|
||||
content = {
|
||||
|
@ -168,6 +170,7 @@ fun AdaptiveSheet(
|
|||
Modifier
|
||||
},
|
||||
)
|
||||
.then(modifier)
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
|
|
|
@ -350,7 +350,7 @@ fun <T> SelectItem(
|
|||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true),
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
|
@ -478,7 +478,7 @@ fun TextItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsFlowRow(
|
||||
fun SettingsChipRow(
|
||||
@StringRes labelRes: Int,
|
||||
content: @Composable FlowRowScope.() -> Unit,
|
||||
) {
|
||||
|
|
Loading…
Reference in a new issue