83df3773ae
Two issues from on-device testing against prod HTTPS: 1. ServerImage was resolving cover URLs correctly (https://minstrel.fabledsword.com/api/albums/.../cover) but the server returned 401 because Image.network doesn't carry the session token automatically the way the browser sends cookies. Forwards the stored session token as an Authorization: Bearer header. No-op when no token is present. 2. The "Cleartext HTTP traffic to 127.0.0.1" audio error reproduced even after the previous defensive check landed, which means the URL handed to ExoPlayer has a valid scheme+host (just the wrong host). The check only catches scheme-less URLs, so it didn't fire. Added debugPrint logging at configure() and setQueueFromTracks() time to show the actual baseUrl + per-track resolved URL on the next reproduction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
2.0 KiB
Dart
56 lines
2.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../auth/auth_provider.dart';
|
|
|
|
/// Image.network wrapper that resolves server-relative URLs (e.g.
|
|
/// `/api/albums/<id>/cover`) against the configured server base URL
|
|
/// from [serverUrlProvider]. Absolute URLs (with scheme) pass through
|
|
/// unchanged. Empty/null base or empty URL render the [fallback]
|
|
/// (defaults to a transparent SizedBox so callers can supply their own
|
|
/// surrounding placeholder).
|
|
///
|
|
/// Mirrors the absolute-or-relative logic the audio handler uses for
|
|
/// stream URLs, so cover art and audio resolve URLs the same way.
|
|
class ServerImage extends ConsumerWidget {
|
|
const ServerImage({
|
|
super.key,
|
|
required this.url,
|
|
this.fit,
|
|
this.fallback,
|
|
});
|
|
|
|
final String url;
|
|
final BoxFit? fit;
|
|
final Widget? fallback;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final empty = fallback ?? const SizedBox.shrink();
|
|
if (url.isEmpty) return empty;
|
|
final base = ref.watch(serverUrlProvider).value;
|
|
final resolved = _resolve(base, url);
|
|
if (resolved == null) return empty;
|
|
// Cover endpoints are gated by RequireUser server-side. Image.network
|
|
// doesn't carry cookies/Bearer tokens automatically the way the
|
|
// browser's <img> 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);
|
|
}
|
|
|
|
static String? _resolve(String? baseUrl, String url) {
|
|
final parsed = Uri.tryParse(url);
|
|
if (parsed != null && parsed.hasScheme) return url;
|
|
if (baseUrl == null || baseUrl.isEmpty) return null;
|
|
final base = baseUrl.endsWith('/')
|
|
? baseUrl.substring(0, baseUrl.length - 1)
|
|
: baseUrl;
|
|
final path = url.startsWith('/') ? url : '/$url';
|
|
return '$base$path';
|
|
}
|
|
}
|