# Browse: Sources & Extensions ## Purpose The "Browse" tab: discovering/installing extensions, listing sources, browsing a single source (popular/latest/search with filters), and global search across all sources. ## Structure `browse_screen.dart` is a two-tab shell (Sources / Extensions) with a shared AppBar. Sources-tab search pushes `GlobalSearchRoute`; Extensions-tab search filters in place via `extensionQueryProvider`. | Area | Key files | |---|---| | Extensions | `presentation/extension/{extension_screen, controller/extension_controller}.dart`, `widgets/extension_list_tile.dart`, `data/extension_repository/extension_repository.dart` | | Sources | `presentation/source/{source_screen, controller/source_controller}.dart`, `widgets/source_list_tile.dart`, `data/source_repository/source_repository.dart` | | Source manga list | `presentation/source_manga_list/{source_manga_list_screen, controller/...}.dart`, `widgets/{source_manga_grid_view, source_manga_list_view, source_manga_filter, filter_to_widget}.dart` | | Global search | `presentation/global_search/{global_search_screen, controller/source_quick_search_controller}.dart`, `widgets/source_short_search.dart` | | Source preferences | `presentation/source_preference/...` | ## Domain types All `typedef`s over GraphQL codegen (except `Language`, which is `freezed`): `Extension = Fragment$ExtensionDto`, `SourceDto = Fragment$SourceDto`, `SourceType = Enum$FetchSourceMangaType` (POPULAR/LATEST/SEARCH/$unknown), `MangaPage = Fragment$SourceMangaPage`. Filters: `Filter`/`PrimitiveFilter` union + 7 concrete subtypes; `SourcePreference` union + 5 types. ## Notable behavior - **Extensions:** grouped into update/installed/by-language; NSFW-gated; language allowlist persisted (`DBKeys.extensionLanguageFilter`). Install via `UpdateExtension` or file upload (`InstallExternalExtension`). - **Sources:** grouped lastUsed/all/by-language/localSource; tapping saves `sourceLastUsedProvider` and navigates to `SourceTypeRoute(POPULAR)`. - **Hide sources:** the Sources filter screen (`SourceFilterRoute`, tune action) toggles per-source visibility. Hidden state persists as the `tsumiru_isHidden` source meta key (sibling to pinning's `webUI_isPinned`), so it syncs across devices. `browsableSourceListProvider` strips hidden sources from browse/global-search/migration; the filter screen reads pre-hide `allSourcesByLanguageProvider` so hidden sources stay listable. - **Source browsing:** `infinite_scroll_pagination` `PagingController` (`firstPageKey: 1`); POPULAR/LATEST/SEARCH chips; filter drawer (end-drawer tablet / bottom-sheet phone) rendered via exhaustive `switch` in `filter_to_widget.dart`. - **Applied browse filters:** `appliedSourceFilterProvider(sourceId)` (keep-alive, deliberately not persisted) holds the applied `List` for the app session, matching WebUI's per-source session storage. The sheet is seeded from it on every open (`SourceMangaFilter.appliedChanges` + a `useEffect`, since the end-drawer instance can keep its hook state between opens), so submitting builds on the current selection rather than replacing it. Reset clears the provider and refreshes the paging controller. - **Global search:** `quickSearchResultsProvider(query)` fans out `sourceQuickSearchMangaListProvider(sourceId, query)` across filtered sources, all wrapped in `rateLimitQueueProvider(query)`. - **Bulk add duplicate detection:** `source_manga_display_view.dart` runs `splitSelectionForDuplicates` (library domain's shared matcher, checking both the library and earlier picks in the same selection) before adding. Clean picks batch-add immediately; flagged picks resolve one at a time through the Komikku-style dialog queue (Add anyway / Allow all / Skip it / Skip all / Show entry / Cancel) — cancel stops remaining prompts, entries already added stay added. The closing snackbar's counts come from server-confirmed ids, not the requested selection. Single-add (`addMangaToLibraryWithCategory`) gates on the same matcher and, on a hit, can migrate the existing entry into the new one via the bulk migration runner instead of adding a duplicate. ## Gotchas - **Extension list fetch is a mutation** (`FetchExtensionList`) — every "refresh" tells the server to re-check for updates (a roundtrip). - **`SourceType.SEARCH` is labelled "Filter"** in the UI (`sourceTypeFilter`), matching Tachiyomi convention. - **`sourceDisplayModeProvider` uses `DisplayMode.sourceDisplayList`** (grid/list subset); `descriptiveList` is lumped with `list`. - **Filter leaf widgets use `kPositionPlaceholder`** — real position is assigned by the parent; don't set it in the leaf. - **`currentChanges` falls back to `const []`, not `[]`** — it is a `useEffect` key in `FilterGroupWidget`, and a fresh list every build would re-fire the effect on every build. - **Global search rate-limits by query string** (a queue per distinct query), not by time. - **`baseSourcePreferenceListProvider` uses `ref.read`** (not reactive) — invalidate to refresh. - **Source language filter sorts enabled-first once** on dialog open (frozen for the session). - **Local source `lang` is the literal `"localsourcelang"`** — explicitly removed and pinned to the bottom. - **`MangaPageDto` fragment is dead code** (repo uses `SourceMangaPage`). - **Installing an extension auto-adds its language** to the source language filter so the new source is visible.