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.
Each UChapDataPreload holds a GetChapterPagesModel that itself holds all pageUrls, archiveImages, etc. For a 50-page chapter this creates 50 model objects each referencing all 50 URLs.
MangasListState previously stored selected manga IDs as List<int>.
Every visible library card called .contains() on that list once per
rebuild to determine its highlight state, making each check O(n) in
the number of selected items. The provider's own update/selectAll/
selectSome methods also used .contains() and .remove() on a List.
Change the state type to Set<int> throughout, making all membership
checks O(1). Updated all consumers: library_gridview_widget,
library_listview_widget, library_app_bar, library_dialogs, and
MangasSetIsReadState.
Problem
- Genz Updates could still return 403/Cloudflare in extension requests even when the same site worked in WebView.
- The extension HTTP path primarily used cached cookies from settings instead of reliably using the current WebView cookie jar.
- Cloudflare challenge detection was too narrow (mostly English markers) and could misclassify localized challenge pages as resolved.
Solution
- Prefer live WebView cookies for outgoing extension HTTP requests, with settings cookies as fallback.
- Sync cookie + user-agent from embedded WebView on onLoadStop in addition to history updates.
- Expand challenge detection with additional multilingual and platform markers.
Implementation Details
- Updated `MCookieManager.interceptRequest` to read cookies from `CookieManager.getCookies()` and use them as the request `Cookie` header when available.
- Added `_syncCookieAndUaFromWebView()` in `MangaWebView` and invoked it from both `onLoadStop` and `onUpdateVisitedHistory` to persist fresh clearance cookies immediately.
- Introduced a centralized `_cloudflareChallengePattern` in `m_client.dart` and reused it in `_containsCloudflareChallengeHtml` and `_isCloudflareChallengePage` for consistent challenge detection.
- prevent stale settings overwrites by reloading settings inside Isar write txn before updating chapterPageUrlsList
- guard getPageLength() against missing chapter entries and empty urls to avoid No element crashes
- simplify read-threshold calculation in setPageIndex() via totalPages/pagesRemaining for continuous and paged modes
- map visible continuous double-page indices to actual page indices in _readProgressListener before persisting progress
- snapshot item positions and clamp indices during fast scrolling to avoid volatile first/last access races
This commit improves memory management, reduces redundant interpreter
instantiation, and standardizes service usage patterns.
- Add `dispose()` to `ExtensionService` interface and implement it across
Dart, JS, LNReader, and Mihon services.
- Replace repeated interpreter creation in `DartExtensionService` with a
persistent `_interpreter` instance initialized once in the constructor.
- Add disposal logic for JS DOM selector and Cheerio instances to prevent
memory leaks.
- Introduce `withExtensionService()` helper to ensure services are always
disposed after use.
- Update call sites across the codebase to use `withExtensionService()`
or manual try/finally disposal.
- Improve isolate service message handling by extracting `responsePort`
earlier.
- Ensure safer defaults (e.g., returning empty lists, const lists) when
service calls fail.
- Add StreamSubscription to manage ReceivePort listener lifecycle
- Introduce handshake timeout when waiting for isolate SendPort to prevent hangs
- Add response timeout in get<T>() to avoid indefinitely waiting for isolate replies
- Replace direct ReceivePort.listen with tracked subscription for safer cleanup
- Improve error handling for invalid or missing isolate responses
- Strengthen isolate startup reliability and shutdown consistency
- Use the logger to log failed updates
- After the update-botToast another botToast is being spawned to show exactly which manga(s) couldn't be updated.
- Remove the doWhile loop because it is unnecessary. The condition of mangaList.length == numbers is always true, meaning it only runs once.