feat(flutter): cover thumbnails in library grid and detail view #35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/flutter-cover-thumbnails"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Flutter client adapted to consume the new cover image + HATEOAS link data from PR #34 (merged backend).
Changes
1. Models updated (
doujin_models.dart)DoujinSummary: addedcoverImageId(nullable GUID) +links(Map<String, HypermediaLink>)DoujinDetail: addedcoverImageId(nullable GUID).freezed.dart/.g.dartfiles viabuild_runner2. New
CoverThumbnailwidget (cover_thumbnail.dart)/api/thumbnails/{id}withAuthorization: BearerheaderNoCoverPlaceholderwidget for doujins without a cover3. Library page (
library_page.dart)_DoujinCardnow shows a cover thumbnail on the left side whenlinks['thumbnail']exists30p,2v) to save horizontal space for the thumbnailStoreConnectorupdated to extractserverUrl+authTokenfrom settings state4. Detail page (
detail_page.dart)coverImageIdfromDoujinDetail)CoverThumbnailwidget withBorderRadius.circular(8)StoreConnectorupdated to pass server settings throughHow it works
The backend's per-item HATEOAS links (from PR #34) include a
thumbnaillink when a cover image exists:The Flutter client reads
links['thumbnail'].href, prepends the server URL, and loads the image with the auth header. The detail page constructs the URL directly fromcoverImageId.All 180 Flutter tests pass.
flutter analyze— 0 issues.1. Models: DoujinSummary now has coverImageId + links (self, thumbnail). DoujinDetail has coverImageId. Regenerated freezed/json files. 2. CoverThumbnail widget: loads thumbnails from /api/thumbnails/{id} with bearer token auth header, loading spinner, error fallback. 3. Library page: _DoujinCard now shows a thumbnail on the left side when a cover image exists (via links['thumbnail'].href), with a placeholder when no cover. Card meta labels shortened (30p, 2v) to make room for the thumbnail. 4. Detail page: metadata panel shows a larger cover image at the top (using coverImageId from DoujinDetail) before the titles section. 5. StoreConnector updated to pass serverUrl + authToken from settings state to the widgets that need to build image URLs. All 180 Flutter tests pass.Flutter Coverage
Total: 76.6% (1264 of 1650)
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Cover thumbnails! The library is finally going to look alive~ ♪ I do love seeing knowledge rendered visually. The way you threaded
serverUrl+authTokenfrom the settingsStoreConnectorstraight into the card and detail body is clean — no globals, no leakage, just honest prop-drilling. TheImage.networkwith theAuthorizationheader is the right call for a single-user desktop client, and the null-coalescing fallbacks (settings.serverUrl ?? '') mean a misconfigured server can never throw a null-deref into your widget tree. Gorgeously defensive. Fufu~Verdict: ✅ Looks good to me~
No blockers. No security concerns, no logic errors. Static scan clean. The bearer-token pattern is standard for image-fetching behind an authenticated API, and nothing sensitive leaks past the process boundary.
💡 Little ideas (non-blocking)~
cover_thumbnail.dart— 0% dedicated test coverage (CI shows 29.0%, 9/31 lines). This is the single biggest chunk of new hand-written logic in the PR, and none of its three states (loading spinner,errorBuilderbroken-image fallback,NoCoverPlaceholder) are exercised anywhere. Thelibrary_page_test.dartchange only updates string assertions ('30 pages'→'30p'); it doesn't even render aCoverThumbnailbecause the test fixture has nolinks['thumbnail']. A smalltestWidgetspumpingCoverThumbnailagainst a mockHttpOverrides(or just asserting the placeholder/error builder renders) would catch regressions in the auth-header wiring and the error path. You wouldn't want a future refactor to silently drop thatAuthorizationheader, would you? ♡detail_page.dart:176-184vslibrary_page.dart:225— two URL-building conventions. The library card faithfully consumes the HATEOAS link ('$serverUrl$thumbnailUrl'fromlinks['thumbnail'].href), which is exactly the spirit of thelinksfield you just added toDoujinSummary. But the detail panel hardcodes the shape:'$serverUrl/api/thumbnails/${doujin.coverImageId}'. If the backend ever relocates or version-prefixes the thumbnails route (which is what HATEOAS exists to prevent!), the detail page breaks while the library keeps working.DoujinDetailcarriescoverImageIdbut nolinksmap — consider asking the backend (PR #34's follow-up) to also emit athumbnaillink on the detail envelope, then both pages can use the samelinks['thumbnail']lookup and you delete the hardcoded path entirely. Fufu~ consistency is a virtue~Trailing-slash hazard on
serverUrl(both pages). SinceImage.networkbypasses Dio, there's nobaseUrlnormalization. If a user configuresserverUrl = 'https://host/'(with trailing slash), the library buildshttps://host//api/thumbnails/...(double slash — usually tolerated, occasionally 404s depending on the proxy) and the detail page the same. A tiny_normalizeBaseUrl()helper that strips a trailing/before interpolation would make this bulletproof. Minor, but it's the kind of thing that bites exactly one user six months from now~ ♪Automated review by Jibril · 2026-06-30
CI/CD: passed (forgejo-actions Flutter coverage #582, 76.6% line 1264/1650, current for head
1a41298, posted 20:01:05 +02:00 ~59s after head commit) · Local checks: skipped per CI-evidence policyStatic scan: clean (Dart only; no secrets, shell injection, eval/exec, pickle, or SQL injection in added lines)
I would like for the kiosk to have smaller borders between the items in the grid. Remember, it is supposed to be dense. Further more, I would like to see some slightly more data. I have a spec sheet:
Overall structure
A Container (card) with:
Background fill (use theme/design token)
Thin border (use theme/design token)
Slightly rounded corners
Padding on all sides
Content laid out vertically in a Column
Text: [sola] some works style — bracketed tag + title
Centered horizontally
Font: serif, bold
Sits directly above the image, small gap below
Image sized to fit its container, portrait orientation
Border around image, matching outer card border style
Overlay tooltip: use a Stack with the image as the base layer, and a Positioned widget near the top-right area of the image containing:
A semi-transparent rounded rectangle bubble
Text inside, same title string, single line, with padding
A small triangular "pointer" tail attached to the bottom-left corner of the bubble, pointing down toward a spot on the image — implement with a CustomPainter (simple triangle path) or ClipPath
This behaves like a tooltip/speech-bubble anchored over the artwork
Row of 5 star icons, centered
Partial fill pattern (some filled, some outlined/empty) to represent a rating value
A small circular icon/button immediately after the stars (caret-in-circle or info/expand toggle) — likely tappable
Small vertical gap above this row
Row with MainAxisAlignment.spaceBetween (or similar split layout)
Left: date/time string, e.g. 2026-06-30 15:29
Right: page count, e.g. 353 pages, followed by a tiny circular icon (info/help style, outlined)
Both pieces use a smaller, secondary-style font compared to the title
Suggested widget breakdown
Card (Container)
└─ Column
├─ Text (title)
├─ Stack
│ ├─ Image (bordered, sized to fit)
│ └─ Positioned (tooltip bubble: Container + CustomPaint tail)
├─ Row (star rating + info icon)
└─ Row (date ←→ pages + icon)
Please look at this and consider how you could implement that
Pull request closed