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:
parent
e9767cac78
commit
ef58951bac
19 changed files with 1046 additions and 390 deletions
|
|
@ -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>()) }
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <--
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <--
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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 <--
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <--
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue