Skip to content

feat: Drag-to-reorder sources and groups in Browse - #1812

Open
JoeJoeflyn wants to merge 1 commit into
komikku-app:masterfrom
JoeJoeflyn:feat/source-reordering
Open

JoeJoeflyn wants to merge 1 commit into
komikku-app:masterfrom
JoeJoeflyn:feat/source-reordering

Conversation

@JoeJoeflyn

@JoeJoeflyn JoeJoeflyn commented Jul 21, 2026

Copy link
Copy Markdown

Summary

  • 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 (drag handle icon in Sources toolbar) with drag handles on both group headers and source items
  • Group headers move the entire group block; sources only move within their own group
  • Pinned group always stays at top, then last_used, then custom-ordered groups, then SY categories, then alphabetical language groups
  • Extract sourceGroupKey() and groupKeyOf() helpers to deduplicate group key computation

Test plan

  • Open Browse → Sources, tap drag handle icon in toolbar
  • Drag a source within a group — order persists after app restart
  • Drag a group header — entire group block moves
  • Verify pinned sources stay at top regardless of custom order
  • Verify last_used group stays below pinned
  • Exit reorder mode, verify normal browse still works

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:

  • Introduce a reorder mode toggle in the Sources tab with drag handles on group headers and source items.
  • Allow reordering of source groups with a custom group order preference that affects listing order.
  • Enable reordering of sources within each group and persist their positions via a new sort field.

Enhancements:

  • Update source grouping and sorting logic so pinned and last-used groups remain at the top, followed by custom-ordered groups, SY categories, and language groups.
  • Refine source list UI headers and items to support integrated drag handles while keeping normal browsing behavior unchanged.
  • Centralize group key computation into reusable helpers for consistent grouping across the screen model.

Build:

  • Wire up SourceRepository to read and update per-source sort values from the database, including stubs for testing.

Documentation:

  • Add a localized string for the new "Reorder sources" action in the toolbar.

Chores:

  • Extend the Source domain model with a sort field used for custom in-group ordering.
screenrecording-2026-07-22_02-27-04.mp4

@sourcery-ai

sourcery-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements 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 groups

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Add a reorder mode to Sources screen with drag handles and drag-and-drop behavior for groups and items.
  • Extend SourcesScreen parameters with callbacks for group and source reorder events.
  • Conditionally render either the existing fast-scrolling list or a new LazyColumn when reorderMode is enabled.
  • Use rememberReorderableLazyListState and ReorderableItem to manage drag interactions, including separate behavior for moving headers vs items.
  • Maintain a local mutable list of SourceUiModel items for visual reordering, synchronized with state via LaunchedEffect.
  • Add drag handle icons to SourceHeader and SourceItem via new showDragHandle and dragModifier parameters, wiring them to draggableHandle().
app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt
Introduce state and logic in SourcesScreenModel to support reorder mode, group ordering, and source ordering within groups.
  • Add reorderMode flag to State, with toggleReorderMode handler used by the toolbar action.
  • Change grouping logic to use new sourceGroupKey helper and to apply custom group order preference while keeping pinned and last_used groups fixed at the top.
  • Sort sources within each group using the new sort field, with name as a tiebreaker.
  • Implement reorderGroup to modify the customGroupOrder preference based on drag results.
  • Implement reorderSourceWithinGroup to recompute sort indices for all sources in a group and persist them via SourceRepository.updateSort.
  • Extract sourceGroupKey and groupKeyOf helpers for consistent group-key computation across UI and model.
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt
Wire toolbar UI to toggle reorder mode and pass reorder callbacks to the Sources screen.
  • Add a new AppBar.Action with drag indicator icon and localized title to toggle reorder mode, highlighting when active.
  • Pass screenModel::reorderGroup and screenModel::reorderSourceWithinGroup into SourcesScreen alongside existing callbacks.
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesTab.kt
i18n-kmk/src/commonMain/moko-resources/base/strings.xml
Persist per-source ordering in the database and expose it through the domain model and repository.
  • Extend Source domain model with a sort Long field defaulting to 0.
  • Subscribe to sources.sqldelight table to read id and sort values and combine them with SourceManager sources in SourceRepositoryImpl.getSources, populating the new sort field.
  • Add updateSort(sourceId, sort) to SourceRepository interface and implement it in SourceRepositoryImpl via sourcesQueries.updateSort.
  • Update StubSourceRepositoryImpl signature to include sort parameter in StubSource construction.
  • Add migration 47 and sources.sqldelight changes to define the new sort column and update logic (as referenced in the summary, though the diff for 47.sqm is elided).
domain/src/main/java/tachiyomi/domain/source/model/Source.kt
domain/src/main/java/tachiyomi/domain/source/repository/SourceRepository.kt
data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt
data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt
data/src/main/sqldelight/tachiyomi/data/sources.sq
data/src/main/sqldelight/tachiyomi/migrations/47.sqm
Add preference storage for custom source group ordering.
  • Introduce customGroupOrder() string preference in SourcePreferences with documentation describing the comma-separated group-key format.
  • Use this preference in SourcesScreenModel both to read the saved group ordering when building the group TreeMap and to persist changes in reorderGroup.
app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt Outdated
Comment thread app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt Outdated
- 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
JoeJoeflyn force-pushed the feat/source-reordering branch from 54a6c34 to 150a964 Compare July 21, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant