feat: Drag-to-reorder sources and groups in Browse - #1812
Open
JoeJoeflyn wants to merge 1 commit into
Open
JoeJoeflyn wants to merge 1 commit into
JoeJoeflyn wants to merge 1 commit into
Conversation
Contributor
Reviewer's GuideImplements drag-to-reorder for source groups and individual sources in the Browse → Sources screen, persisting group order via preferences and source order via a new sort field backed by DB and repository changes, with a UI toggle and drag handles for reordering mode. Sequence diagram for drag-to-reorder sources and groupssequenceDiagram
actor User
participant SourcesTab
participant SourcesScreenModel
participant SourcesScreen
participant SourcePreferences
participant SourceRepository
participant Database
User->>SourcesTab: tap AppBar.Action(icon=DragIndicator)
SourcesTab->>SourcesScreenModel: toggleReorderMode()
SourcesScreenModel->>SourcesScreenModel: mutableState.update(reorderMode)
SourcesScreenModel-->>SourcesTab: State(reorderMode=true)
SourcesTab-->>SourcesScreen: SourcesScreen(state.reorderMode=true, onReorderGroup=reorderGroup, onReorderSource=reorderSourceWithinGroup)
rect rgba(200,200,255,0.2)
note over User,SourcesScreen: Drag a group header
User->>SourcesScreen: drag header (ReorderableItem)
SourcesScreen->>SourcesScreen: rememberReorderableLazyListState(from,to)
SourcesScreen->>SourcesScreenModel: onReorderGroup(groupKey,newIndex)
SourcesScreenModel->>SourcePreferences: customGroupOrder().get()
SourcePreferences-->>SourcesScreenModel: comma-separated group keys
SourcesScreenModel->>SourcePreferences: customGroupOrder().set(...)
end
rect rgba(200,255,200,0.2)
note over User,SourcesScreen: Drag a source within a group
User->>SourcesScreen: drag item (ReorderableItem)
SourcesScreen->>SourcesScreen: rememberReorderableLazyListState(from,to)
SourcesScreen->>SourcesScreenModel: onReorderSource(groupKey,source.id,newIndex)
SourcesScreenModel->>SourcesScreenModel: reorderSourceWithinGroup(...)
SourcesScreenModel->>SourceRepository: updateSort(sourceId, sort=index)
SourceRepository->>Database: sourcesQueries.updateSort(sort, sourceId)
Database-->>SourceRepository: updated rows
end
Database-->>SourceRepository: sourcesQueries.findAll(id, sort)
SourceRepository->>SourceRepository: combine(sourceManager.sources, sourceSorts)
SourceRepository-->>SourcesScreenModel: Flow<List<Source(sort=...)>>
SourcesScreenModel-->>SourcesScreen: State.items sorted by sort within groups
SourcesScreen-->>User: reordered sources and groups persist
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
StubSourceRepositoryImpl, the newsortparameter is accepted but ignored when constructingStubSource; consider either using it or removing it from the signature to avoid confusion in tests and stubs. - The
customGroupOrderpreference is stored as a comma-separated string and merged with current headers; if any future group keys can contain commas or leading/trailing whitespace, you may want to normalize or escape keys when persisting and reading to avoid subtle ordering bugs.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `StubSourceRepositoryImpl`, the new `sort` parameter is accepted but ignored when constructing `StubSource`; consider either using it or removing it from the signature to avoid confusion in tests and stubs.
- The `customGroupOrder` preference is stored as a comma-separated string and merged with current headers; if any future group keys can contain commas or leading/trailing whitespace, you may want to normalize or escape keys when persisting and reading to avoid subtle ordering bugs.
## Individual Comments
### Comment 1
<location path="app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt" line_range="209-216" />
<code_context>
+ }
+ if (toHeaderIdx != fromHeaderIdx) return@rememberReorderableLazyListState
+
+ // Move the item in the list
+ itemsState.removeAt(from.index)
+ itemsState.add(to.index, sourceItem)
+
+ // Compute new index within the group
+ val groupStart = fromHeaderIdx + 1
+ val newIndex = to.index - groupStart
+ onReorderSource(groupKey, sourceItem.source.id, newIndex)
+ }
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** Adjust `to.index` when re-inserting after removal to avoid off‑by‑one moves
Because you remove the item before re‑inserting, indices after `from.index` shift left. When `from.index < to.index`, inserting at the original `to.index` will place the item one position too far down. Consider adjusting the insertion index, e.g.:
```kotlin
val targetIndex = if (from.index < to.index) to.index - 1 else to.index
itemsState.removeAt(from.index)
itemsState.add(targetIndex.coerceIn(0, itemsState.size), sourceItem)
```
so the drop position matches the visual target.
</issue_to_address>
### Comment 2
<location path="app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt" line_range="131-137" />
<code_context>
.filter(queryFilter(searchQuery))
- // KMK <--
+
+ // User-customized group order stored as comma-separated keys (e.g. "pinned,en,ja")
+ val customGroupOrderList = sourcePreferences.customGroupOrder().get()
+ .split(",")
+ .filter { it.isNotBlank() }
mutableState.update { state ->
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
+ // Pinned and last_used always stay at top, regardless of custom order
+ when {
+ d1 == PINNED_KEY && d2 != PINNED_KEY -> return@TreeMap -1
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify or revise interaction between custom group order and pinned/last_used groups
The comparator always keeps `PINNED_KEY` and `LAST_USED_KEY` at the top, but `reorderGroup()` still persists these keys into `custom_source_group_order`. So users can drag these groups, yet their position relative to others is effectively fixed by the comparator. Consider either excluding these keys from reordering/persistence, or adjusting the comparator so custom order can also move them. Otherwise the UI suggests they are fully reorderable when they are not.
Suggested implementation:
```
// User-customized group order stored as comma-separated keys (e.g. "en,ja").
// Special groups (PINNED_KEY, LAST_USED_KEY) are excluded from custom ordering and always stay at the top.
val customGroupOrderList = sourcePreferences.customGroupOrder().get()
.split(",")
.filter { it.isNotBlank() }
.filter { it != PINNED_KEY && it != LAST_USED_KEY }
mutableState.update { state ->
val map = TreeMap<String, MutableList<Source>> { d1, d2 ->
// Pinned and last_used always stay at top, regardless of custom order
```
```
// Custom group order takes priority over default alphabetical sorting for non-special groups
val d1Custom = customGroupOrderList.indexOf(d1)
val d2Custom = customGroupOrderList.indexOf(d2)
if (d1Custom != -1 && d2Custom != -1) return@TreeMap d1Custom.compareTo(d2Custom)
if (d1Custom != -1) return@TreeMap -1
if (d2Custom != -1) return@TreeMap 1
```
To fully align behavior with this change and avoid persisting misleading order for special groups:
1. Update the `reorderGroup()` (or equivalent drag-reorder handler) in this file to skip `PINNED_KEY` and `LAST_USED_KEY` when building and saving `custom_source_group_order` (i.e., do not include them in the comma-separated list written to `sourcePreferences.customGroupOrder()`).
2. If the persisted `custom_source_group_order` currently includes `PINNED_KEY` or `LAST_USED_KEY`, consider adding a small migration/cleanup step (e.g., when reading, strip those keys before saving back) so existing users' preferences are normalized.
3. Ensure any UI affordances for reordering clearly prevent dragging these special groups, or at least that drag operations on them do not alter the stored custom order.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- Add sort column to sources table (migration 47) for persisting source order within groups - Add customGroupOrder preference for persisting group order - Add reorder mode toggle with drag handles on headers and items - Group headers move entire block; sources only move within group - Pinned always top, then last_used, then custom-ordered groups, then categories, then alphabetical language groups - Extract sourceGroupKey() and groupKeyOf() helpers to deduplicate group key computation
JoeJoeflyn
force-pushed
the
feat/source-reordering
branch
from
July 21, 2026 20:07
54a6c34 to
150a964
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
sortcolumn tosourcestable (migration 47) for persisting source order within groupscustomGroupOrderpreference for persisting group ordersourceGroupKey()andgroupKeyOf()helpers to deduplicate group key computationTest plan
Summary by Sourcery
Add a drag-to-reorder mode in the Browse → Sources screen, persisting custom ordering of source groups and individual sources.
New Features:
Enhancements:
Build:
Documentation:
Chores:
screenrecording-2026-07-22_02-27-04.mp4