diff --git a/flutter_client/lib/player/player_bar.dart b/flutter_client/lib/player/player_bar.dart index 6785d296..674cb6fb 100644 --- a/flutter_client/lib/player/player_bar.dart +++ b/flutter_client/lib/player/player_bar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -85,7 +87,12 @@ class _TrackInfoColumn extends StatelessWidget { children: [ if (media.artUri != null) Image( - image: NetworkImage(media.artUri.toString()), + // artUri is a file:// path written by AlbumCoverCache — + // FileImage is the right loader; NetworkImage would + // attempt HTTP on a file:// URI and fail. + image: media.artUri!.isScheme('file') + ? FileImage(File.fromUri(media.artUri!)) as ImageProvider + : NetworkImage(media.artUri.toString()), width: 48, height: 48, errorBuilder: (_, __, ___) => diff --git a/flutter_client/lib/shared/widgets/server_image.dart b/flutter_client/lib/shared/widgets/server_image.dart index 2b8ca417..be44538e 100644 --- a/flutter_client/lib/shared/widgets/server_image.dart +++ b/flutter_client/lib/shared/widgets/server_image.dart @@ -35,11 +35,31 @@ class ServerImage extends ConsumerWidget { // doesn't carry cookies/Bearer tokens automatically the way the // browser's tag does, so we forward the session token as a // header. Without this, the server returns 401 and the image fails. - final token = ref.watch(sessionTokenProvider).value; - final headers = (token != null && token.isNotEmpty) - ? {'Authorization': 'Bearer $token'} - : null; - return Image.network(resolved, fit: fit, headers: headers); + // + // sessionTokenProvider is a FutureProvider — on first read after a + // hot restart its .value is null until the secure-storage read + // resolves. If we fired Image.network with headers:null at that + // moment, the network image cache would lock in the 401 response + // and never retry. Instead, hold the fallback until the token is + // available, then mount the Image with the auth header attached. + final tokenAsync = ref.watch(sessionTokenProvider); + return tokenAsync.when( + loading: () => empty, + error: (_, __) => empty, + data: (token) { + final headers = (token != null && token.isNotEmpty) + ? {'Authorization': 'Bearer $token'} + : null; + return Image.network( + resolved, + fit: fit, + headers: headers, + // Keep failures local — a single 401/timeout shouldn't dump + // a stack trace or replace the parent Container's background. + errorBuilder: (_, __, ___) => empty, + ); + }, + ); } static String? _resolve(String? baseUrl, String url) {