feat: feed reorder screen (#356)

* feat: Add screen to reorder Feeds

* refactor: Update icons/title and add preview for FeedOrderListItem

* refactor & cleanup

* feat(source): Add functionality to reorder feeds in source feed screen.

* feat: Add sorting functionality to source feed order screen

* refactor: Update SourceFeedScreen AppBar buttons

* refactor: Reuse FeedDeleteConfirmDialog and FeedSortAlphabeticallyDialog

* refactor: Reuse FeedOrderListItem
This commit is contained in:
Cuong-Tran 2024-09-15 21:28:10 +07:00
parent e9767cac78
commit ef58951bac
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
19 changed files with 1046 additions and 390 deletions

View file

@ -69,7 +69,7 @@ import tachiyomi.domain.source.interactor.GetSavedSearchBySourceIdFeed
import tachiyomi.domain.source.interactor.GetSavedSearchGlobalFeed
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.InsertSavedSearch
import tachiyomi.domain.source.interactor.SwapFeedOrder
import tachiyomi.domain.source.interactor.ReorderFeed
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
import tachiyomi.domain.source.repository.SavedSearchRepository
import tachiyomi.domain.track.interactor.IsTrackUnfollowed
@ -157,7 +157,7 @@ class SYDomainModule : InjektModule {
addFactory { GetSavedSearchGlobalFeed(get()) }
addFactory { GetSavedSearchBySourceIdFeed(get()) }
// KMK -->
addFactory { SwapFeedOrder(get()) }
addFactory { ReorderFeed(get()) }
// KMK <--
addSingletonFactory<CustomMangaRepository> { CustomMangaRepositoryImpl(get<Application>()) }

View file

@ -0,0 +1,60 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.browse.components.FeedOrderListItem
import eu.kanade.tachiyomi.ui.browse.feed.FeedScreenState
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.plus
@Composable
fun FeedOrderScreen(
state: FeedScreenState,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickMoveUp: (FeedSavedSearch) -> Unit,
onClickMoveDown: (FeedSavedSearch) -> Unit,
) {
when {
state.isLoading -> LoadingScreen()
state.isEmpty -> EmptyScreen(
stringRes = MR.strings.empty_screen,
)
else -> {
val lazyListState = rememberLazyListState()
val feeds = state.items ?: emptyList()
LazyColumn(
state = lazyListState,
contentPadding = topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
itemsIndexed(
items = feeds,
key = { _, feed -> "feed-${feed.feed.id}" },
) { index, feed ->
FeedOrderListItem(
modifier = Modifier.animateItem(),
title = feed.title,
canMoveUp = index != 0,
canMoveDown = index != feeds.lastIndex,
onMoveUp = { onClickMoveUp(feed.feed) },
onMoveDown = { onClickMoveDown(feed.feed) },
onDelete = { onClickDelete(feed.feed) },
)
}
}
}
}
}

View file

@ -1,9 +1,7 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -12,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
@ -48,7 +46,6 @@ import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@ -76,7 +73,7 @@ fun FeedScreen(
onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit,
onClickSource: (CatalogueSource) -> Unit,
// KMK -->
onLongClickFeed: (FeedItemUI, FeedSavedSearch?, FeedSavedSearch?) -> Unit,
onLongClickFeed: (FeedItemUI, Boolean, Boolean) -> Unit,
// KMK <--
onClickManga: (Manga) -> Unit,
// KMK -->
@ -114,15 +111,10 @@ fun FeedScreen(
) {
// KMK -->
val feeds = state.items.orEmpty()
// KMK <--
items(
feeds,
key = { "feed-${it.feed.id}" },
) { item ->
// KMK -->
val index = feeds.indexOf(item)
val prevFeed = feeds.getOrNull(index - 1)
val nextFeed = feeds.getOrNull(index + 1)
itemsIndexed(
items = feeds,
key = { _, it -> "feed-${it.feed.id}" },
) { index, item ->
// KMK <--
GlobalSearchResultItem(
modifier = Modifier.animateItem(),
@ -130,7 +122,11 @@ fun FeedScreen(
subtitle = item.subtitle,
onLongClick = {
// KMK -->
onLongClickFeed(item, prevFeed?.feed, nextFeed?.feed)
onLongClickFeed(
item,
index != 0,
index != feeds.lastIndex,
)
// KMK <--
},
onClick = {
@ -337,7 +333,8 @@ fun RadioSelectorSearchable(
SourcesSearch(
searchQuery = queryString,
onChangeSearchQuery = onChangeSearchQuery ?: {},
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.padding(bottom = MaterialTheme.padding.small),
).takeIf { onChangeSearchQuery != null }
options.forEachIndexed { index, option ->
@ -356,65 +353,3 @@ fun RadioSelectorSearchable(
}
}
// KMK <--
@Composable
fun FeedDeleteConfirmDialog(
feed: FeedSavedSearch,
onDismiss: () -> Unit,
onClickDeleteConfirm: (FeedSavedSearch) -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(SYMR.strings.feed))
},
text = {
Text(text = stringResource(SYMR.strings.feed_delete))
},
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = { onClickDeleteConfirm(feed) }) {
Text(text = stringResource(MR.strings.action_delete))
}
},
)
}
// KMK -->
@Composable
fun FeedActionsDialog(
feedItem: FeedItemUI,
hasPrevFeed: Boolean,
hasNextFeed: Boolean,
onDismiss: () -> Unit,
onClickDelete: () -> Unit,
onMoveUp: () -> Unit,
onMoveDown: () -> Unit,
onMoveBottom: () -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(SYMR.strings.feed))
},
text = {
Text(text = if (feedItem.savedSearch != null) feedItem.savedSearch.name else feedItem.source?.name ?: "")
},
onDismissRequest = onDismiss,
confirmButton = {
FlowRow(horizontalArrangement = Arrangement.SpaceEvenly) {
TextButton(onClick = onMoveUp, enabled = hasPrevFeed) {
Text(text = stringResource(KMR.strings.action_move_up))
}
TextButton(onClick = onMoveDown, enabled = hasNextFeed) {
Text(text = stringResource(KMR.strings.action_move_down))
}
TextButton(onClick = onMoveBottom, enabled = hasNextFeed) {
Text(text = stringResource(KMR.strings.action_move_bottom))
}
TextButton(onClick = onClickDelete) {
Text(text = stringResource(MR.strings.action_delete))
}
}
},
)
}
// KMK <--

View file

@ -0,0 +1,95 @@
package eu.kanade.presentation.browse
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.browse.components.FeedOrderListItem
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.tachiyomi.ui.browse.source.feed.SourceFeedState
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.plus
@Composable
fun SourceFeedOrderScreen(
state: SourceFeedState,
onClickDelete: (FeedSavedSearch) -> Unit,
onClickMoveUp: (FeedSavedSearch) -> Unit,
onClickMoveDown: (FeedSavedSearch) -> Unit,
onClickSortAlphabetically: () -> Unit,
navigateUp: (() -> Unit)? = null,
) {
Scaffold(
topBar = { scrollBehavior ->
AppBar(
titleContent = {
AppBarTitle(stringResource(KMR.strings.action_sort_feed))
},
navigateUp = navigateUp,
actions = {
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = onClickSortAlphabetically,
),
)
},
isActionMode = false,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
when {
state.isLoading -> LoadingScreen()
state.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
.isEmpty() -> EmptyScreen(
stringRes = MR.strings.empty_screen,
)
else -> {
val lazyListState = rememberLazyListState()
val feeds = state.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
LazyColumn(
state = lazyListState,
contentPadding = paddingValues + topSmallPaddingValues +
PaddingValues(horizontal = MaterialTheme.padding.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
itemsIndexed(
items = feeds,
key = { _, feed -> "feed-${feed.feed.id}" },
) { index, feed ->
FeedOrderListItem(
modifier = Modifier.animateItem(),
title = feed.title,
canMoveUp = index != 0,
canMoveDown = index != feeds.lastIndex,
onMoveUp = { onClickMoveUp(feed.feed) },
onMoveDown = { onClickMoveDown(feed.feed) },
onDelete = { onClickDelete(feed.feed) },
)
}
}
}
}
}
}

View file

@ -2,24 +2,22 @@ package eu.kanade.presentation.browse
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.SourceSettingsButton
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.AppBarTitle
import eu.kanade.presentation.components.SearchToolbar
@ -32,21 +30,18 @@ import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.util.plus
import tachiyomi.source.local.LocalSource
sealed class SourceFeedUI {
abstract val id: Long
abstract val title: String
@Composable
@ReadOnlyComposable
get
abstract val title: Any
abstract val results: List<Manga>?
@ -54,10 +49,8 @@ sealed class SourceFeedUI {
data class Latest(override val results: List<Manga>?) : SourceFeedUI() {
override val id: Long = -1
override val title: String
@Composable
@ReadOnlyComposable
get() = stringResource(MR.strings.latest)
override val title: StringResource
get() = MR.strings.latest
override fun withResults(results: List<Manga>?): SourceFeedUI {
return copy(results = results)
@ -65,10 +58,8 @@ sealed class SourceFeedUI {
}
data class Browse(override val results: List<Manga>?) : SourceFeedUI() {
override val id: Long = -2
override val title: String
@Composable
@ReadOnlyComposable
get() = stringResource(MR.strings.browse)
override val title: StringResource
get() = MR.strings.browse
override fun withResults(results: List<Manga>?): SourceFeedUI {
return copy(results = results)
@ -83,8 +74,6 @@ sealed class SourceFeedUI {
get() = feed.id
override val title: String
@Composable
@ReadOnlyComposable
get() = savedSearch.name
override fun withResults(results: List<Manga>?): SourceFeedUI {
@ -103,7 +92,10 @@ fun SourceFeedScreen(
onClickBrowse: () -> Unit,
onClickLatest: () -> Unit,
onClickSavedSearch: (SavedSearch) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
// KMK -->
// onClickDelete: (FeedSavedSearch) -> Unit,
onLongClickFeed: (SourceFeedUI.SourceSavedSearch, Boolean, Boolean) -> Unit,
// KMK <--
onClickManga: (Manga) -> Unit,
onClickSearch: (String) -> Unit,
searchQuery: String?,
@ -111,8 +103,9 @@ fun SourceFeedScreen(
getMangaState: @Composable (Manga) -> State<Manga>,
// KMK -->
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
sourceId: Long,
onWebViewClick: (() -> Unit)?,
onSourceSettingClick: (() -> Unit?)?,
onSortFeedClick: (() -> Unit)?,
onLongClickManga: (Manga) -> Unit,
bulkFavoriteScreenModel: BulkFavoriteScreenModel,
// KMK <--
@ -150,7 +143,8 @@ fun SourceFeedScreen(
// KMK -->
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
sourceId = sourceId,
onSourceSettingClick = onSourceSettingClick,
onSortFeedClick = onSortFeedClick,
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
// KMK <--
)
@ -174,7 +168,10 @@ fun SourceFeedScreen(
onClickBrowse = onClickBrowse,
onClickLatest = onClickLatest,
onClickSavedSearch = onClickSavedSearch,
onClickDelete = onClickDelete,
// KMK -->
// onClickDelete = onClickDelete,
onLongClickFeed = onLongClickFeed,
// KMK <--
onClickManga = onClickManga,
// KMK -->
onLongClickManga = onLongClickManga,
@ -195,7 +192,10 @@ fun SourceFeedList(
onClickBrowse: () -> Unit,
onClickLatest: () -> Unit,
onClickSavedSearch: (SavedSearch) -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
// KMK -->
// onClickDelete: (FeedSavedSearch) -> Unit,
onLongClickFeed: (SourceFeedUI.SourceSavedSearch, Boolean, Boolean) -> Unit,
// KMK <--
onClickManga: (Manga) -> Unit,
// KMK -->
onLongClickManga: (Manga) -> Unit,
@ -205,17 +205,32 @@ fun SourceFeedList(
ScrollbarLazyColumn(
contentPadding = paddingValues + topSmallPaddingValues,
) {
items(
// KMK -->
itemsIndexed(
items,
key = { "source-feed-${it.id}" },
) { item ->
key = { _, it -> "source-feed-${it.id}" },
) { index, item ->
// KMK <--
GlobalSearchResultItem(
modifier = Modifier.animateItem(),
title = item.title,
title =
// KMK -->
if (item !is SourceFeedUI.SourceSavedSearch) {
stringResource(item.title as StringResource)
} else {
// KMK <--
item.title
},
subtitle = null,
onLongClick = if (item is SourceFeedUI.SourceSavedSearch) {
{
onClickDelete(item.feed)
// KMK -->
onLongClickFeed(
item,
index != items.size - items.filterIsInstance<SourceFeedUI.SourceSavedSearch>().size,
index != items.lastIndex,
)
// KMK <--
}
} else {
null
@ -283,8 +298,9 @@ fun SourceFeedToolbar(
onClickSearch: (String) -> Unit,
// KMK -->
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
sourceId: Long,
onWebViewClick: (() -> Unit)?,
onSourceSettingClick: (() -> Unit?)?,
onSortFeedClick: (() -> Unit)?,
toggleSelectionMode: () -> Unit,
// KMK <--
) {
@ -302,24 +318,39 @@ fun SourceFeedToolbar(
// KMK -->
actions = {
AppBarActions(
actions = persistentListOf(
bulkSelectionButton(toggleSelectionMode),
),
)
persistentListOf(
if (sourceId != LocalSource.ID) {
IconButton(
onClick = onWebViewClick,
) {
Icon(
imageVector = Icons.Outlined.Public,
contentDescription = stringResource(MR.strings.action_web_view),
)
actions = persistentListOf<AppBar.AppBarAction>().builder()
.apply {
add(bulkSelectionButton(toggleSelectionMode))
onWebViewClick?.let {
add(
AppBar.Action(
title = stringResource(MR.strings.action_web_view),
onClick = { onWebViewClick() },
icon = Icons.Outlined.Public,
),
)
}
onSortFeedClick?.let {
add(
AppBar.OverflowAction(
title = stringResource(KMR.strings.action_sort_feed),
onClick = { onSortFeedClick() },
),
)
}
onSourceSettingClick?.let {
add(
AppBar.OverflowAction(
title = stringResource(MR.strings.label_settings),
onClick = { onSourceSettingClick() },
),
)
}
}
} else {
null
},
SourceSettingsButton(sourceId),
.build(),
)
},
// KMK <--

View file

@ -0,0 +1,90 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Splitscreen
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun FeedOrderListItem(
title: String,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: () -> Unit,
onMoveDown: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
ElevatedCard(
modifier = modifier,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
start = MaterialTheme.padding.medium,
top = MaterialTheme.padding.medium,
end = MaterialTheme.padding.medium,
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Splitscreen,
contentDescription = null,
)
Text(
text = title,
modifier = Modifier
.padding(start = MaterialTheme.padding.medium),
)
}
Row {
IconButton(
onClick = onMoveUp,
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = onMoveDown,
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
}
}
}
}
@Preview
@Composable
private fun FeedOrderListItemPreview() {
FeedOrderListItem(
title = "Feed 1",
canMoveUp = true,
canMoveDown = true,
onMoveUp = {},
onMoveDown = {},
onDelete = {},
)
}

View file

@ -1,10 +1,38 @@
package eu.kanade.presentation.browse.components
import androidx.compose.foundation.clickable
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ArrowUpward
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.TabbedDialogPaddings
import eu.kanade.presentation.more.settings.LocalPreferenceMinHeight
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource
@ -60,3 +88,153 @@ fun SourceFeedDeleteDialog(
},
)
}
// KMK -->
private val PaddingSize = 16.dp
private val ButtonPadding = PaddingValues(top = 16.dp, bottom = 16.dp)
private val TitlePadding = PaddingValues(bottom = 16.dp, top = 8.dp)
@Composable
fun FeedActionsDialog(
feed: FeedSavedSearch,
title: String,
canMoveUp: Boolean,
canMoveDown: Boolean,
onDismissRequest: () -> Unit,
onClickDelete: (FeedSavedSearch) -> Unit,
onMoveUp: (FeedSavedSearch) -> Unit,
onMoveDown: (FeedSavedSearch) -> Unit,
modifier: Modifier = Modifier,
) {
val minHeight = LocalPreferenceMinHeight.current
AdaptiveSheet(
modifier = modifier,
onDismissRequest = onDismissRequest,
) {
Column(
modifier = Modifier
.padding(
vertical = TabbedDialogPaddings.Vertical,
horizontal = TabbedDialogPaddings.Horizontal,
)
.fillMaxWidth(),
) {
Text(
modifier = Modifier.padding(TitlePadding),
text = title,
style = MaterialTheme.typography.headlineMedium,
)
Spacer(Modifier.height(PaddingSize))
if (canMoveUp) {
TextPreferenceWidget(
title = stringResource(KMR.strings.action_move_up),
icon = Icons.Outlined.ArrowUpward,
onPreferenceClick = {
onDismissRequest()
onMoveUp(feed)
},
)
HorizontalDivider()
}
if (canMoveDown) {
TextPreferenceWidget(
title = stringResource(KMR.strings.action_move_down),
icon = Icons.Outlined.ArrowDownward,
onPreferenceClick = {
onDismissRequest()
onMoveDown(feed)
},
)
HorizontalDivider()
}
TextPreferenceWidget(
title = stringResource(MR.strings.action_delete),
icon = Icons.Outlined.Delete,
onPreferenceClick = {
onDismissRequest()
onClickDelete(feed)
},
)
Row(
modifier = Modifier
.sizeIn(minHeight = minHeight)
.clickable { onDismissRequest.invoke() }
.padding(ButtonPadding)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
OutlinedButton(onClick = onDismissRequest, modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier
.padding(vertical = 8.dp),
text = stringResource(MR.strings.action_cancel),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleLarge,
fontSize = 16.sp,
)
}
}
}
}
}
@Preview
@Composable
private fun FeedActionsDialogPreview() {
FeedActionsDialog(
feed = FeedSavedSearch(
id = 1,
source = 1,
savedSearch = null,
global = false,
feedOrder = 0,
),
title = "Feed 1",
canMoveUp = true,
canMoveDown = true,
onDismissRequest = { },
onClickDelete = { },
onMoveUp = { },
onMoveDown = { },
)
}
@Composable
fun FeedSortAlphabeticallyDialog(
onDismissRequest: () -> Unit,
onSort: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onSort()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(KMR.strings.action_sort_feed))
},
text = {
Text(text = stringResource(KMR.strings.sort_feed_confirmation))
},
)
}
// KMK <--

View file

@ -1,49 +0,0 @@
package eu.kanade.presentation.browse.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.model.installedExtension
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import tachiyomi.domain.source.model.Source
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.source.local.LocalSource
@Composable
fun SourceSettingsButton(
id: Long,
@Suppress("UNUSED_PARAMETER") modifier: Modifier = Modifier,
) {
// Create a fake source
val source = Source(id, "", "", supportsLatest = false, isStub = false)
SourceSettingsButton(source = source)
}
@Composable
fun SourceSettingsButton(
source: Source,
@Suppress("UNUSED_PARAMETER") modifier: Modifier = Modifier,
) {
// Avoid E-Hentai & ExHentai which is built-in & not actually installed extensions
if (source.id == LocalSource.ID || source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID) return
val navigator = LocalNavigator.currentOrThrow
IconButton(onClick = {
if (source.installedExtension !== null) {
navigator.push(ExtensionDetailsScreen(source.installedExtension!!.pkgName))
}
}) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(MR.strings.label_settings),
)
}
}

View file

@ -6,7 +6,6 @@ import androidx.compose.runtime.produceState
import androidx.compose.ui.util.fastAny
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.browse.FeedItemUI
@ -41,8 +40,9 @@ import tachiyomi.domain.source.interactor.GetFeedSavedSearchGlobal
import tachiyomi.domain.source.interactor.GetSavedSearchBySourceId
import tachiyomi.domain.source.interactor.GetSavedSearchGlobalFeed
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.SwapFeedOrder
import tachiyomi.domain.source.interactor.ReorderFeed
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearchUpdate
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
@ -59,15 +59,14 @@ open class FeedScreenModel(
val sourcePreferences: SourcePreferences = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val getFeedSavedSearchGlobal: GetFeedSavedSearchGlobal = Injekt.get(),
getFeedSavedSearchGlobal: GetFeedSavedSearchGlobal = Injekt.get(),
private val getSavedSearchGlobalFeed: GetSavedSearchGlobalFeed = Injekt.get(),
private val countFeedSavedSearchGlobal: CountFeedSavedSearchGlobal = Injekt.get(),
private val getSavedSearchBySourceId: GetSavedSearchBySourceId = Injekt.get(),
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
// KMK -->
private val swapFeedOrder: SwapFeedOrder = Injekt.get(),
private val reorderFeed: ReorderFeed = Injekt.get(),
// KMK <--
) : StateScreenModel<FeedScreenState>(FeedScreenState()) {
@ -159,13 +158,17 @@ open class FeedScreenModel(
// KMK -->
fun openActionsDialog(
feed: FeedItemUI,
prevFeed: FeedSavedSearch? = null,
nextFeed: FeedSavedSearch? = null,
canMoveUp: Boolean,
canMoveDown: Boolean,
) {
screenModelScope.launchIO {
mutableState.update { state ->
state.copy(
dialog = Dialog.FeedActions(feed, prevFeed, nextFeed),
dialog = Dialog.FeedActions(
feedItem = feed,
canMoveUp = canMoveUp,
canMoveDown = canMoveDown,
),
)
}
}
@ -176,7 +179,7 @@ open class FeedScreenModel(
return countFeedSavedSearchGlobal.await() > MaxFeedItems
}
fun getEnabledSources(): ImmutableList<CatalogueSource> {
private fun getEnabledSources(): ImmutableList<CatalogueSource> {
val languages = sourcePreferences.enabledLanguages().get()
val pinnedSources = sourcePreferences.pinnedSources().get()
val disabledSources = sourcePreferences.disabledSources().get()
@ -190,7 +193,7 @@ open class FeedScreenModel(
return list.sortedBy { it.id.toString() !in pinnedSources }.toImmutableList()
}
suspend fun getSourceSavedSearches(sourceId: Long): ImmutableList<SavedSearch> {
private suspend fun getSourceSavedSearches(sourceId: Long): ImmutableList<SavedSearch> {
return getSavedSearchBySourceId.await(sourceId).toImmutableList()
}
@ -215,15 +218,30 @@ open class FeedScreenModel(
}
// KMK -->
fun swapFeedOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch) {
screenModelScope.launchNonCancellable {
swapFeedOrder.swapOrder(feed1, feed2)
fun moveUp(feed: FeedSavedSearch) {
screenModelScope.launch {
reorderFeed.moveUp(feed)
}
}
fun moveToBottom(feed: FeedSavedSearch) {
screenModelScope.launchNonCancellable {
swapFeedOrder.moveToBottom(feed)
fun moveDown(feed: FeedSavedSearch) {
screenModelScope.launch {
reorderFeed.moveDown(feed)
}
}
fun sortAlphabetically() {
screenModelScope.launch {
reorderFeed.sortAlphabetically(
state.value.items
?.sortedBy { feed -> feed.title }
?.mapIndexed { index, feed ->
FeedSavedSearchUpdate(
id = feed.feed.id,
feedOrder = index.toLong(),
)
},
)
}
}
// KMK <--
@ -242,7 +260,7 @@ open class FeedScreenModel(
feed: FeedSavedSearch,
savedSearch: SavedSearch?,
source: CatalogueSource?,
results: List<DomainManga>?,
@Suppress("SameParameterValue") results: List<DomainManga>?,
): FeedItemUI {
return FeedItemUI(
feed,
@ -340,6 +358,16 @@ open class FeedScreenModel(
coroutineDispatcher.close()
}
// KMK -->
fun showDialog(dialog: Dialog) {
if (!state.value.isLoading) {
mutableState.update {
it.copy(dialog = dialog)
}
}
}
// KMK <--
fun dismissDialog() {
mutableState.update { it.copy(dialog = null) }
}
@ -352,9 +380,11 @@ open class FeedScreenModel(
// KMK -->
data class FeedActions(
val feedItem: FeedItemUI,
val prevFeed: FeedSavedSearch?,
val nextFeed: FeedSavedSearch?,
val canMoveUp: Boolean,
val canMoveDown: Boolean,
) : Dialog()
data object SortAlphabetically : Dialog()
// KMK <--
}

View file

@ -1,22 +1,30 @@
package eu.kanade.tachiyomi.ui.browse.feed
import androidx.activity.compose.BackHandler
import androidx.compose.animation.Crossfade
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material.icons.outlined.SwapVert
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.browse.FeedActionsDialog
import eu.kanade.presentation.browse.FeedAddDialog
import eu.kanade.presentation.browse.FeedAddSearchDialog
import eu.kanade.presentation.browse.FeedDeleteConfirmDialog
import eu.kanade.presentation.browse.FeedOrderScreen
import eu.kanade.presentation.browse.FeedScreen
import eu.kanade.presentation.browse.components.FeedActionsDialog
import eu.kanade.presentation.browse.components.FeedSortAlphabeticallyDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.TabContent
import eu.kanade.tachiyomi.ui.browse.AddDuplicateMangaDialog
@ -50,11 +58,15 @@ fun feedTab(
// KMK -->
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
val showingFeedOrderScreen = rememberSaveable { mutableStateOf(false) }
val haptic = LocalHapticFeedback.current
BackHandler(enabled = bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.backHandler()
BackHandler(enabled = bulkFavoriteState.selectionMode || showingFeedOrderScreen.value) {
when {
bulkFavoriteState.selectionMode -> bulkFavoriteScreenModel.backHandler()
showingFeedOrderScreen.value -> showingFeedOrderScreen.value = false
}
}
LaunchedEffect(bulkFavoriteState.selectionMode) {
@ -78,84 +90,125 @@ fun feedTab(
return TabContent(
titleRes = SYMR.strings.feed,
actions = persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_add),
icon = Icons.Outlined.Add,
onClick = {
screenModel.openAddDialog()
},
),
// KMK -->
bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode),
actions =
// KMK -->
if (showingFeedOrderScreen.value) {
persistentListOf(
AppBar.Action(
title = stringResource(KMR.strings.action_sort_feed),
icon = Icons.Outlined.SwapVert,
iconTint = MaterialTheme.colorScheme.primary,
onClick = { showingFeedOrderScreen.value = false },
),
AppBar.Action(
title = stringResource(MR.strings.action_sort),
icon = Icons.Outlined.SortByAlpha,
onClick = { screenModel.showDialog(FeedScreenModel.Dialog.SortAlphabetically) },
),
)
} else {
// KMK <--
),
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_add),
icon = Icons.Outlined.Add,
onClick = {
screenModel.openAddDialog()
},
),
// KMK -->
AppBar.Action(
title = stringResource(KMR.strings.action_sort_feed),
icon = Icons.Outlined.SwapVert,
onClick = { showingFeedOrderScreen.value = true },
),
bulkSelectionButton(bulkFavoriteScreenModel::toggleSelectionMode),
// KMK <--
)
},
content = { contentPadding, snackbarHostState ->
FeedScreen(
state = state,
contentPadding = contentPadding,
onClickSavedSearch = { savedSearch, source ->
screenModel.sourcePreferences.lastUsedSource().set(savedSearch.source)
navigator.push(
BrowseSourceScreen(
source.id,
listingQuery = null,
savedSearch = savedSearch.id,
),
// KMK -->
Crossfade(
targetState = showingFeedOrderScreen.value,
label = "feed_order_crossfade",
) { showingFeedOrderScreen ->
if (showingFeedOrderScreen) {
FeedOrderScreen(
state = state,
onClickDelete = screenModel::openDeleteDialog,
onClickMoveUp = screenModel::moveUp,
onClickMoveDown = screenModel::moveDown,
)
},
onClickSource = { source ->
screenModel.sourcePreferences.lastUsedSource().set(source.id)
navigator.push(
BrowseSourceScreen(
source.id,
} else {
// KMK <--
FeedScreen(
state = state,
contentPadding = contentPadding,
onClickSavedSearch = { savedSearch, source ->
screenModel.sourcePreferences.lastUsedSource().set(savedSearch.source)
navigator.push(
BrowseSourceScreen(
source.id,
listingQuery = null,
savedSearch = savedSearch.id,
),
)
},
onClickSource = { source ->
screenModel.sourcePreferences.lastUsedSource().set(source.id)
navigator.push(
BrowseSourceScreen(
source.id,
// KMK -->
listingQuery = if (!source.supportsLatest) {
GetRemoteManga.QUERY_POPULAR
} else {
// KMK <--
GetRemoteManga.QUERY_LATEST
},
),
)
},
// KMK -->
onLongClickFeed = screenModel::openActionsDialog,
// KMK <--
onClickManga = { manga ->
// KMK -->
listingQuery = if (!source.supportsLatest) {
GetRemoteManga.QUERY_POPULAR
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(manga)
} else {
// KMK <--
GetRemoteManga.QUERY_LATEST
},
),
)
},
// KMK -->
onLongClickFeed = screenModel::openActionsDialog,
// KMK <--
onClickManga = { manga ->
// KMK -->
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(manga)
} else {
navigator.push(MangaScreen(manga.id, true))
}
},
// KMK -->
onLongClickManga = { manga ->
if (!bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
},
selection = bulkFavoriteState.selection,
// KMK <--
navigator.push(MangaScreen(manga.id, true))
}
},
// KMK -->
onLongClickManga = { manga ->
if (!bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
},
selection = bulkFavoriteState.selection,
// KMK <--
onRefresh = screenModel::init,
getMangaState = { manga -> screenModel.getManga(initialManga = manga) },
)
onRefresh = screenModel::init,
getMangaState = { manga -> screenModel.getManga(initialManga = manga) },
)
}
}
state.dialog?.let { dialog ->
val onDismissRequest = screenModel::dismissDialog
when (dialog) {
is FeedScreenModel.Dialog.AddFeed -> {
FeedAddDialog(
sources = dialog.options,
onDismiss = screenModel::dismissDialog,
onDismiss = onDismissRequest,
onClickAdd = {
if (it != null) {
screenModel.openAddSearchDialog(it)
}
screenModel.dismissDialog()
onDismissRequest()
},
)
}
@ -163,46 +216,39 @@ fun feedTab(
FeedAddSearchDialog(
source = dialog.source,
savedSearches = dialog.options,
onDismiss = screenModel::dismissDialog,
onDismiss = onDismissRequest,
onClickAdd = { source, savedSearch ->
screenModel.createFeed(source, savedSearch)
screenModel.dismissDialog()
onDismissRequest()
},
)
}
is FeedScreenModel.Dialog.DeleteFeed -> {
FeedDeleteConfirmDialog(
feed = dialog.feed,
onDismiss = screenModel::dismissDialog,
onClickDeleteConfirm = {
screenModel.deleteFeed(it)
screenModel.dismissDialog()
SourceFeedDeleteDialog(
onDismissRequest = onDismissRequest,
deleteFeed = {
screenModel.deleteFeed(dialog.feed)
onDismissRequest()
},
)
}
// KMK -->
is FeedScreenModel.Dialog.FeedActions -> {
FeedActionsDialog(
feedItem = dialog.feedItem,
hasPrevFeed = dialog.prevFeed != null,
hasNextFeed = dialog.nextFeed != null,
onDismiss = screenModel::dismissDialog,
onClickDelete = {
screenModel.openDeleteDialog(dialog.feedItem.feed)
screenModel.dismissDialog()
},
onMoveUp = {
dialog.prevFeed?.let { screenModel.swapFeedOrder(dialog.feedItem.feed, it) }
screenModel.dismissDialog()
},
onMoveDown = {
dialog.nextFeed?.let { screenModel.swapFeedOrder(dialog.feedItem.feed, it) }
screenModel.dismissDialog()
},
onMoveBottom = {
dialog.nextFeed?.let { screenModel.moveToBottom(dialog.feedItem.feed) }
screenModel.dismissDialog()
},
feed = dialog.feedItem.feed,
title = dialog.feedItem.title,
canMoveUp = dialog.canMoveUp,
canMoveDown = dialog.canMoveDown,
onDismissRequest = onDismissRequest,
onClickDelete = { screenModel.openDeleteDialog(it) },
onMoveUp = { screenModel.moveUp(it) },
onMoveDown = { screenModel.moveDown(it) },
)
}
is FeedScreenModel.Dialog.SortAlphabetically -> {
FeedSortAlphabeticallyDialog(
onDismissRequest = onDismissRequest,
onSort = { screenModel.sortAlphabetically() },
)
}
// KMK <--

View file

@ -1,21 +1,30 @@
package eu.kanade.tachiyomi.ui.browse.source.feed
import androidx.activity.compose.BackHandler
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.model.installedExtension
import eu.kanade.presentation.browse.MissingSourceScreen
import eu.kanade.presentation.browse.SourceFeedOrderScreen
import eu.kanade.presentation.browse.SourceFeedScreen
import eu.kanade.presentation.browse.SourceFeedUI
import eu.kanade.presentation.browse.components.FeedActionsDialog
import eu.kanade.presentation.browse.components.FeedSortAlphabeticallyDialog
import eu.kanade.presentation.browse.components.SourceFeedAddDialog
import eu.kanade.presentation.browse.components.SourceFeedDeleteDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.isLocalOrStub
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.AddDuplicateMangaDialog
import eu.kanade.tachiyomi.ui.browse.AllowDuplicateDialog
@ -23,12 +32,14 @@ import eu.kanade.tachiyomi.ui.browse.BulkFavoriteScreenModel
import eu.kanade.tachiyomi.ui.browse.ChangeMangaCategoryDialog
import eu.kanade.tachiyomi.ui.browse.ChangeMangasCategoryDialog
import eu.kanade.tachiyomi.ui.browse.RemoveMangaDialog
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterDialog
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import eu.kanade.tachiyomi.util.system.toast
import exh.md.follows.MangaDexFollowsScreen
import exh.source.isEhBasedSource
import exh.ui.ifSourcesLoaded
import exh.util.nullIfBlank
import tachiyomi.domain.manga.model.Manga
@ -38,6 +49,7 @@ import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.domain.source.model.Source as ModelSource
class SourceFeedScreen(val sourceId: Long) : Screen() {
@ -66,56 +78,107 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
val bulkFavoriteScreenModel = rememberScreenModel { BulkFavoriteScreenModel() }
val bulkFavoriteState by bulkFavoriteScreenModel.state.collectAsState()
val showingFeedOrderScreen = rememberSaveable { mutableStateOf(false) }
val haptic = LocalHapticFeedback.current
// KMK <--
SourceFeedScreen(
name = screenModel.source.name,
isLoading = state.isLoading,
items = state.items,
hasFilters = state.filters.isNotEmpty(),
onFabClick = screenModel::openFilterSheet,
onClickBrowse = { onBrowseClick(navigator, screenModel.source) },
onClickLatest = { onLatestClick(navigator, screenModel.source) },
onClickSavedSearch = { onSavedSearchClick(navigator, screenModel.source, it) },
onClickDelete = screenModel::openDeleteFeed,
onClickManga = {
// KMK -->
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(it)
} else {
// KMK <--
onMangaClick(navigator, it)
}
},
onClickSearch = { onSearchClick(navigator, screenModel.source, it) },
searchQuery = state.searchQuery,
onSearchQueryChange = screenModel::search,
getMangaState = { screenModel.getManga(initialManga = it) },
// KMK -->
navigateUp = { navigator.pop() },
onWebViewClick = {
val source = screenModel.source as? HttpSource ?: return@SourceFeedScreen
navigator.push(
WebViewScreen(
url = source.baseUrl,
initialTitle = source.name,
sourceId = source.id,
),
BackHandler(enabled = bulkFavoriteState.selectionMode || showingFeedOrderScreen.value) {
when {
bulkFavoriteState.selectionMode -> bulkFavoriteScreenModel.backHandler()
showingFeedOrderScreen.value -> showingFeedOrderScreen.value = false
}
}
Crossfade(
targetState = showingFeedOrderScreen.value,
label = "feed_order_crossfade",
) { targetState ->
if (targetState) {
SourceFeedOrderScreen(
state = state,
onClickDelete = screenModel::openDeleteFeed,
onClickMoveUp = screenModel::moveUp,
onClickMoveDown = screenModel::moveDown,
onClickSortAlphabetically = {
screenModel.showDialog(SourceFeedScreenModel.Dialog.SortAlphabetically)
},
navigateUp = { showingFeedOrderScreen.value = false },
)
},
sourceId = screenModel.source.id,
onLongClickManga = { manga ->
if (!bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
},
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
// KMK <--
)
} else {
// KMK <--
SourceFeedScreen(
name = screenModel.source.name,
isLoading = state.isLoading,
items = state.items,
hasFilters = state.filters.isNotEmpty(),
onFabClick = screenModel::openFilterSheet,
onClickBrowse = { onBrowseClick(navigator, screenModel.source) },
onClickLatest = { onLatestClick(navigator, screenModel.source) },
onClickSavedSearch = { onSavedSearchClick(navigator, screenModel.source, it) },
// KMK -->
// onClickDelete = screenModel::openDeleteFeed,
onLongClickFeed = screenModel::openActionsDialog,
// KMK <--
onClickManga = {
// KMK -->
if (bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.toggleSelection(it)
} else {
// KMK <--
onMangaClick(navigator, it)
}
},
onClickSearch = { onSearchClick(navigator, screenModel.source, it) },
searchQuery = state.searchQuery,
onSearchQueryChange = screenModel::search,
getMangaState = { screenModel.getManga(initialManga = it) },
// KMK -->
navigateUp = { navigator.pop() },
onWebViewClick = {
val source = screenModel.source as HttpSource
navigator.push(
WebViewScreen(
url = source.baseUrl,
initialTitle = source.name,
sourceId = source.id,
),
)
}.takeIf { screenModel.source is HttpSource },
onSourceSettingClick = {
val dummy = ModelSource(
sourceId,
"",
"",
supportsLatest = false,
isStub = false,
)
dummy.installedExtension?.let {
navigator.push(ExtensionDetailsScreen(it.pkgName))
}
}.takeIf {
!screenModel.source.isLocalOrStub() &&
!screenModel.source.isEhBasedSource() &&
screenModel.state.value.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
.isNotEmpty()
},
onSortFeedClick = { showingFeedOrderScreen.value = true }
.takeIf {
screenModel.state.value.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
.isNotEmpty()
},
onLongClickManga = { manga ->
if (!bulkFavoriteState.selectionMode) {
bulkFavoriteScreenModel.addRemoveManga(manga, haptic)
} else {
navigator.push(MangaScreen(manga.id, true))
}
},
bulkFavoriteScreenModel = bulkFavoriteScreenModel,
// KMK <--
)
}
}
val onDismissRequest = screenModel::dismissDialog
when (val dialog = state.dialog) {
@ -138,7 +201,27 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
},
)
}
SourceFeedScreenModel.Dialog.Filter -> {
// KMK -->
is SourceFeedScreenModel.Dialog.FeedActions -> {
FeedActionsDialog(
feed = dialog.feedItem.feed,
title = dialog.feedItem.title,
canMoveUp = dialog.canMoveUp,
canMoveDown = dialog.canMoveDown,
onDismissRequest = screenModel::dismissDialog,
onClickDelete = { screenModel.openDeleteFeed(it) },
onMoveUp = { screenModel.moveUp(it) },
onMoveDown = { screenModel.moveDown(it) },
)
}
is SourceFeedScreenModel.Dialog.SortAlphabetically -> {
FeedSortAlphabeticallyDialog(
onDismissRequest = screenModel::dismissDialog,
onSort = { screenModel.sortAlphabetically() },
)
}
// KMK <--
is SourceFeedScreenModel.Dialog.Filter -> {
SourceFilterDialog(
onDismissRequest = onDismissRequest,
filters = state.filters,
@ -228,19 +311,13 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
else -> {}
}
// KMK <--
BackHandler(bulkFavoriteState.selectionMode) {
// KMK -->
bulkFavoriteScreenModel.backHandler()
// KMK <--
}
}
private fun onMangaClick(navigator: Navigator, manga: Manga) {
navigator.push(MangaScreen(manga.id, true))
}
fun onBrowseClick(navigator: Navigator, sourceId: Long, search: String? = null, savedSearch: Long? = null, filters: String? = null) {
private fun onBrowseClick(navigator: Navigator, sourceId: Long, search: String? = null, savedSearch: Long? = null, filters: String? = null) {
// KMK -->
// navigator.replace(BrowseSourceScreen(sourceId, search, savedSearch = savedSearch, filtersJson = filters))
navigator.push(BrowseSourceScreen(sourceId, search, savedSearch = savedSearch, filtersJson = filters))
@ -254,7 +331,7 @@ class SourceFeedScreen(val sourceId: Long) : Screen() {
// KMK <--
}
fun onBrowseClick(navigator: Navigator, source: Source) {
private fun onBrowseClick(navigator: Navigator, source: Source) {
// KMK -->
// navigator.replace(BrowseSourceScreen(source.id, GetRemoteManga.QUERY_POPULAR))
navigator.push(BrowseSourceScreen(source.id, GetRemoteManga.QUERY_POPULAR))

View file

@ -9,7 +9,6 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asState
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.interactor.GetExhSavedSearch
import eu.kanade.domain.ui.UiPreferences
@ -47,8 +46,10 @@ import tachiyomi.domain.source.interactor.DeleteFeedSavedSearchById
import tachiyomi.domain.source.interactor.GetFeedSavedSearchBySourceId
import tachiyomi.domain.source.interactor.GetSavedSearchBySourceIdFeed
import tachiyomi.domain.source.interactor.InsertFeedSavedSearch
import tachiyomi.domain.source.interactor.ReorderFeed
import tachiyomi.domain.source.model.EXHSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearchUpdate
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.kmk.KMR
@ -65,13 +66,15 @@ open class SourceFeedScreenModel(
private val sourceManager: SourceManager = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(),
private val getFeedSavedSearchBySourceId: GetFeedSavedSearchBySourceId = Injekt.get(),
private val getSavedSearchBySourceIdFeed: GetSavedSearchBySourceIdFeed = Injekt.get(),
private val countFeedSavedSearchBySourceId: CountFeedSavedSearchBySourceId = Injekt.get(),
private val insertFeedSavedSearch: InsertFeedSavedSearch = Injekt.get(),
private val deleteFeedSavedSearchById: DeleteFeedSavedSearchById = Injekt.get(),
private val getExhSavedSearch: GetExhSavedSearch = Injekt.get(),
// KMK -->
private val reorderFeed: ReorderFeed = Injekt.get(),
// KMK <--
) : StateScreenModel<SourceFeedState>(SourceFeedState()) {
var source = sourceManager.getOrStub(sourceId)
@ -142,6 +145,36 @@ open class SourceFeedScreenModel(
}
}
// KMK -->
fun moveUp(feed: FeedSavedSearch) {
screenModelScope.launch {
reorderFeed.moveUp(feed, false)
}
}
fun moveDown(feed: FeedSavedSearch) {
screenModelScope.launch {
reorderFeed.moveDown(feed, false)
}
}
fun sortAlphabetically() {
screenModelScope.launchNonCancellable {
reorderFeed.sortAlphabetically(
state.value.items
.filterIsInstance<SourceFeedUI.SourceSavedSearch>()
.sortedBy { feed -> feed.title }
.mapIndexed { index, feed ->
FeedSavedSearchUpdate(
id = feed.feed.id,
feedOrder = index.toLong(),
)
},
)
}
}
// KMK <--
private suspend fun getSourcesToGetFeed(feedSavedSearch: List<FeedSavedSearch>): ImmutableList<SourceFeedUI> {
// KMK -->
val source = source
@ -324,7 +357,35 @@ open class SourceFeedScreenModel(
mutableState.update { it.copy(dialog = Dialog.DeleteFeed(feed)) }
}
fun openAddFeed(feedId: Long, name: String) {
// KMK -->
fun openActionsDialog(
feed: SourceFeedUI.SourceSavedSearch,
canMoveUp: Boolean,
canMoveDown: Boolean,
) {
screenModelScope.launchIO {
mutableState.update { state ->
state.copy(
dialog = Dialog.FeedActions(
feedItem = feed,
canMoveUp = canMoveUp,
canMoveDown = canMoveDown,
),
)
}
}
}
fun showDialog(dialog: Dialog) {
if (!state.value.isLoading) {
mutableState.update {
it.copy(dialog = dialog)
}
}
}
// KMK <--
private fun openAddFeed(feedId: Long, name: String) {
mutableState.update { it.copy(dialog = Dialog.AddFeed(feedId, name)) }
}
@ -336,6 +397,16 @@ open class SourceFeedScreenModel(
data object Filter : Dialog()
data class DeleteFeed(val feed: FeedSavedSearch) : Dialog()
data class AddFeed(val feedId: Long, val name: String) : Dialog()
// KMK -->
data class FeedActions(
val feedItem: SourceFeedUI.SourceSavedSearch,
val canMoveUp: Boolean,
val canMoveDown: Boolean,
) : Dialog()
data object SortAlphabetically : Dialog()
// KMK <--
}
override fun onDispose() {

View file

@ -1,8 +1,10 @@
package tachiyomi.data.source
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearchUpdate
import tachiyomi.domain.source.model.SavedSearch
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
@ -86,25 +88,28 @@ class FeedSavedSearchRepositoryImpl(
}
// KMK -->
override suspend fun swapOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch) {
return handler.await(true) {
feed_saved_searchQueries.setOrder(
id = feed2.id,
order = feed1.feedOrder,
)
feed_saved_searchQueries.setOrder(
id = feed1.id,
order = feed2.feedOrder,
)
override suspend fun updatePartial(update: FeedSavedSearchUpdate) {
handler.await {
updatePartialBlocking(update)
}
}
override suspend fun moveToBottom(feed: FeedSavedSearch) {
return handler.await(true) {
feed_saved_searchQueries.moveToBottom(
id = feed.id,
)
override suspend fun updatePartial(updates: List<FeedSavedSearchUpdate>) {
handler.await(inTransaction = true) {
for (update in updates) {
updatePartialBlocking(update)
}
}
}
private fun Database.updatePartialBlocking(update: FeedSavedSearchUpdate) {
feed_saved_searchQueries.update(
source = update.source,
saved_search = update.savedSearch,
global = update.global,
feed_order = update.feedOrder,
id = update.id,
)
}
// KMK <--
}

View file

@ -75,10 +75,11 @@ SELECT coalesce(f.source, s.source) source, f.global, f.saved_search, s.name, s.
FROM feed_saved_search AS f LEFT JOIN saved_search AS s
ON f.saved_search = s._id ORDER BY f.feed_order;
setOrder:
UPDATE feed_saved_search SET feed_order = :order WHERE _id = :id;
moveToBottom:
UPDATE feed_saved_search SET feed_order = (SELECT MAX(feed_order) + 1 FROM feed_saved_search)
WHERE _id = :id;
update:
UPDATE feed_saved_search
SET source = coalesce(:source, source),
saved_search = coalesce(:saved_search, saved_search),
global = coalesce(:global, global),
feed_order = coalesce(:feed_order, feed_order)
WHERE _id = :id;
-- KMK <--

View file

@ -0,0 +1,85 @@
package tachiyomi.domain.source.interactor
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearchUpdate
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
import java.util.Collections
class ReorderFeed(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
private val mutex = Mutex()
suspend fun moveUp(feed: FeedSavedSearch, global: Boolean = true): Result = awaitGlobal(feed, MoveTo.UP, global)
suspend fun moveDown(feed: FeedSavedSearch, global: Boolean = true): Result = awaitGlobal(feed, MoveTo.DOWN, global)
private suspend fun awaitGlobal(feed: FeedSavedSearch, moveTo: MoveTo, global: Boolean = true) = withNonCancellableContext {
mutex.withLock {
val feeds = if (global) {
feedSavedSearchRepository.getGlobal()
.toMutableList()
} else {
feedSavedSearchRepository.getBySourceId(feed.source)
.toMutableList()
}
val currentIndex = feeds.indexOfFirst { it.id == feed.id }
if (currentIndex == -1) {
return@withNonCancellableContext Result.Unchanged
}
val newPosition = when (moveTo) {
MoveTo.UP -> currentIndex - 1
MoveTo.DOWN -> currentIndex + 1
}.toInt()
try {
Collections.swap(feeds, currentIndex, newPosition)
val updates = feeds.mapIndexed { index, feed ->
FeedSavedSearchUpdate(
id = feed.id,
feedOrder = index.toLong(),
)
}
feedSavedSearchRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
}
suspend fun sortAlphabetically(updates: List<FeedSavedSearchUpdate>?) = withNonCancellableContext {
if (updates == null) return@withNonCancellableContext
mutex.withLock {
try {
feedSavedSearchRepository.updatePartial(updates)
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
}
sealed interface Result {
data object Success : Result
data object Unchanged : Result
data class InternalError(val error: Throwable) : Result
}
private enum class MoveTo {
UP,
DOWN,
}
}

View file

@ -1,17 +0,0 @@
package tachiyomi.domain.source.interactor
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.repository.FeedSavedSearchRepository
class SwapFeedOrder(
private val feedSavedSearchRepository: FeedSavedSearchRepository,
) {
suspend fun swapOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch) {
return feedSavedSearchRepository.swapOrder(feed1, feed2)
}
suspend fun moveToBottom(feed: FeedSavedSearch) {
return feedSavedSearchRepository.moveToBottom(feed)
}
}

View file

@ -0,0 +1,16 @@
package tachiyomi.domain.source.model
data class FeedSavedSearchUpdate(
// Tag identifier, unique
val id: Long,
// Source for the saved search
val source: Long? = null,
// If null then get latest/popular, if set get the saved search
val savedSearch: Long? = null,
// If the feed is a global (FeedScreen) or source specific feed (SourceFeedScreen)
val global: Boolean? = null,
val feedOrder: Long? = null,
)

View file

@ -2,6 +2,7 @@ package tachiyomi.domain.source.repository
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.model.FeedSavedSearch
import tachiyomi.domain.source.model.FeedSavedSearchUpdate
import tachiyomi.domain.source.model.SavedSearch
interface FeedSavedSearchRepository {
@ -29,8 +30,8 @@ interface FeedSavedSearchRepository {
suspend fun insertAll(feedSavedSearch: List<FeedSavedSearch>)
// KMK -->
suspend fun swapOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch)
suspend fun updatePartial(update: FeedSavedSearchUpdate)
suspend fun moveToBottom(feed: FeedSavedSearch)
suspend fun updatePartial(updates: List<FeedSavedSearchUpdate>)
// KMK <--
}

View file

@ -9,7 +9,6 @@
<string name="action_faq_and_guides">FAQ and Guides</string>
<string name="action_move_up">Move up</string>
<string name="action_move_down">Move down</string>
<string name="action_move_bottom">Move bottom</string>
<string name="action_confirm_color">Confirm Color</string>
<string name="action_remove_merged">Remove merged entries?</string>
<string name="action_hide">Hide</string>
@ -118,6 +117,8 @@
<!-- Feed Tab -->
<string name="too_many_in_feed">Too many sources in your feed, cannot add more than limited (20)</string>
<string name="action_sort_feed">Sort feeds</string>
<string name="sort_feed_confirmation">Would you like to sort the feeds alphabetically?</string>
<!-- Migration -->
<string name="current_">Current: %1$s</string>