Add tracker Not in Library tab for MAL and AniList#1557
Add tracker Not in Library tab for MAL and AniList#1557JohanKRS wants to merge 5 commits intokomikku-app:masterfrom
Conversation
Reviewer's GuideImplements 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-librarysequenceDiagram
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
Class diagram for remote tracker NotInLibrary supportclassDiagram
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
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Summary of ChangesHello, 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
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The recursive implementations of
AnilistApi.getNotInLibraryEntriesandMyAnimeListApi.getNotInLibraryEntriescan 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.displayedCategoriesyou callInjekt.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
trackerCoverIdbit-packing scheme usesLong.MIN_VALUEand 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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}" | ||
| } | ||
| } | ||
| } |
| LibraryList( | ||
| items = items, | ||
| contentPadding = contentPadding, | ||
| selection = selection, | ||
| onClick = onClickManga, | ||
| onClickRemoteTrack = onClickRemoteTrack, | ||
| onLongClick = onLongClickManga, | ||
| onClickContinueReading = onClickContinueReading, | ||
| searchQuery = searchQuery, | ||
| onGlobalSearchClicked = onGlobalSearchClicked, | ||
| ) |
There was a problem hiding this comment.
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,
)
|
Addressed the current review feedback in Updated items:
Verification rerun after the changes:
|
Summary
Closes #1556.
Adds an opt-in tracker-only
Not in Librarytab when the library is grouped byTracker Status, with support for MyAnimeList and AniList.What changed
Not in LibraryentriesSupports "Not in Library" trackingNot in Librarytracker-status tab that appears after the normal tracker-status groupsVerification
: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:assembleDebugNot in Librarytab and supported tracker indicatorNot trackedNot in Libraryitem on a cold startNotes
Not in Librarytab is intentionally opt-in to avoid unexpected tracker list fetchesSummary 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:
Bug Fixes:
Enhancements:
Tests: