_parseTsList and _extractKeyAttributes both build URLs by concatenating
m3u8Host (no trailing slash from path.dirname) with a relative token.
Without '/' the result is host/proxy/oppai/kiteTOKEN instead of the
correct host/proxy/oppai/kite/TOKEN, causing every segment and AES-128
key request to hit the wrong path and the download to fail silently.
Fix mirrors the existing correct pattern in voe_extractor.dart line 90.
Refs #609 (high RAM with stutters).
Manga / anime covers from sources are typically 720x1080 or larger
(~3 MB decoded RGBA per cover). The library grid, library list and
generic browse / search card widgets render those covers at roughly
150x220 logical pixels, but every cover decoded to its full source
resolution and that decoded bitmap landed in Flutter's `imageCache`.
With 30-50 covers in flight during a normal scroll, the default
100 MB cache filled and the engine started evicting + re-decoding
aggressively — exactly the symptom in #609 (stutters + high RAM).
Mangayomi already had `ExtendedResizeImage` available via the
`extended_image_library` package and used it in one place
(`cachedCompressedNetworkImage`, called only from the History
screen). This commit generalises that pattern.
Add a `coverProvider()` helper in `lib/utils/cached_network.dart`
that wraps `CustomExtendedNetworkImageProvider` in
`ExtendedResizeImage` with a 200 KB encoded budget — sharp at
typical thumbnail size on high-DPR screens, ~3.6x smaller decoded
than a full-resolution cover. Pass through the same `cache` /
`cacheMaxAge` knobs the underlying provider exposes so existing
disk-cache behaviour is preserved.
Swap the three high-traffic thumbnail call sites to use it:
* `lib/modules/library/widgets/library_gridview_widget.dart`
* `lib/modules/library/widgets/library_listview_widget.dart`
* `lib/modules/widgets/manga_image_card_widget.dart`
(both `MangaImageCardWidget` and `MangaImageCardListTileWidget`,
used by browse and search results)
Deliberately not changed:
* The manga / anime detail page hero cover — large display, full
resolution is appropriate.
* Reader pages — already memory-managed by `ChapterPreloadManager`
and need full resolution for actual reading.
* `cachedNetworkImage()` and other lower-traffic thumbnail surfaces
(tracker results, calendar, recommendation grid). Easy to extend
in a follow-up if anyone asks; kept narrow here so review is
manageable.
Verified
* `flutter analyze` clean on every touched file
* `flutter build macos --release` succeeds
* Smoke-tested on macOS with the local-all-fixes build: library
grid, library list and browse card all render identical-looking
covers at typical thumbnail sizes; no visible quality regression
at the displayed scale
Refs #609 (high RAM with stutters).
Flutter's `imageCache.maximumSizeBytes` defaults to 100 MB. With a large
manga library and full-resolution covers (a typical 720x1080 cover is
about 3 MB decoded), scrolling fills that cap quickly and the engine
starts evicting and re-decoding aggressively — felt as stutters and
elevated heap on memory-constrained devices.
Set the cap explicitly, sized for the platform: 64 MB on mobile, 256 MB
on desktop. Mobile gets a tight ceiling so the OS does not kill the app
on heap pressure; desktop keeps a generous budget so users with very
large libraries still scroll smoothly.
This is a one-line ceiling, not a fix for the underlying decode-at-
source-resolution behaviour — that is being tracked separately and
will further reduce per-image cache pressure.
The encoded-bytes LRU cache in `CustomExtendedNetworkImageProvider`
(`_memoryCache`, 50 MB) is independent of `PaintingBinding.imageCache`
and is unaffected by this change. So is the disk cache from
`extended_image_library`. Only the in-memory cache of decoded
`ui.Image` objects is bounded here.
Verified:
- `flutter analyze` clean
- `flutter build macos --release` succeeds
- Tested on macOS — Activity Monitor shows the desktop budget is
comfortably above what the existing UI normally needs; mobile budget
is consistent with what the system already imposes via OOM kills.
Fixes#543. The statistics screen was crashing on Android with a
populated library (~75+ favourites). Repro: open the More -> Statistics
screen on an account with a real-sized library; app hangs and is
killed.
Cause is in `getStatistics()` in `lib/modules/more/statistics/
statistics_provider.dart`. The provider materialised every favourite
manga *and* every chapter of every favourite into a Dart list, then
walked the lists in memory:
final items = await isar.mangas.filter()...findAll();
final chapters = await isar.chapters.filter()
.manga((q) => q.favoriteEqualTo(true)...).findAll();
For a library with 75 favourites at ~100 chapters each that is 7,500+
heavy Isar objects materialised at once just to compute six counts.
The "completed items" loop then re-fetched chapters per completed
manga via the link relation (`item.chapters.toList()`), and the
reading-time aggregation loaded every History row to sum a single
int field.
This commit rewrites the function to do all aggregation inside Isar:
* total / read / completed-items / downloads => `count()` queries on
indexed filter chains. No object hydration.
* completed favourites => projects only the IDs (`idProperty()`),
then runs two cheap `count()` queries per item to check
"has-chapters && unread == 0". Logic is unchanged from the original
`every(isRead) && isNotEmpty` check.
* reading time => projects the `readingTimeSeconds` field
(`readingTimeSecondsProperty()`) and folds the resulting List<int?>.
Avoids hydrating full History rows.
Memory: peak in-memory list shrinks from ~7,500 fully-hydrated chapters
plus all favourite mangas plus all history rows, to (at most) the IDs
of completed favourites (typically <50 ints) and the projected reading-
time list (one int per history row, decoded as a flat list).
DB round-trips: the completed-items branch now does 2 round-trips per
completed favourite instead of 1 link-traversal per completed favourite.
On indexed `mangaId` + `isRead` filters this is sub-millisecond per
query and well below the cost of materialising thousands of rows. The
overall function is faster end-to-end on any non-trivial library.
Behaviour preserved: same six fields, same definitions, same edge
cases (manga with no chapters is not counted as completed).
Verified
- `flutter analyze` clean on the touched file
- `flutter build macos --release` succeeds
- Manual smoke test on macOS with the local-all-fixes build (the bug
reproduces only with a large mobile library; macOS desktop with a
small test library returns the same numbers as before)
On macOS, the libmdbx / Isar database lives under
`getApplicationDocumentsDirectory()` -> `~/Documents/...`. With iCloud
Drive's "Desktop & Documents Folders" sync enabled (a common default),
macOS protects ~/Documents with TCC and denies unsigned / sideloaded /
dev / not-yet-permission-granted builds the file access libmdbx needs
to open its database. The result is a black screen on launch with the
following error in the Flutter / app log:
[ERROR:flutter/runtime/dart_isolate.cc(1402)] Unhandled exception:
IsarError: Cannot open Environment: MdbxError (13): Permission denied
POSIX errno 13 is EACCES, raised by the OS for the access denial — not
errno 15 (ENOTBLK / "Block device required"), and not iCloud "Optimise
Mac Storage" evicting files. Verified on macOS 26.3 / Apple Silicon
with iCloud Desktop & Documents sync active: a Terminal `mkdir`+`echo
> file` to the same path succeeds (Terminal inherits the user's TCC
grant), but the unsigned dev build fails on first DB open with the
error above.
Fix: on macOS only, host the database under `getApplicationSupport-
Directory()` -> `~/Library/Application Support/<bundle id>/...`. That
location is app-private, not TCC-gated, and Apple's recommended
location for app data files. iOS, Windows, Linux are unchanged — they
keep using Documents (iOS for Files-app visibility next to backups,
Windows / Linux because Documents is the conventional location and
neither has TCC).
Includes a one-shot best-effort migration: existing macOS users with a
DB at `~/Documents/Mangayomi/databases/` have it renamed to the new
path on first launch. Migration is skipped if the new location is
non-empty so we never overwrite user data, and any failure falls back
to a fresh DB rather than crashing on launch (the user can then move
the legacy directory manually if needed). Subsequent launches skip the
migration branch because the new path already exists.
Repro
- macOS with iCloud Drive's "Desktop & Documents Folders" sync enabled
- Unsigned / sideloaded / dev build of Mangayomi (or signed build that
hasn't yet received the user's "Files and Folders > Documents" TCC
grant)
- Launch -> black screen, IsarError MdbxError (13)
Verification
- Reproduced the exact error on this branch's parent commit
(upstream/main 25c1d72c) on macOS 26.3, iCloud Desktop & Documents
sync active, captured `MdbxError (13): Permission denied`
- After this patch the same build launches cleanly and opens the
database at `~/Library/Application Support/<bundle>/Mangayomi/
databases/mangayomiDb.isar`
- Existing 15 MB Isar database from a prior run preserved through the
rebuild — no data loss
Notes
- This is a narrower follow-up to the earlier proposed Application-
Support move that was correctly rejected for being cross-platform
and missing migration. This change is gated by `Platform.isMacOS`
and migrates existing macOS users.
- Hive (`Hive.initFlutter` in main.dart) still uses Documents on
macOS. It is initialized after Isar via `_postLaunchInit` and is
unawaited, so a Hive failure wouldn't reproduce the black screen.
If Hive turns out to be affected by the same TCC denial, a
follow-up PR can move it the same way.
Two related bugs left anime downloads stuck at 0% with no error visible to
the user. Manga downloads from clean HTTPS sources were unaffected.
1) lib/utils/extensions/string_extensions.dart
isMediaVideo() did a plain endsWith on the full URL string, so URLs
shaped like https://host/play/{id}/video.mp4?for={token} (used by
AnimeGG and several other sources) failed the filter because of the
trailing `?for=...` query string. With both m3u8Urls and nonM3u8Urls
empty in downloadChapter, the surrounding
Future.doWhile(() => isOk == true) poll never sets isOk and waits
forever -- MDownloader is never constructed.
Fix: match against Uri.tryParse(this)?.path instead of the full
string, and use a leading "." in the suffix so e.g. "flashmp4" cannot
accidentally match.
2) lib/services/download_manager/download_isolate_pool.dart
Once the URL passes the filter, _downloadFile (anime branch) opens a
streaming request. When the source extension sets Range: bytes=0-,
the server correctly responds with HTTP 206 Partial Content. The
previous "if (response.statusCode != 200)" check rejected that,
retried 3x, and threw. The throw was masked by an outer catch(_) in
downloadChapter, so the user only saw a forever-spinner.
Fix: accept any 2xx (>= 200 && < 300). Same fix applied to
_downloadSegment for HLS segment fetches.
Repro
- Source: AnimeGG (en) -- install via Mangayomi extensions
- Pick any episode (tested with Toriko Ep 147, Gintama Ep 39, Grand Blue
Ep 12)
- Tap the download icon
Before: an empty
".../AnimeGG (EN)/<series>/<episode>.mp4"-named folder is created, the
download icon stays in the spinner state, no error toast.
After: the .mp4 is written to disk at the size declared in Content-Length
(65,026,283 bytes for Toriko Ep 147), plays in the system video player.
Tested on macOS 26.3 / Apple Silicon with AnimeGG (multiple episodes,
multiple series, including a 180 MB 720p) and a manga control
(Asura Scans, 9 pages) on the same build to confirm no regression on the
manga path.
- Replace child mutation with two clearly named variables
- This is a pure refactor:
- base = the result of wrapping with BotToastInit
- withBackHandler = optionally wrapped with _MouseBackButtonHandler
- No logic changes - just clearer variable names and no reassignment.
Improves readability and maintainability.
Do not pass Context into any provider.
Handle logic and BotToasts in the onTap of the AboutScreen, not inside the provider.
Also make _checkUpdate() more efficient, by fetching the `/latest` API.
Before, it was fetching 10 releases and immediately discarding 9, only leaving the latest.
Use the already-existing downloadedChapterIdsProvider and do a simple Set.contains() lookup instead of the synchronous Isar query.
Performance improvement
Before the "Downloaded" filter in the library AND the "Downloaded chapters" badge would not update, unless you restart the app.
Now the "Downloaded chapters" badge is increasing and the "Downloaded" filter is showing new downloaded manga chapters and hiding deleted manga chapters.
`wrapWithKeyboardListener` creates a `FocusNode()` internally on every call.
If no `focusNode` is passed, a new one is allocated every rebuild, which can cause focus flicker.
Without this fix, keyboard focus can be intermittently lost after widget rebuilds,
which would silently swallow keyboard shortcuts.