fix(flutter): cover URLs resolve against server baseUrl + audio stream URL guard

Cover-art Image.network calls were passing server-relative URLs
(/api/albums/<id>/cover) straight to NetworkImage, which interprets
"no scheme" as file:/// and crashes with "No host specified in URI".
Same root cause regardless of HTTPS or HTTP server.

ServerImage wraps Image.network with a Riverpod read of
serverUrlProvider and prefixes the configured base URL. Absolute URLs
(e.g. discover screen's Lidarr image_urls) pass through unchanged.

Three call sites updated: album_card, artist_card, playlists_list_screen.
discover_screen left as-is — its row.imageUrl is already absolute (Lidarr
returns full URLs from MusicBrainz / Spotify metadata) and it has a
meaningful errorBuilder that ServerImage doesn't expose.

Also adds a defensive check in audio_handler.setQueueFromTracks: if
the constructed stream URL ends up scheme-less, throw a StateError
naming baseUrl + track.streamUrl + track.id instead of letting it
fall through to ExoPlayer which surfaces a confusing "Cleartext HTTP
traffic to 127.0.0.1 not permitted" error (Android's URL parser
defaults a scheme-less URI to localhost). User reported this exact
confusing error against an HTTPS prod server; the better message
will pinpoint where the empty baseUrl comes from on next reproduction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 22:47:25 -04:00
parent 751c2878f7
commit 602ef3bfdf
5 changed files with 79 additions and 9 deletions
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../models/album.dart';
import '../../shared/widgets/server_image.dart';
import '../../theme/theme_extension.dart';
class AlbumCard extends StatelessWidget {
@@ -27,7 +28,7 @@ class AlbumCard extends StatelessWidget {
color: fs.slate,
child: album.coverUrl.isEmpty
? SvgPicture.asset('assets/svg/album-fallback.svg', fit: BoxFit.cover)
: Image.network(album.coverUrl, fit: BoxFit.cover),
: ServerImage(url: album.coverUrl, fit: BoxFit.cover),
),
),
const SizedBox(height: 8),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../models/artist.dart';
import '../../shared/widgets/server_image.dart';
import '../../theme/theme_extension.dart';
class ArtistCard extends StatelessWidget {
@@ -26,7 +27,7 @@ class ArtistCard extends StatelessWidget {
color: fs.slate,
child: artist.coverUrl.isEmpty
? SvgPicture.asset('assets/svg/album-fallback.svg', fit: BoxFit.cover)
: Image.network(artist.coverUrl, fit: BoxFit.cover),
: ServerImage(url: artist.coverUrl, fit: BoxFit.cover),
),
),
const SizedBox(height: 8),