import 'dart:io'; import 'package:flutter/material.dart'; import '../theme/tokens.dart'; /// Cover art that renders from EITHER a local file (`file://` URI) or a network /// URL, picking [FileImage] vs [NetworkImage] purely by the URI scheme. This is /// the single place all art-rendering call sites go through, so a downloaded /// track's art shows offline (the caller hands us the already-built URI string; /// resolving download-vs-network happens upstream in `providers.dart`). /// /// Behaviour preserved from the old scattered `Image.network(...)` call sites: /// * `fit: BoxFit.cover`, `gaplessPlayback: true`, and a `ValueKey(uri)` so /// switching tracks keeps the previous frame until the new art decodes (no /// flash) and rebuilds cleanly. /// * a surface-filled [placeholder] with a muted album icon for the null, /// loading-error, and missing-file cases (via `errorBuilder`). /// /// Sizing is never hardcoded — [width]/[height]/[fit] are respected as passed. /// Optional [borderRadius] clips the art with a [ClipRRect]; callers that pass /// it should drop their own outer `ClipRRect` so we don't double-clip. class ArtImage extends StatelessWidget { const ArtImage( this.uri, { super.key, this.fit = BoxFit.cover, this.width, this.height, this.placeholder, this.borderRadius, }); /// The already-built art URI. A `file://` URI loads from disk; anything else /// (http/https) loads over the network. `null` → [placeholder]. final String? uri; final BoxFit fit; final double? width; final double? height; /// Shown for null/error/missing art. Defaults to [_ArtPlaceholder]. final Widget? placeholder; /// If set, the art (and placeholder) are clipped to these rounded corners. final BorderRadius? borderRadius; @override Widget build(BuildContext context) { final fallback = placeholder ?? const _ArtPlaceholder(); Widget child; if (uri == null) { child = fallback; } else { final parsed = Uri.tryParse(uri!); final ImageProvider provider = (parsed != null && parsed.scheme == 'file') ? FileImage(File(parsed.toFilePath())) : NetworkImage(uri!); child = Image( image: provider, key: ValueKey(uri), fit: fit, gaplessPlayback: true, errorBuilder: (_, _, _) => fallback, ); } // Back the art with the surface fill so transparent/loading gaps read as // a panel rather than the bare canvas (matches the old ColoredBox wrap). child = ColoredBox(color: TimbreColors.surface, child: child); if (width != null || height != null) { child = SizedBox(width: width, height: height, child: child); } if (borderRadius != null) { child = ClipRRect(borderRadius: borderRadius!, child: child); } return child; } } /// The default art placeholder: a muted album glyph on the surface fill. Used /// for null art and as the loading/error fallback. class _ArtPlaceholder extends StatelessWidget { const _ArtPlaceholder(); @override Widget build(BuildContext context) => ColoredBox( color: TimbreColors.surface, child: Center( child: Icon(Icons.album_outlined, color: TimbreColors.dimmed), ), ); }