Skip to content

Add tracker Not in Library tab for MAL and AniList#1557

Open
JohanKRS wants to merge 5 commits intokomikku-app:masterfrom
JohanKRS:add-not-in-library-tracker-support
Open

Add tracker Not in Library tab for MAL and AniList#1557
JohanKRS wants to merge 5 commits intokomikku-app:masterfrom
JohanKRS:add-not-in-library-tracker-support

Conversation

@JohanKRS
Copy link
Copy Markdown

@JohanKRS JohanKRS commented Mar 6, 2026

Summary

Closes #1556.

Adds an opt-in tracker-only Not in Library tab when the library is grouped by Tracker Status, with support for MyAnimeList and AniList.

What changed

  • added a new Tracking preference to enable or disable tracker-only Not in Library entries
  • marked the supported tracker rows in settings with Supports "Not in Library" tracking
  • added explicit remote-list support for MyAnimeList and AniList so their unmatched tracker entries can be loaded into the library UI
  • added a synthetic Not in Library tracker-status tab that appears after the normal tracker-status groups
  • rendered tracker-only entries in the existing library list/grid layouts with tracker badge, status, and progress
  • routed tracker-only entries to Global Search with the tracker title prefilled
  • carried the originating tracker entry through the add-to-library flow so favoriting the manga automatically binds the existing tracker record
  • fixed the cold-start Global Search handoff so the first tap works even when no pinned sources are configured
  • added regression coverage for tracker entry mapping, remote-only library grouping, the pending bind flow, and the search filter normalization

Verification

  • :app:testDebugUnitTest --tests 'eu.kanade.domain.track.interactor.AddTracksTest' --tests 'eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModelTest' --tests 'eu.kanade.tachiyomi.data.track.TrackerRemoteEntryMappingTest' --tests 'eu.kanade.tachiyomi.ui.library.RemoteTrackerLibraryTest'
  • :app:compileDebugKotlin
  • :app:assembleDebug
  • manual APK testing covered:
    • the Not in Library tab and supported tracker indicator
    • adding a title from the tracker-only handoff and confirming it no longer ends up in Not tracked
    • opening Global Search from a Not in Library item on a cold start

Notes

  • the Not in Library tab is intentionally opt-in to avoid unexpected tracker list fetches
  • tracker-only rows are ephemeral UI entries and do not participate in library selection or bulk actions
  • entries from different trackers are shown separately rather than cross-deduplicated

Summary by Sourcery

Add an opt-in "Not in Library" tracker tab and remote tracker entry integration for supported services when grouping the library by tracker status.

New Features:

  • Introduce a library grouping tab that surfaces tracker entries not yet in the local library for supported trackers when enabled in settings.
  • Allow opening tracker-only entries into Global Search and carrying the tracker context through to source and manga screens so adding to library auto-binds the track record.

Bug Fixes:

  • Normalize global search source filter behavior so cold-start searches work when no pinned sources are configured.

Enhancements:

  • Render tracker-only items alongside local library entries in list and grid layouts with appropriate tracker badges and progress display.
  • Extend MyAnimeList and AniList APIs and tracker implementations to fetch and map full remote lists for use as tracker-only library entries.
  • Augment tracking settings UI to indicate trackers that support "Not in Library" entries and add a toggle to show or hide these entries.
  • Refine library pager and refresh behavior to support the synthetic "Not in Library" category without affecting normal library updates.

Tests:

  • Add unit tests covering remote tracker entry mapping, library display of remote-only items, pending tracker binding on add-to-library, and global search filter normalization.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 6, 2026

Reviewer's Guide

Implements an opt-in "Not in Library" virtual tracker-status tab for MyAnimeList and AniList when the library is grouped by tracker status, including fetching remote-only tracker entries, displaying them alongside local library items with dedicated badges, and binding them automatically when the user adds the manga to their library, plus small UX fixes and regression tests.

Sequence diagram for clicking a NotInLibrary tracker entry and binding it on add-to-library

sequenceDiagram
    actor User
    participant LibraryUI
    participant LibraryScreenModel
    participant GlobalSearchScreen
    participant GlobalSearchScreenModel
    participant BrowseSourceScreen
    participant BrowseSourceScreenModel
    participant MangaScreen
    participant MangaScreenModel
    participant AddTracks
    participant TrackerManager
    participant Tracker
    participant TrackSearch

    %% User taps a Not in Library entry in the library
    User->>LibraryUI: Tap RemoteTrackerLibraryItem
    LibraryUI->>GlobalSearchScreen: push(GlobalSearchScreen(searchQuery = track.title, initialTrackSearch = track.toTrackSearch()))

    %% Global search runs with prefilled query
    GlobalSearchScreen->>GlobalSearchScreenModel: init with searchQuery, initialTrackSearch
    GlobalSearchScreenModel->>GlobalSearchScreenModel: search()
    GlobalSearchScreenModel-->>GlobalSearchScreen: SearchItemResult (per source)

    alt Single manga match in a source
        GlobalSearchScreen->>MangaScreen: replace(MangaScreen(mangaId, fromSource = true, initialTrackSearch))
        MangaScreen->>MangaScreenModel: init(mangaId, isFromSource = true, initialTrackSearch)
    else Multiple matches
        User->>GlobalSearchScreen: Tap source result
        GlobalSearchScreen->>BrowseSourceScreen: push(BrowseSourceScreen(sourceId, searchQuery, initialTrackSearch))
        BrowseSourceScreen->>BrowseSourceScreenModel: init(initialTrackSearch)
        User->>BrowseSourceScreen: Tap manga
        BrowseSourceScreen->>MangaScreen: push(MangaScreen(mangaId, fromSource = true, initialTrackSearch))
        MangaScreen->>MangaScreenModel: init(mangaId, isFromSource = true, initialTrackSearch)
    end

    %% User adds manga to library from Manga screen
    User->>MangaScreen: Tap favorite
    MangaScreen->>MangaScreenModel: onFavoriteClick(...)
    MangaScreenModel->>MangaScreenModel: updateManga.awaitUpdateFavorite(manga.id, true)
    MangaScreenModel->>MangaScreenModel: onMangaAddedToLibrary(manga, source, openTrackDialog)

    %% Pending track search binding
    MangaScreenModel->>MangaScreenModel: bindPendingTrackSearch(manga.id)
    MangaScreenModel->>TrackerManager: get(trackSearch.tracker_id)
    TrackerManager-->>MangaScreenModel: Tracker
    MangaScreenModel->>AddTracks: bind(tracker, trackSearch, mangaId)
    AddTracks->>TrackSearch: set manga_id = mangaId
    AddTracks->>Tracker: bind(item, hasReadChapters)
    Tracker-->>AddTracks: bound
    AddTracks-->>MangaScreenModel: success
    MangaScreenModel->>MangaScreenModel: pendingTrackSearch = null

    %% Optional: auto-open track dialog
    alt autoOpenTrack enabled and openTrackDialog
        MangaScreenModel->>MangaScreenModel: showTrackDialog()
    end

    MangaScreenModel-->>User: Manga added and tracker record bound
Loading

Class diagram for remote tracker NotInLibrary support

classDiagram
    direction LR

    class Tracker {
      <<interface>>
      +id: Long
      +name: String
      +bind(item: Track, hasReadChapters: Boolean)
    }

    class TrackerWithNotInLibrary {
      <<interface>>
      +getNotInLibraryEntries(): List~TrackSearch~
    }

    TrackerWithNotInLibrary --|> Tracker

    class MyAnimeList {
      +id: Long
      +login(username: String, password: String)
      +getNotInLibraryEntries(): List~TrackSearch~
    }

    class Anilist {
      +id: Long
      +login(username: String, password: String)
      +getNotInLibraryEntries(userId: Int): List~TrackSearch~
    }

    MyAnimeList ..|> TrackerWithNotInLibrary
    Anilist ..|> TrackerWithNotInLibrary

    class TrackSearch {
      +tracker_id: Long
      +remote_id: Long
      +title: String
      +status: Long
      +last_chapter_read: Double
      +total_chapters: Long
      +cover_url: String
      +tracking_url: String
      +create(trackerId: Long): TrackSearch
    }

    class RemoteTrackerTrack {
      +trackerId: Long
      +remoteId: Long
      +title: String
      +coverUrl: String
      +status: Long
      +lastChapterRead: Double
      +totalChapters: Long
      +trackingUrl: String
    }

    class RemoteTrackerLibraryItem {
      +track: RemoteTrackerTrack
      +trackerName: String
      +trackerShortName: String
      +statusText: String?
      +progressText: String?
      +key: String
      +coverData: MangaCover
    }

    class MangaCover {
      +mangaId: Long
      +sourceId: Long
      +isMangaFavorite: Boolean
      +ogUrl: String?
      +lastModified: Long
    }

    RemoteTrackerLibraryItem --> RemoteTrackerTrack : wraps
    RemoteTrackerLibraryItem --> MangaCover : builds

    class LibraryDisplayItem {
      <<interface>>
      +key: String
    }

    class Local {
      +item: LibraryItem
      +key: String
    }

    class RemoteTrack {
      +item: RemoteTrackerLibraryItem
      +key: String
    }

    LibraryDisplayItem <|.. Local
    LibraryDisplayItem <|.. RemoteTrack

    class LibraryItem {
      +id: Long
      +libraryManga: LibraryManga
      +downloadCount: Int
      +unreadCount: Int
      +isLocal: Boolean
      +sourceLanguage: String
      +useLangIcon: Boolean
      +source: Source
    }

    Local --> LibraryItem : wraps
    RemoteTrack --> RemoteTrackerLibraryItem : wraps

    class RemoteTrackerFetchConfig {
      +enabled: Boolean
      +trackers: List~TrackerWithNotInLibrary~
    }

    class RemoteTrackKey {
      +trackerId: Long
      +remoteId: Long
    }

    class LibraryScreenModelState {
      +groupType: LibraryGroup
      +groupedFavorites: Map~Category,List~Long~~
      +rawRemoteTrackerItems: List~RemoteTrackerTrack~
      +remoteTrackerItems: List~RemoteTrackerLibraryItem~
      +displayedCategories: List~Category~
    }

    class LibraryScreenModel {
      -remoteTrackerItemsJob: Job?
      -remoteTrackerFetchConfig: RemoteTrackerFetchConfig
      +refreshRemoteTrackerItems(): Boolean
      +buildRemoteTrackerLibraryItems(...): List~RemoteTrackerLibraryItem~
      +buildTrackedRemoteKeys(...): Set~RemoteTrackKey~
    }

    LibraryScreenModel --> LibraryScreenModelState : manages
    LibraryScreenModel --> TrackerWithNotInLibrary : uses
    LibraryScreenModelState --> RemoteTrackerLibraryItem : contains
    LibraryScreenModelState --> RemoteTrackerTrack : contains

    class TrackPreferences {
      +showNotInLibraryTrackerEntries(): BooleanPreference
    }

    class TrackStatus {
      <<enum>>
      +NOT_IN_LIBRARY
    }

    LibraryScreenModel --> TrackPreferences : reads
    LibraryScreenModelState --> TrackStatus : uses NOT_IN_LIBRARY id

    class AddTracks {
      +bind(tracker: Tracker, item: Track, mangaId: Long)
      +bindEnhancedTrackers(manga: Manga, source: Source)
    }

    class MangaScreenModel {
      -pendingTrackSearch: TrackSearch?
      +onFavoriteClick(skipPreOnFavoriteAction: Boolean)
      -onMangaAddedToLibrary(manga: Manga, source: Source, openTrackDialog: Boolean)
      -bindPendingTrackSearch(mangaId: Long)
    }

    class BrowseSourceScreenModel {
      -pendingTrackSearch: TrackSearch?
      +changeMangaFavorite(manga: Manga)
      -bindPendingTrackSearch(mangaId: Long)
    }

    MangaScreenModel --> AddTracks : uses
    BrowseSourceScreenModel --> AddTracks : uses
    MangaScreenModel --> TrackSearch : holds pendingTrackSearch
    BrowseSourceScreenModel --> TrackSearch : holds pendingTrackSearch

    class GlobalSearchScreen {
      +searchQuery: String
      +initialTrackSearch: TrackSearch?
    }

    class BrowseSourceScreen {
      +initialTrackSearch: TrackSearch?
    }

    class MangaScreen {
      +mangaId: Long
      +fromSource: Boolean
      +initialTrackSearch: TrackSearch?
    }

    GlobalSearchScreen --> TrackSearch : passes initialTrackSearch
    GlobalSearchScreen --> BrowseSourceScreen : pushes with initialTrackSearch
    GlobalSearchScreen --> MangaScreen : opens with initialTrackSearch
    BrowseSourceScreen --> BrowseSourceScreenModel : creates with initialTrackSearch
    MangaScreen --> MangaScreenModel : creates with initialTrackSearch

    class TrackPreferencesWidget {
      +tracker: Tracker
      +checked: Boolean
      +supportLabel: String?
    }

    class TrackerPreferenceItem {
      +tracker: Tracker
      +login(): Unit
      +logout(): Unit
      +supportLabel: String?
    }

    TrackerPreferenceItem --> TrackPreferencesWidget : configures
    TrackerPreferenceItem --> Tracker : references
Loading

File-Level Changes

Change Details Files
Add fetching and state management for remote-only tracker entries and expose them as a synthetic Not in Library category in the library UI.
  • Inject TrackPreferences and TrackerWithNotInLibrary support into LibraryScreenModel and track a RemoteTrackerFetchConfig based on group-by mode, preference state, and logged-in trackers.
  • Start/cancel a background job to fetch Not in Library entries from all supported trackers in parallel and store rawRemoteTrackerItems in state.
  • Derive filtered, sorted remoteTrackerItems from raw remote entries, search query, and existing tracked keys using helper functions, and expose them as LibraryDisplayItem.RemoteTrack via getDisplayItemsForCategory.
  • Insert a synthetic Not in Library Category into displayedCategories when remote tracker items exist and adjust item counts and empty-library detection accordingly.
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryDisplayItem.kt
app/src/test/java/eu/kanade/tachiyomi/ui/library/RemoteTrackerLibraryTest.kt
Render remote tracker-only entries in existing list and grid layouts with tracker-specific badges and click handling that routes into Global Search and the library add flow.
  • Change LibraryList, LibraryCompactGrid, and LibraryComfortableGrid to operate on a sealed LibraryDisplayItem type, branching between local LibraryItem and RemoteTrackerLibraryItem rendering.
  • Add tracker-specific badges (tracker label, status, progress) and non-library-colored styling for remote items while preserving existing badges for local items.
  • Extend LibraryPager and LibraryContent to pass both a getDisplayItemsForCategory function and an onClickRemoteTrack callback into the list/grid composables.
  • Hook LibraryTab to open GlobalSearchScreen with the track title and a TrackSearch built from the RemoteTrackerTrack when a remote item is clicked.
app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt
app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt
app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt
app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt
app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt
app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt
Wire remote tracker entries through Global Search, Browse Source, and Manga screens so adding a manga from a tracker-only entry automatically binds the existing tracker record.
  • Thread an optional initialTrackSearch parameter through GlobalSearchScreen, BrowseSourceScreen, MangaScreen, BrowseSourceScreenModel, and MangaScreenModel.
  • Store pendingTrackSearch in the screen models and, on manga being favorited, call a new onMangaAddedToLibrary helper that binds any pending TrackSearch before enhanced trackers and optionally opens the track dialog.
  • Add mapping helpers between TrackSearch and RemoteTrackerTrack, and ensure AddTracks.bind assigns manga_id on the Track before delegating to the tracker and inserting the DB entity.
  • Add tests covering that AddTracks.bind propagates mangaId and that RemoteTrackerTrack/TrackSearch mapping retains the necessary fields for pending binding.
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
app/src/test/java/eu/kanade/domain/track/interactor/AddTracksTest.kt
app/src/test/java/eu/kanade/tachiyomi/data/track/TrackerRemoteEntryMappingTest.kt
Extend MyAnimeList and AniList tracker APIs and implementations to fetch full remote lists and expose Not in Library entries for the new UI.
  • Create TrackerWithNotInLibrary interface and implement it in MyAnimeList and Anilist trackers with getNotInLibraryEntries that delegates to their APIs.
  • For AniList, add a paginated GraphQL query on AnilistApi.getNotInLibraryEntries that loads mediaList with list status and media fields, mapping ALUserListItem to TrackSearch.
  • For MAL, add MALUserListResult DTOs and MyAnimeListApi.getNotInLibraryEntries/getUserListPage that page through /users/@me/mangalist with extended LIST_FIELDS and map to TrackSearch via parseListSearchItem.
  • Add ALUserManga.toTrackSearch and extend ALUserListMediaList with pageInfo to support AniList list paging, plus tests verifying DTO deserialization and mapping.
app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerWithNotInLibrary.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserList.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt
app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt
app/src/test/java/eu/kanade/tachiyomi/data/track/TrackerRemoteEntryMappingTest.kt
Add user-facing preference and settings UI for enabling Not in Library tracker entries and indicating tracker support.
  • Add TrackPreferences.showNotInLibraryTrackerEntries preference and wire it into LibraryScreenModel to gate remote fetching and item building.
  • Expose a new SwitchPreference in SettingsTrackingScreen with title and summary strings, and pass a supportLabel into TrackerPreference for MAL and AniList.
  • Extend TrackingPreferenceWidget to render an optional secondary supportLabel line under the tracker name using new TrackerLabel and SupportLabel composables.
  • Add localized strings for the Not in Library preference, support label, Not in Library tab title, and refresh snackbar text.
app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt
app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
i18n/src/commonMain/moko-resources/base/strings.xml
Improve global search source filter normalization to avoid broken Pinned-only behavior on cold start and reuse it across search screens.
  • Extract normalizeSearchSourceFilter helper in SearchScreenModel that coerces SourceFilter.PinnedOnly to SourceFilter.All when there are no pinned sources.
  • Call shouldPinnedSourcesHidden (which uses the new normalizer) at the start of search() and during SearchScreenModel initialization for both GlobalSearchScreenModel and MigrateSearchScreenModel.
  • Add unit tests for normalizeSearchSourceFilter behavior with and without pinned sources.
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt
app/src/test/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModelTest.kt
Extend TrackStatus enum and wire in UX behaviour for refreshing Not in Library entries and hiding the standard empty-library screen when only remote items exist.
  • Add NOT_IN_LIBRARY value to TrackStatus enum with its own string resource.
  • Use TrackStatus.NOT_IN_LIBRARY.int as the synthetic category id and order when building the Not in Library tab, and ensure the library-empty check also considers hasRemoteTrackerItems.
  • Adjust LibraryTab refresh behavior to trigger screenModel.refreshRemoteTrackerItems for the Not in Library category and show a dedicated snackbar message.
  • Add i18n strings for the Not in Library tab label and refresh snackbar content.
app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
i18n/src/commonMain/moko-resources/base/strings.xml

Assessment against linked issues

Issue Objective Addressed Explanation
#1556 When the library is grouped by Tracker Status, add a "Not in Library" tab that shows tracker entries which exist in the user's tracker but not in the local Komikku library.
#1556 From the "Not in Library" tab, allow the user to add those tracker-only entries into the library using one of the installed sources.

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the tracking functionality by introducing a dedicated 'Not in Library' tab. This allows users to view and manage manga they are tracking on services like MyAnimeList and AniList, even if those manga haven't been added to their local library. The changes streamline the process of adding tracked manga to the library and improve the overall user experience by providing better visibility and integration of external tracking data within the application's library interface.

Highlights

  • New "Not in Library" Tab: Introduced an opt-in 'Not in Library' tab within the Library screen when grouped by 'Tracker Status'. This tab displays manga entries from supported trackers (MyAnimeList and AniList) that are being tracked but have not yet been added to the local library.
  • Tracker Integration and UI: Implemented explicit remote-list support for MyAnimeList and AniList, allowing their untracked entries to be loaded and displayed in the library UI. These entries are rendered with tracker badges, status, and progress, and can be routed to Global Search with the title prefilled for easy addition to the library.
  • Add to Library Flow Enhancement: Improved the add-to-library flow to carry the originating tracker entry, ensuring that favoriting a manga automatically binds the existing tracker record to the newly added library entry.
  • Global Search Fix: Fixed a cold-start issue in Global Search, ensuring that the first tap works correctly even when no pinned sources are configured.
  • New Preferences and UI Elements: Added a new tracking preference to enable/disable the 'Not in Library' tab and marked supported trackers in settings with a 'Supports "Not in Library" tracking' label. New composable badges (TrackerLabelBadge, TrackerStatusBadge, TrackerProgressBadge) were added for displaying tracker information in the library.
  • New Interfaces and Status: Introduced a TrackerWithNotInLibrary interface for trackers that support fetching remote entries not in the local library, and added a NOT_IN_LIBRARY status to TrackStatus.
  • Comprehensive Testing: Added regression coverage for tracker entry mapping, remote-only library grouping, the pending bind flow, and search filter normalization, ensuring the stability and correctness of the new features.
Changelog
  • app/src/main/java/eu.kanade/presentation/more/settings/PreferenceItem.kt
    • Passed the new supportLabel to the TrackingPreferenceWidget.
  • app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
    • Assigned manga_id to the Track item before binding and inserting it, ensuring correct association during the add-to-library flow.
  • app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt
    • Added a new preference showNotInLibraryTrackerEntries to control the visibility of the 'Not in Library' tab.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryBadges.kt
    • Added TrackerLabelBadge, TrackerStatusBadge, and TrackerProgressBadge composables for displaying tracker-related information on library items.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryComfortableGrid.kt
    • Updated LibraryComfortableGrid to accept LibraryDisplayItem and render RemoteTrackerLibraryItem with new tracker badges.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryCompactGrid.kt
    • Updated LibraryCompactGrid to accept LibraryDisplayItem and render RemoteTrackerLibraryItem with new tracker badges.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt
    • Modified LibraryContent to pass onClickRemoteTrack and use getDisplayItemsForCategory for displaying library items.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryList.kt
    • Updated LibraryList to accept LibraryDisplayItem and render RemoteTrackerLibraryItem with new tracker badges.
  • app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt
    • Modified LibraryPager to use LibraryDisplayItem and pass onClickRemoteTrack to its child components.
  • app/src/main/java/eu/kanade/presentation/more/settings/Preference.kt
    • Added an optional supportLabel parameter to TrackerPreference for displaying additional information.
  • app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
    • Added a new SwitchPreference for showNotInLibraryTrackerEntries and included supportLabel for MyAnimeList and AniList trackers.
  • app/src/main/java/eu/kanade/presentation/more/settings/widget/TrackingPreferenceWidget.kt
    • Refactored TrackingPreferenceWidget to display the supportLabel below the tracker name.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/TrackStatus.kt
    • Added a new NOT_IN_LIBRARY entry to the TrackStatus enum.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerWithNotInLibrary.kt
    • Added a new interface TrackerWithNotInLibrary with a getNotInLibraryEntries suspend function.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt
    • Implemented the TrackerWithNotInLibrary interface and provided an implementation for getNotInLibraryEntries.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt
    • Added a getNotInLibraryEntries suspend function to fetch untracked manga from AniList, including pagination logic.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt
    • Added a toTrackSearch extension function to convert ALUserManga to TrackSearch.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt
    • Added ALUserListPageInfo data class and included pageInfo in ALUserListMediaList for pagination.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt
    • Implemented the TrackerWithNotInLibrary interface and provided an implementation for getNotInLibraryEntries.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt
    • Added getNotInLibraryEntries and getUserListPage suspend functions to fetch untracked manga from MyAnimeList, including pagination and a new LIST_FIELDS constant.
  • app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/dto/MALUserList.kt
    • Added new data classes MALUserListResult and MALUserListNode for deserializing MyAnimeList user list responses.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt
    • Moved the call to shouldPinnedSourcesHidden() to ensure correct initialization.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
    • Added initialTrackSearch parameter to BrowseSourceScreen and passed it to MangaScreen.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt
    • Added initialTrackSearch as a constructor parameter and implemented bindPendingTrackSearch to associate tracker entries when a manga is favorited.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt
    • Added initialTrackSearch parameter to GlobalSearchScreen and passed it to MangaScreen and BrowseSourceScreen for consistent tracking.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt
    • Moved the call to shouldPinnedSourcesHidden() to ensure correct initialization.
  • app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt
    • Refactored shouldPinnedSourcesHidden to normalize the search source filter and added a new utility function normalizeSearchSourceFilter.
  • app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryDisplayItem.kt
    • Added sealed interface LibraryDisplayItem and data classes RemoteTrackerTrack, RemoteTrackerLibraryItem, along with utility functions for conversion and cover ID generation.
  • app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt
    • Integrated logic for fetching, filtering, and displaying remote tracker items in the library, including the new 'Not in Library' category and associated state management.
  • app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt
    • Updated LibraryTab to handle the new 'Not in Library' category, refresh remote tracker entries, and navigate to Global Search with prefilled data for remote items.
  • app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt
    • Added initialTrackSearch parameter to MangaScreen for passing tracker information during navigation.
  • app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt
    • Added initialTrackSearch as a constructor parameter and implemented bindPendingTrackSearch to associate tracker entries when a manga is added to the library.
  • app/src/test/java/eu/kanade/domain/track/interactor/AddTracksTest.kt
    • Added a unit test to verify that manga_id is correctly assigned to TrackSearch before binding and insertion.
  • app/src/test/java/eu/kanade/tachiyomi/data/track/TrackerRemoteEntryMappingTest.kt
    • Added unit tests to confirm correct mapping between AniList/MyAnimeList DTOs and TrackSearch/RemoteTrackerTrack objects.
  • app/src/test/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModelTest.kt
    • Added unit tests for normalizeSearchSourceFilter to ensure correct behavior when handling pinned sources.
  • app/src/test/java/eu/kanade/tachiyomi/ui/library/RemoteTrackerLibraryTest.kt
    • Added unit tests for buildTrackedRemoteKeys, buildRemoteTrackerLibraryItems, remoteTrackerItemMatchesQuery, remoteTrackerProgressText, and trackerBadgeLabel.
  • i18n/src/commonMain/moko-resources/base/strings.xml
    • Added new string resources: pref_tracker_show_not_in_library_entries, pref_tracker_show_not_in_library_entries_summary, tracker_not_in_library_supported, not_in_library, and refreshing_tracker_entries.
Activity
  • The pull request introduces a new feature and includes several new unit tests to cover the added functionality and ensure correctness. No specific human review comments or progress updates were provided in the description.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


In digital halls, where manga reside, A new tab appears, with nowhere to hide. Tracked tales unseen, now brought to the light, AniList, MAL, shining ever so bright. Your library grows, with a click and a bind.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The recursive implementations of AnilistApi.getNotInLibraryEntries and MyAnimeListApi.getNotInLibraryEntries can walk many pages and risk deep recursion; consider rewriting these to an iterative loop that accumulates results to avoid potential stack overflows on large lists.
  • In LibraryState.displayedCategories you call Injekt.get<Application>().stringResource(...) inside a derived state property; this mixes DI and Android resources into what is otherwise a pure UI state helper—consider moving the string resolution into the composable layer and passing the label in rather than pulling it here.
  • The trackerCoverId bit-packing scheme uses Long.MIN_VALUE and custom masking; to avoid accidental collisions or future misuse it would be helpful to either centralize this ID generation behind a documented helper (with tests explaining the layout) or use a more self‑describing approach (e.g., high-order prefix constants) and reference them where these IDs are interpreted.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The recursive implementations of `AnilistApi.getNotInLibraryEntries` and `MyAnimeListApi.getNotInLibraryEntries` can walk many pages and risk deep recursion; consider rewriting these to an iterative loop that accumulates results to avoid potential stack overflows on large lists.
- In `LibraryState.displayedCategories` you call `Injekt.get<Application>().stringResource(...)` inside a derived state property; this mixes DI and Android resources into what is otherwise a pure UI state helper—consider moving the string resolution into the composable layer and passing the label in rather than pulling it here.
- The `trackerCoverId` bit-packing scheme uses `Long.MIN_VALUE` and custom masking; to avoid accidental collisions or future misuse it would be helpful to either centralize this ID generation behind a documented helper (with tests explaining the layout) or use a more self‑describing approach (e.g., high-order prefix constants) and reference them where these IDs are interpreted.

## Individual Comments

### Comment 1
<location path="app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryDisplayItem.kt" line_range="71-76" />
<code_context>
+    }
+}
+
+internal fun trackerCoverId(
+    trackerId: Long,
+    remoteId: Long,
+): Long {
+    return Long.MIN_VALUE or ((trackerId and 0x7FL) shl 56) or (remoteId and 0x00FFFFFFFFFFFFFFL)
+}
</code_context>
<issue_to_address>
**suggestion:** The bit-packing scheme for `trackerCoverId` restricts trackerId to 7 bits and remoteId to 56 bits; consider documenting or asserting these constraints.

This expression preserves only the lower 7 bits of `trackerId` and the lower 56 bits of `remoteId`, so larger values will silently collide. Please either document the expected ranges for these IDs or add debug-time checks to enforce them.

```suggestion
/**
 * Packs [trackerId] and [remoteId] into a single [Long] value.
 *
 * The bit layout is:
 * - sign bit set to 1 (Long.MIN_VALUE)
 * - 7 bits for [trackerId] (bits 56–62)
 * - 56 bits for [remoteId] (bits 0–55)
 *
 * This means:
 * - [trackerId] must be in the range 0..127 (fits in 7 bits)
 * - [remoteId] must be in the range 0..0x00FFFFFFFFFFFFFF (fits in 56 bits)
 *
 * Values outside these ranges would be truncated and could cause collisions,
 * so we enforce the ranges at runtime.
 */
internal fun trackerCoverId(
    trackerId: Long,
    remoteId: Long,
): Long {
    require(trackerId in 0x0L..0x7FL) {
        "trackerId must be in range 0..127 (7 bits), was $trackerId"
    }
    require(remoteId in 0x0L..0x00FFFFFFFFFFFFFFL) {
        "remoteId must be in range 0..0x00FFFFFFFFFFFFFF (56 bits), was $remoteId"
    }

    return Long.MIN_VALUE or ((trackerId and 0x7FL) shl 56) or (remoteId and 0x00FFFFFFFFFFFFFFL)
}
```
</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.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new feature. Please review the specific comments for minor suggestions to improve code quality.

Comment on lines +997 to 1009
private suspend fun bindPendingTrackSearch(mangaId: Long) {
val trackSearch = pendingTrackSearch ?: return
val tracker = trackerManager.get(trackSearch.tracker_id) ?: return

try {
addTracks.bind(tracker, trackSearch, mangaId)
pendingTrackSearch = null
} catch (e: Exception) {
logcat(LogPriority.WARN, e) {
"Failed to bind pending tracker entry trackerId=${trackSearch.tracker_id} remoteId=${trackSearch.remote_id}"
}
}
}
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.

medium

This bindPendingTrackSearch function is identical to the one in BrowseSourceScreenModel. To improve maintainability and avoid code duplication, consider extracting this logic into a shared component, such as a new domain interactor (e.g., BindPendingTrack).

Comment on lines 85 to 95
LibraryList(
items = items,
contentPadding = contentPadding,
selection = selection,
onClick = onClickManga,
onClickRemoteTrack = onClickRemoteTrack,
onLongClick = onLongClickManga,
onClickContinueReading = onClickContinueReading,
searchQuery = searchQuery,
onGlobalSearchClicked = onGlobalSearchClicked,
)
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.

low

The LibraryList call appears to have an incorrect indentation level. It should be indented to be inside the when branch's block for better readability.

                LibraryList(
                    items = items,
                    contentPadding = contentPadding,
                    selection = selection,
                    onClick = onClickManga,
                    onClickRemoteTrack = onClickRemoteTrack,
                    onLongClick = onLongClickManga,
                    onClickContinueReading = onClickContinueReading,
                    searchQuery = searchQuery,
                    onGlobalSearchClicked = onGlobalSearchClicked,
                )

@JohanKRS
Copy link
Copy Markdown
Author

JohanKRS commented Mar 6, 2026

Addressed the current review feedback in 3bce32d.

Updated items:

  • rewrote AniList and MyAnimeList getNotInLibraryEntries() pagination to iterative loops instead of recursive accumulation
  • removed Android string lookup from the State.displayedCategories derived helper by resolving the label in LibraryScreenModel and passing it through state
  • documented and range-checked trackerCoverId(), and added regression coverage for both the packed id layout and synthetic category creation
  • extracted the duplicated pending-track binding logic used by MangaScreenModel and BrowseSourceScreenModel into a shared helper
  • fixed the LibraryPager indentation nit from the inline comment

Verification rerun after the changes:

  • :app:testDebugUnitTest --tests 'eu.kanade.domain.track.interactor.AddTracksTest' --tests 'eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModelTest' --tests 'eu.kanade.tachiyomi.data.track.TrackerRemoteEntryMappingTest' --tests 'eu.kanade.tachiyomi.ui.library.RemoteTrackerLibraryTest'
  • :app:compileDebugKotlin

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.

"Not in Library" tab when grouping by Tracker Status on the library view

1 participant