import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../models/music.dart'; import '../models/support.dart'; import '../services/cache_service.dart'; import '../state/music.dart'; import '../state/settings.dart'; import '../state/theme.dart'; part 'images.g.dart'; @riverpod CacheInfo _artistArtCacheInfo( _ArtistArtCacheInfoRef ref, { required String artistId, bool thumbnail = true, }) { final cache = ref.watch(cacheServiceProvider); return cache.artistArtCacheInfo(artistId, thumbnail: thumbnail); } @riverpod FutureOr _artistArtCachedUrl( _ArtistArtCachedUrlRef ref, { required String artistId, bool thumbnail = true, }) async { final cache = ref.watch(_artistArtCacheInfoProvider( artistId: artistId, thumbnail: thumbnail, )); final file = await cache.cacheManager.getFileFromCache(cache.cacheKey); return file?.originalUrl; } @riverpod FutureOr _artistArtUriCacheInfo( _ArtistArtUriCacheInfoRef ref, { required String artistId, bool thumbnail = true, }) async { final cache = ref.watch(cacheServiceProvider); final info = ref.watch(_artistArtCacheInfoProvider( artistId: artistId, thumbnail: thumbnail, )); final cachedUrl = await ref.watch(_artistArtCachedUrlProvider( artistId: artistId, thumbnail: thumbnail, ).future); final offline = ref.watch(offlineModeProvider); // already cached, don't try to get the real url again if (cachedUrl != null) { return UriCacheInfo( uri: Uri.parse(cachedUrl), cacheKey: info.cacheKey, cacheManager: info.cacheManager, ); } if (offline) { final file = await cache.imageCache.getFileFromCache(info.cacheKey); if (file != null) { return UriCacheInfo( uri: Uri.parse(file.originalUrl), cacheKey: info.cacheKey, cacheManager: info.cacheManager, ); } else { return cache.placeholder(thumbnail: thumbnail); } } // assume the url is good or let this fail return UriCacheInfo( uri: (await cache.artistArtUri(artistId, thumbnail: thumbnail))!, cacheKey: info.cacheKey, cacheManager: info.cacheManager, ); } class ArtistArtImage extends HookConsumerWidget { final String artistId; final bool thumbnail; final BoxFit fit; final PlaceholderStyle placeholderStyle; final double? height; final double? width; const ArtistArtImage({ super.key, required this.artistId, this.thumbnail = true, this.fit = BoxFit.cover, this.placeholderStyle = PlaceholderStyle.color, this.height, this.width, }); @override Widget build(BuildContext context, WidgetRef ref) { final cache = ref.watch(_artistArtUriCacheInfoProvider( artistId: artistId, thumbnail: thumbnail, )); // TODO: figure out how to animate this without messing up with boxfit/ratio return cache.when( data: (data) => UriCacheInfoImage( cache: data, fit: fit, placeholderStyle: placeholderStyle, height: height, width: width, ), error: (_, __) => Container( color: Colors.red, height: height, width: width, ), loading: () => Container( color: Theme.of(context).colorScheme.secondaryContainer, height: height, width: width, ), ); } } class SongAlbumArt extends HookConsumerWidget { final Song song; final bool square; const SongAlbumArt({ super.key, required this.song, this.square = true, }); @override Widget build(BuildContext context, WidgetRef ref) { final album = ref.watch(albumProvider(song.albumId!)).valueOrNull; return AnimatedSwitcher( duration: const Duration(milliseconds: 150), child: album != null ? AlbumArt(album: album) : const PlaceholderImage(), ); } } class AlbumArt extends HookConsumerWidget { final Album album; final bool square; const AlbumArt({ super.key, required this.album, this.square = true, }); @override Widget build(BuildContext context, WidgetRef ref) { // generate the palette used in other views ahead of time ref.watch(albumArtPaletteProvider(album.id)); final cache = ref.watch(cacheServiceProvider); Widget image = UriCacheInfoImage(cache: cache.albumArt(album)); if (square) { image = AspectRatio(aspectRatio: 1.0, child: image); } return CardClip(child: image); } } class CircleClip extends StatelessWidget { final Widget child; const CircleClip({ super.key, required this.child, }); @override Widget build(BuildContext context) { return ClipOval( clipBehavior: Clip.antiAlias, child: AspectRatio( aspectRatio: 1.0, child: child, ), ); } } class CardClip extends StatelessWidget { final Widget child; final bool square; const CardClip({ super.key, required this.child, this.square = true, }); @override Widget build(BuildContext context) { final cardShape = Theme.of(context).cardTheme.shape; return ClipRRect( borderRadius: cardShape is RoundedRectangleBorder ? cardShape.borderRadius : null, child: !square ? child : AspectRatio( aspectRatio: 1.0, child: child, ), ); } } enum PlaceholderStyle { color, spinner, } class UriCacheInfoImage extends StatelessWidget { final UriCacheInfo cache; final BoxFit fit; final PlaceholderStyle placeholderStyle; final double? height; final double? width; const UriCacheInfoImage({ super.key, required this.cache, this.fit = BoxFit.cover, this.placeholderStyle = PlaceholderStyle.color, this.height, this.width, }); @override Widget build(BuildContext context) { return CachedNetworkImage( imageUrl: cache.uri.toString(), cacheKey: cache.cacheKey, cacheManager: cache.cacheManager, fit: fit, height: height, width: width, fadeInDuration: const Duration(milliseconds: 300), fadeOutDuration: const Duration(milliseconds: 500), placeholder: (context, url) => placeholderStyle == PlaceholderStyle.spinner ? Container() : Container( color: Theme.of(context).colorScheme.secondaryContainer, ), errorWidget: (context, url, error) => PlaceholderImage( fit: fit, height: height, width: width, ), ); } } class PlaceholderImage extends HookConsumerWidget { final BoxFit fit; final double? height; final double? width; final bool thumbnail; const PlaceholderImage({ super.key, this.fit = BoxFit.cover, this.height, this.width, this.thumbnail = true, }); @override Widget build(BuildContext context, WidgetRef ref) { return Image.asset( thumbnail ? 'assets/placeholder_thumb.png' : 'assets/placeholder.png', fit: fit, height: height, width: width, ); } } class _ExpandedRatio extends StatelessWidget { final Widget child; final double aspectRatio; const _ExpandedRatio({ required this.child, this.aspectRatio = 1.0, }); @override Widget build(BuildContext context) { return Expanded( child: AspectRatio( aspectRatio: aspectRatio, child: child, ), ); } } class MultiImage extends HookConsumerWidget { final Iterable cacheInfo; const MultiImage({ super.key, required this.cacheInfo, }); @override Widget build(BuildContext context, WidgetRef ref) { final images = cacheInfo.map((cache) => UriCacheInfoImage(cache: cache)); final row1 = []; final row2 = []; if (images.length >= 4) { row1.addAll([ _ExpandedRatio(child: images.elementAt(0)), _ExpandedRatio(child: images.elementAt(1)), ]); row2.addAll([ _ExpandedRatio(child: images.elementAt(2)), _ExpandedRatio(child: images.elementAt(3)), ]); } if (images.length == 3) { row1.addAll([ _ExpandedRatio(child: images.elementAt(0)), _ExpandedRatio(child: images.elementAt(1)), ]); row2.addAll([ _ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(2)), ]); } if (images.length == 2) { row1.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(0))); row2.add(_ExpandedRatio(aspectRatio: 2.0, child: images.elementAt(1))); } if (images.length == 1) { row1.addAll([_ExpandedRatio(child: images.elementAt(0))]); } return Column( children: [ Row(children: row1), Row(children: row2), ], ); } }