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
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.
by extracting the helper methods `_seekTo()` and `_seekBy()`.
This also fixes a potential bug, where in line 1634 it was calculated
`skipDuration - _currentPosition.value.inSeconds` instead of the other way around.
That doesn't make sense. If currentPosition = 120 and skipDuration = 10, this becomes:
`_tempPosition = Duration(seconds: 10 - 120)`; so `= Duration(seconds: -110)`
A negative duration makes no sense as a UI indicator of a seek target.
By extracting:
```
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
```
to a file `system_ui.dart` and calling the method `restoreSystemUI()`
Changes:
- Add season-keyword regex (staffel, season, saison, temporada) and
episode-keyword regex (folge, episode, ep.) to reliably extract
the correct number regardless of name format
- parseChapterNumber() now encodes season context into the sort key
(season * 100000 + episode) so multi-season anime sort correctly
across seasons without mixing episode numbers
- Add parseEpisodeNumber() which strips season context and returns
only the episode number within a season; use this for tracker
updates (MAL/AniList/Kitsu) and AniSkip lookups, where the tracker
entry is already season-specific
- Switch updateTrackChapterRead and getAniSkipResults to
parseEpisodeNumber to fix incorrect episode reporting for
multi-season anime
- Compile all RegExp objects as static finals instead of per-call
instantiation
- Refactor duplicated parse logic into a single private _parse()
method with an applySeason flag
When this setting is turned on, the app will automatically mark chapters as read if they have the same chapter number as a chapter you've already read.
This is useful for sources where:
- multiple scanlators upload the same chapter number
- duplicate entries appear due to different naming formats
- a series has repeated or alternative releases of the same chapter
With the setting enabled, you won’t have to manually mark each duplicate as read.
- Add ChapterRecognition for numeric chapter parsing
- Replace old comparators with chapter‑number–aware sorting
- Unify sort modes and simplify list handling
- Rewrite updateMangaDetail with URL‑based dedupe
- Preserve read state across scanlators
- Update existing chapters instead of recreating
- Only create Update entries for new unread chapters
- Recompute smartUpdateDays using combined chapter list
- Remove outdated reversed/index‑based logic
`chapters.length - chapters.reversed.toList().indexOf(chapters.reversed.toList()[finalIndex]) - 1;`
is just the same as
`chapters.length - 1 - finalIndex`
but it creates two lists, which is wasteful.
- Replace synchronous Isar writes with async transactions in `AboutScreen`
- Make `AppLogger.init()` and `dispose()` fully async and guarded with `_busy`
- Remove `StreamController` queue and write directly to the IOSink
- Prevent double‑initialization and race conditions using `_initialized` + `_busy`
- Update log toggle handler to await logger init/dispose for consistency
Fix following Exception when disabling and re-enabling logger:
```
Exception has occurred.
StateError (Bad state: Stream has already been listened to.)
```
The horizontal layout (_buildHorizontalLayout) used FittedBox as a
shared scaffold for both vertical and horizontal reading modes.
FittedBox passes unbounded width constraints to its child, which caused
RenderFlex to throw "children have non-zero flex but incoming width
constraints are unbounded" when Row + Expanded children were used.
This cascaded into a chain of "RenderBox was not laid out" assertions
all the way up through IntrinsicHeight, ConstrainedBox, FittedBox,
LayoutBuilder, and into the ScrollablePositionedList in
image_view_webtoon.dart.
Fix: split the two layout paths before they reach a scaffold widget.
The vertical path keeps FittedBox (safe, maxWidth is capped at 480px).
The horizontal path uses SizedBox(width: MediaQuery screenWidth) so
Row/Expanded always receive a finite width constraint.