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
This commit is contained in:
parent
64a2c98ad7
commit
c6165c3c51
15 changed files with 215 additions and 14 deletions
|
|
@ -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<CustomMangaRepository> { CustomMangaRepositoryImpl(get<Application>()) }
|
||||
addFactory { GetCustomMangaInfo(get()) }
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>(FeedScreenState()) {
|
||||
|
||||
private val _events = Channel<Event>(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<FeedSavedSearch>): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
||||
val savedSearches = getSavedSearchGlobalFeed.await()
|
||||
.associateBy { it.id }
|
||||
|
|
@ -313,6 +348,14 @@ open class FeedScreenModel(
|
|||
data class AddFeed(val options: ImmutableList<CatalogueSource>) : Dialog()
|
||||
data class AddFeedSearch(val source: CatalogueSource, val options: ImmutableList<SavedSearch?>) : 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 {
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ open class SourceFeedScreenModel(
|
|||
source = source.id,
|
||||
savedSearch = savedSearchId,
|
||||
global = false,
|
||||
feedOrder = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class MoveLatestToFeedMigration : Migration {
|
|||
source = it.toLong(),
|
||||
savedSearch = null,
|
||||
global = true,
|
||||
feedOrder = 0,
|
||||
)
|
||||
}
|
||||
if (!feedSavedSearch.isNullOrEmpty()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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 <--
|
||||
5
data/src/main/sqldelight/tachiyomi/migrations/33.sqm
Normal file
5
data/src/main/sqldelight/tachiyomi/migrations/33.sqm
Normal file
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,4 +27,10 @@ interface FeedSavedSearchRepository {
|
|||
suspend fun insert(feedSavedSearch: FeedSavedSearch): Long?
|
||||
|
||||
suspend fun insertAll(feedSavedSearch: List<FeedSavedSearch>)
|
||||
|
||||
// KMK -->
|
||||
suspend fun swapOrder(feed1: FeedSavedSearch, feed2: FeedSavedSearch)
|
||||
|
||||
suspend fun moveToBottom(feed: FeedSavedSearch)
|
||||
// KMK <--
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
<string name="action_toggle_nsfw_only">Toggle NSFW only</string>
|
||||
<string name="action_bulk_select">Bulk selection mode</string>
|
||||
<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>
|
||||
|
||||
<!-- Preferences -->
|
||||
<!-- Appearance section -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue