From c6165c3c51a3278792f5acf03f7b99dd7ca5c55d Mon Sep 17 00:00:00 2001 From: "Tran M. Cuong" Date: Sun, 4 Aug 2024 03:02:19 +0700 Subject: [PATCH] feat: change feed's order (#220) allow change feed's order manually by moving up/down partially implement #130 * add feed_order - add/migrate feed_order column - auto increament feed_order when adding new feed - showing feed in order using feed_order * refactor code to support feedOrder * swap feed order * Fix generating feed's order when no feed existed --- .../java/eu/kanade/domain/SYDomainModule.kt | 4 ++ .../kanade/presentation/browse/FeedScreen.kt | 61 ++++++++++++++++++- .../data/backup/models/BackupFeed.kt | 4 +- .../ui/browse/feed/FeedScreenModel.kt | 43 +++++++++++++ .../tachiyomi/ui/browse/feed/FeedTab.kt | 31 +++++++++- .../source/feed/SourceFeedScreenModel.kt | 1 + .../migrations/MoveLatestToFeedMigration.kt | 1 + .../data/source/FeedSavedSearchMapper.kt | 2 + .../source/FeedSavedSearchRepositoryImpl.kt | 23 +++++++ .../tachiyomi/data/feed_saved_search.sq | 27 +++++--- .../sqldelight/tachiyomi/migrations/33.sqm | 5 ++ .../domain/source/interactor/SwapFeedOrder.kt | 17 ++++++ .../domain/source/model/FeedSavedSearch.kt | 1 + .../repository/FeedSavedSearchRepository.kt | 6 ++ .../moko-resources/base/strings.xml | 3 + 15 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/33.sqm create mode 100644 domain/src/main/java/tachiyomi/domain/source/interactor/SwapFeedOrder.kt diff --git a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt index 33656b7e4..21a59726b 100644 --- a/app/src/main/java/eu/kanade/domain/SYDomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/SYDomainModule.kt @@ -68,6 +68,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.repository.FeedSavedSearchRepository import tachiyomi.domain.source.repository.SavedSearchRepository import tachiyomi.domain.track.interactor.IsTrackUnfollowed @@ -151,6 +152,9 @@ class SYDomainModule : InjektModule { addFactory { CountFeedSavedSearchBySourceId(get()) } addFactory { GetSavedSearchGlobalFeed(get()) } addFactory { GetSavedSearchBySourceIdFeed(get()) } + // KMK --> + addFactory { SwapFeedOrder(get()) } + // KMK <-- addSingletonFactory { CustomMangaRepositoryImpl(get()) } addFactory { GetCustomMangaInfo(get()) } diff --git a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt index d82a32e5f..feae3d57c 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/FeedScreen.kt @@ -1,7 +1,9 @@ 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 @@ -42,6 +44,7 @@ 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.i18n.sy.SYMR import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.material.PullRefresh @@ -67,7 +70,9 @@ fun FeedScreen( contentPadding: PaddingValues, onClickSavedSearch: (SavedSearch, CatalogueSource) -> Unit, onClickSource: (CatalogueSource) -> Unit, - onClickDelete: (FeedSavedSearch) -> Unit, + // KMK --> + onLongClickFeed: (FeedItemUI, FeedSavedSearch?, FeedSavedSearch?) -> Unit, + // KMK <-- onClickManga: (Manga) -> Unit, // KMK --> onLongClickManga: (Manga) -> Unit, @@ -102,16 +107,26 @@ fun FeedScreen( contentPadding = contentPadding + topSmallPaddingValues, modifier = Modifier.fillMaxSize(), ) { + // KMK --> + val feeds = state.items.orEmpty() + // KMK <-- items( - state.items.orEmpty(), + 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) + // KMK <-- GlobalSearchResultItem( modifier = Modifier.animateItem(), title = item.title, subtitle = item.subtitle, onLongClick = { - onClickDelete(item.feed) + // KMK --> + onLongClickFeed(item, prevFeed?.feed, nextFeed?.feed) + // KMK <-- }, onClick = { if (item.savedSearch != null && item.source != null) { @@ -289,3 +304,43 @@ fun FeedDeleteConfirmDialog( }, ) } + +// 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 <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupFeed.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupFeed.kt index f534dd83c..eba8c1f6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupFeed.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupFeed.kt @@ -11,11 +11,11 @@ data class BackupFeed( ) val backupFeedMapper = - { _: Long, source: Long, _: Long?, global: Boolean -> + { _: Long, source: Long, _: Long?, global: Boolean, _: Long -> BackupFeed( source = source, global = global, ) } -fun SelectAllFeedHasSavedSearch.backupFeedMapper() = backupFeedMapper(_id, source, saved_search, global) +fun SelectAllFeedHasSavedSearch.backupFeedMapper() = backupFeedMapper(_id, source, saved_search, global, feed_order) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt index ad0b2a173..d72df0b57 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedScreenModel.kt @@ -41,6 +41,7 @@ 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.model.FeedSavedSearch import tachiyomi.domain.source.model.SavedSearch import tachiyomi.domain.source.service.SourceManager @@ -65,6 +66,9 @@ open class FeedScreenModel( 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(), + // KMK <-- ) : StateScreenModel(FeedScreenState()) { private val _events = Channel(Int.MAX_VALUE) @@ -152,6 +156,22 @@ open class FeedScreenModel( } } + // KMK --> + fun openActionsDialog( + feed: FeedItemUI, + prevFeed: FeedSavedSearch? = null, + nextFeed: FeedSavedSearch? = null, + ) { + screenModelScope.launchIO { + mutableState.update { state -> + state.copy( + dialog = Dialog.FeedActions(feed, prevFeed, nextFeed), + ) + } + } + } + // KMK <-- + private suspend fun hasTooManyFeeds(): Boolean { return countFeedSavedSearchGlobal.await() > MaxFeedItems } @@ -182,6 +202,7 @@ open class FeedScreenModel( source = source.id, savedSearch = savedSearch?.id, global = true, + feedOrder = 0, ), ) } @@ -193,6 +214,20 @@ open class FeedScreenModel( } } + // KMK --> + fun swapFeedOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch) { + screenModelScope.launchNonCancellable { + swapFeedOrder.swapOrder(feed1, feed2) + } + } + + fun moveToBottom(feed: FeedSavedSearch) { + screenModelScope.launchNonCancellable { + swapFeedOrder.moveToBottom(feed) + } + } + // KMK <-- + private suspend fun getSourcesToGetFeed(feedSavedSearch: List): List> { val savedSearches = getSavedSearchGlobalFeed.await() .associateBy { it.id } @@ -313,6 +348,14 @@ open class FeedScreenModel( data class AddFeed(val options: ImmutableList) : Dialog() data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList) : Dialog() data class DeleteFeed(val feed: FeedSavedSearch) : Dialog() + + // KMK --> + data class FeedActions( + val feedItem: FeedItemUI, + val prevFeed: FeedSavedSearch?, + val nextFeed: FeedSavedSearch?, + ) : Dialog() + // KMK <-- } sealed class Event { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt index 66f89a111..43c53ff7f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedTab.kt @@ -12,6 +12,7 @@ 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 @@ -118,7 +119,9 @@ fun feedTab( ), ) }, - onClickDelete = screenModel::openDeleteDialog, + // KMK --> + onLongClickFeed = screenModel::openActionsDialog, + // KMK <-- onClickManga = { manga -> // KMK --> if (bulkFavoriteState.selectionMode) { @@ -177,6 +180,32 @@ fun feedTab( }, ) } + // 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() + } + ) + } + // KMK <-- } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt index 81e7ffe3f..4d493bb0c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedScreenModel.kt @@ -130,6 +130,7 @@ open class SourceFeedScreenModel( source = source.id, savedSearch = savedSearchId, global = false, + feedOrder = 0, ), ) } diff --git a/app/src/main/java/mihon/core/migration/migrations/MoveLatestToFeedMigration.kt b/app/src/main/java/mihon/core/migration/migrations/MoveLatestToFeedMigration.kt index 4eea4b709..61bc21d0f 100644 --- a/app/src/main/java/mihon/core/migration/migrations/MoveLatestToFeedMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/MoveLatestToFeedMigration.kt @@ -48,6 +48,7 @@ class MoveLatestToFeedMigration : Migration { source = it.toLong(), savedSearch = null, global = true, + feedOrder = 0, ) } if (!feedSavedSearch.isNullOrEmpty()) { diff --git a/data/src/main/java/tachiyomi/data/source/FeedSavedSearchMapper.kt b/data/src/main/java/tachiyomi/data/source/FeedSavedSearchMapper.kt index d656d7b7f..2686df8fd 100644 --- a/data/src/main/java/tachiyomi/data/source/FeedSavedSearchMapper.kt +++ b/data/src/main/java/tachiyomi/data/source/FeedSavedSearchMapper.kt @@ -8,12 +8,14 @@ object FeedSavedSearchMapper { source: Long, savedSearch: Long?, global: Boolean, + feedOrder: Long, ): FeedSavedSearch { return FeedSavedSearch( id = id, source = source, savedSearch = savedSearch, global = global, + feedOrder = feedOrder, ) } } diff --git a/data/src/main/java/tachiyomi/data/source/FeedSavedSearchRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/FeedSavedSearchRepositoryImpl.kt index 5c64549ad..4c498a486 100644 --- a/data/src/main/java/tachiyomi/data/source/FeedSavedSearchRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/FeedSavedSearchRepositoryImpl.kt @@ -70,4 +70,27 @@ 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 moveToBottom(feed: FeedSavedSearch) { + return handler.await(true) { + feed_saved_searchQueries.moveToBottom( + id = feed.id, + ) + } + } + // KMK <-- } diff --git a/data/src/main/sqldelight/tachiyomi/data/feed_saved_search.sq b/data/src/main/sqldelight/tachiyomi/data/feed_saved_search.sq index be4d77df8..85b8b159e 100644 --- a/data/src/main/sqldelight/tachiyomi/data/feed_saved_search.sq +++ b/data/src/main/sqldelight/tachiyomi/data/feed_saved_search.sq @@ -5,6 +5,7 @@ CREATE TABLE feed_saved_search ( source INTEGER NOT NULL, saved_search INTEGER, global INTEGER AS Boolean NOT NULL, + feed_order INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(saved_search) REFERENCES saved_search (_id) ON DELETE CASCADE ); @@ -12,16 +13,17 @@ CREATE TABLE feed_saved_search ( CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); selectAllGlobal: -SELECT * FROM feed_saved_search WHERE global = 1; +SELECT * FROM feed_saved_search WHERE global = 1 ORDER BY feed_order; countGlobal: SELECT count(*) FROM feed_saved_search WHERE global = 1; selectBySource: -SELECT * FROM feed_saved_search WHERE source = :sourceId AND global = 0; +SELECT * FROM feed_saved_search WHERE source = :sourceId AND global = 0 ORDER BY feed_order; insert: -INSERT INTO feed_saved_search (source, saved_search, global) VALUES (:sourceId, :savedSearch, :global); +INSERT INTO feed_saved_search (source, saved_search, global, feed_order) + VALUES (:sourceId, :savedSearch, :global, (SELECT COALESCE(MAX(feed_order), 0) + 1 FROM feed_saved_search)); deleteById: DELETE FROM feed_saved_search WHERE _id = :id; @@ -32,7 +34,7 @@ DELETE FROM feed_saved_search; selectGlobalFeedSavedSearch: SELECT saved_search.* FROM ( - SELECT saved_search FROM feed_saved_search WHERE global = 1 + SELECT saved_search FROM feed_saved_search WHERE global = 1 ORDER BY feed_order ) AS M JOIN saved_search ON saved_search._id = M.saved_search; @@ -40,7 +42,7 @@ ON saved_search._id = M.saved_search; selectSourceFeedSavedSearch: SELECT saved_search.* FROM ( - SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId + SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId ORDER BY feed_order ) AS M JOIN saved_search ON saved_search._id = M.saved_search; @@ -48,7 +50,7 @@ ON saved_search._id = M.saved_search; countSourceFeedSavedSearch: SELECT count(*) FROM ( - SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId + SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId ORDER BY feed_order ) AS M JOIN saved_search ON saved_search._id = M.saved_search; @@ -57,7 +59,16 @@ selectLastInsertedRowId: SELECT last_insert_rowid(); selectAllGlobalNonSavedSearch: -SELECT * FROM feed_saved_search WHERE global = 1 AND saved_search IS NULL; +SELECT * FROM feed_saved_search WHERE global = 1 AND saved_search IS NULL ORDER BY feed_order; selectAllFeedHasSavedSearch: -SELECT * FROM feed_saved_search WHERE saved_search IS NOT NULL; \ No newline at end of file +SELECT * FROM feed_saved_search WHERE saved_search IS NOT NULL ORDER BY feed_order; + +-- KMK --> +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; +-- KMK <-- \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/migrations/33.sqm b/data/src/main/sqldelight/tachiyomi/migrations/33.sqm new file mode 100644 index 000000000..1e7b36d95 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/33.sqm @@ -0,0 +1,5 @@ +-- Add feed_order column to feed_saved_search -- +ALTER TABLE feed_saved_search ADD COLUMN feed_order INTEGER NOT NULL DEFAULT 0; + +-- Using ID as initial feed_order to existed order +UPDATE feed_saved_search SET feed_order = _id; \ No newline at end of file diff --git a/domain/src/main/java/tachiyomi/domain/source/interactor/SwapFeedOrder.kt b/domain/src/main/java/tachiyomi/domain/source/interactor/SwapFeedOrder.kt new file mode 100644 index 000000000..42ca866dd --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/source/interactor/SwapFeedOrder.kt @@ -0,0 +1,17 @@ +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) + } +} diff --git a/domain/src/main/java/tachiyomi/domain/source/model/FeedSavedSearch.kt b/domain/src/main/java/tachiyomi/domain/source/model/FeedSavedSearch.kt index aae516d51..ac0b834dd 100644 --- a/domain/src/main/java/tachiyomi/domain/source/model/FeedSavedSearch.kt +++ b/domain/src/main/java/tachiyomi/domain/source/model/FeedSavedSearch.kt @@ -12,4 +12,5 @@ data class FeedSavedSearch( // If the feed is a global (FeedScreen) or source specific feed (SourceFeedScreen) val global: Boolean, + val feedOrder: Long, ) diff --git a/domain/src/main/java/tachiyomi/domain/source/repository/FeedSavedSearchRepository.kt b/domain/src/main/java/tachiyomi/domain/source/repository/FeedSavedSearchRepository.kt index e4b89bc26..915e2c8db 100644 --- a/domain/src/main/java/tachiyomi/domain/source/repository/FeedSavedSearchRepository.kt +++ b/domain/src/main/java/tachiyomi/domain/source/repository/FeedSavedSearchRepository.kt @@ -27,4 +27,10 @@ interface FeedSavedSearchRepository { suspend fun insert(feedSavedSearch: FeedSavedSearch): Long? suspend fun insertAll(feedSavedSearch: List) + + // KMK --> + suspend fun swapOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch) + + suspend fun moveToBottom(feed: FeedSavedSearch) + // KMK <-- } diff --git a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml index 67ebf5ed0..2be27c103 100644 --- a/i18n-kmk/src/commonMain/moko-resources/base/strings.xml +++ b/i18n-kmk/src/commonMain/moko-resources/base/strings.xml @@ -7,6 +7,9 @@ Toggle NSFW only Bulk selection mode FAQ and Guides + Move up + Move down + Move bottom