602ef3bfdf
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>
46 lines
1.4 KiB
Dart
46 lines
1.4 KiB
Dart
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 {
|
|
const ArtistCard({required this.artist, required this.onTap, super.key});
|
|
final ArtistRef artist;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: SizedBox(
|
|
width: 140,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
ClipOval(
|
|
child: Container(
|
|
width: 124,
|
|
height: 124,
|
|
color: fs.slate,
|
|
child: artist.coverUrl.isEmpty
|
|
? SvgPicture.asset('assets/svg/album-fallback.svg', fit: BoxFit.cover)
|
|
: ServerImage(url: artist.coverUrl, fit: BoxFit.cover),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
artist.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(color: fs.parchment, fontSize: 14),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|