Last commit merged: ef7b285151
This commit is contained in:
LuftVerbot 2023-11-12 14:40:35 +01:00
parent 096276cd68
commit 4709cbedba
54 changed files with 887 additions and 800 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
) {