refactor(image-to-string): use asset placeholders instead of dicebear URIs

This commit is contained in:
Kingkor Roy Tirtho 2022-09-08 23:55:48 +06:00
parent 531fae64f9
commit daa62c73f7
23 changed files with 115 additions and 48 deletions

View File

@ -21,7 +21,10 @@ class AlbumCard extends HookConsumerWidget {
final int marginH = final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
return PlaybuttonCard( return PlaybuttonCard(
imageUrl: TypeConversionUtils.image_X_UrlString(album.images), imageUrl: TypeConversionUtils.image_X_UrlString(
album.images,
placeholder: ImagePlaceholder.collection,
),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: isPlaylistPlaying && playback.isPlaying, isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading && isLoading: playback.status == PlaybackStatus.loading &&

View File

@ -27,7 +27,10 @@ class AlbumView extends HookConsumerWidget {
tracks: tracks, tracks: tracks,
id: album.id!, id: album.id!,
name: album.name!, name: album.name!,
thumbnail: TypeConversionUtils.image_X_UrlString(album.images), thumbnail: TypeConversionUtils.image_X_UrlString(
album.images,
placeholder: ImagePlaceholder.collection,
),
), ),
tracks.indexWhere((s) => s.id == currentTrack?.id), tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
@ -50,7 +53,10 @@ class AlbumView extends HookConsumerWidget {
ref.watch(albumIsSavedForCurrentUserQuery(album.id!)); ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
final albumArt = useMemoized( final albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString(album.images), () => TypeConversionUtils.image_X_UrlString(
album.images,
placeholder: ImagePlaceholder.albumArt,
),
[album.images]); [album.images]);
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();

View File

@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends StatelessWidget { class ArtistCard extends StatelessWidget {
final Artist artist; final Artist artist;
@ -11,11 +13,11 @@ class ArtistCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backgroundImage = CachedNetworkImageProvider((artist final backgroundImage =
.images?.isNotEmpty ?? UniversalImage.imageProvider(TypeConversionUtils.image_X_UrlString(
false) artist.images,
? artist.images!.first.url! placeholder: ImagePlaceholder.artist,
: "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"); ));
return SizedBox( return SizedBox(
height: 240, height: 240,
width: 200, width: 200,

View File

@ -1,4 +1,3 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -10,6 +9,7 @@ import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/components/Shared/TrackTile.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
@ -78,8 +78,11 @@ class ArtistProfile extends HookConsumerWidget {
const SizedBox(width: 50), const SizedBox(width: 50),
CircleAvatar( CircleAvatar(
radius: avatarWidth, radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider( backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(data.images), TypeConversionUtils.image_X_UrlString(
data.images,
placeholder: ImagePlaceholder.artist,
),
), ),
), ),
Padding( Padding(
@ -193,7 +196,9 @@ class ArtistProfile extends HookConsumerWidget {
id: data.id!, id: data.id!,
name: "${data.name!} To Tracks", name: "${data.name!} To Tracks",
thumbnail: TypeConversionUtils.image_X_UrlString( thumbnail: TypeConversionUtils.image_X_UrlString(
data.images), data.images,
placeholder: ImagePlaceholder.artist,
),
), ),
tracks.indexWhere((s) => s.id == currentTrack?.id), tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
@ -233,10 +238,10 @@ class ArtistProfile extends HookConsumerWidget {
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl = String? thumbnailUrl =
TypeConversionUtils.image_X_UrlString( TypeConversionUtils.image_X_UrlString(
track.value.album?.images, track.value.album?.images,
index: index: (track.value.album?.images?.length ?? 1) - 1,
(track.value.album?.images?.length ?? 1) - placeholder: ImagePlaceholder.albumArt,
1); );
return TrackTile( return TrackTile(
playback, playback,
duration: duration, duration: duration,

View File

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/models/sideBarTiles.dart';
@ -135,8 +136,10 @@ class Sidebar extends HookConsumerWidget {
final data = meSnapshot.asData?.value; final data = meSnapshot.asData?.value;
final avatarImg = TypeConversionUtils.image_X_UrlString( final avatarImg = TypeConversionUtils.image_X_UrlString(
data?.images, data?.images,
index: (data?.images?.length ?? 1) - 1); index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
if (extended.value) { if (extended.value) {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -155,7 +158,8 @@ class Sidebar extends HookConsumerWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundImage: backgroundImage:
CachedNetworkImageProvider(avatarImg), UniversalImage.imageProvider(
avatarImg),
onBackgroundImageError: onBackgroundImageError:
(exception, stackTrace) => (exception, stackTrace) =>
Image.asset( Image.asset(
@ -193,7 +197,7 @@ class Sidebar extends HookConsumerWidget {
onTap: () => goToSettings(context), onTap: () => goToSettings(context),
child: CircleAvatar( child: CircleAvatar(
backgroundImage: backgroundImage:
CachedNetworkImageProvider(avatarImg), UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) => onBackgroundImageError: (exception, stackTrace) =>
Image.asset( Image.asset(
"assets/user-placeholder.png", "assets/user-placeholder.png",

View File

@ -3,6 +3,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -53,11 +54,12 @@ class UserDownloads extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.symmetric(horizontal: 5),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage( child: UniversalImage(
height: 40, height: 40,
width: 40, width: 40,
imageUrl: TypeConversionUtils.image_X_UrlString( path: TypeConversionUtils.image_X_UrlString(
track.album?.images, track.album?.images,
placeholder: ImagePlaceholder.albumArt,
), ),
), ),
), ),

View File

@ -108,7 +108,10 @@ class UserLocalTracks extends HookConsumerWidget {
tracks: tracks, tracks: tracks,
id: "local", id: "local",
name: "Local Tracks", name: "Local Tracks",
thumbnail: TypeConversionUtils.image_X_UrlString(null), thumbnail: TypeConversionUtils.image_X_UrlString(
null,
placeholder: ImagePlaceholder.collection,
),
isLocal: true, isLocal: true,
), ),
tracks.indexWhere((s) => s.id == currentTrack?.id), tracks.indexWhere((s) => s.id == currentTrack?.id),

View File

@ -112,6 +112,7 @@ class SyncedLyrics extends HookConsumerWidget {
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images, playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1, index: (playback.track?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
), ),
[playback.track?.album?.images], [playback.track?.album?.images],
); );

View File

@ -25,6 +25,7 @@ class Player extends HookConsumerWidget {
? TypeConversionUtils.image_X_UrlString( ? TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images, playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1, index: (playback.track?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
) )
: "assets/album-placeholder.png", : "assets/album-placeholder.png",
[playback.track?.album?.images], [playback.track?.album?.images],

View File

@ -113,6 +113,7 @@ class PlayerQueue extends HookConsumerWidget {
duration: duration, duration: duration,
thumbnailUrl: TypeConversionUtils.image_X_UrlString( thumbnailUrl: TypeConversionUtils.image_X_UrlString(
track.value.album?.images, track.value.album?.images,
placeholder: ImagePlaceholder.albumArt,
), ),
isActive: playback.track?.id == track.value.id, isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async { onTrackPlayButtonPressed: (currentTrack) async {

View File

@ -42,6 +42,7 @@ class PlayerView extends HookConsumerWidget {
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(
currentTrack?.album?.images, currentTrack?.album?.images,
index: (currentTrack?.album?.images?.length ?? 1) - 1, index: (currentTrack?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
), ),
[currentTrack?.album?.images], [currentTrack?.album?.images],
); );

View File

@ -23,7 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
return PlaybuttonCard( return PlaybuttonCard(
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
title: playlist.name!, title: playlist.name!,
imageUrl: TypeConversionUtils.image_X_UrlString(playlist.images), imageUrl: TypeConversionUtils.image_X_UrlString(
playlist.images,
placeholder: ImagePlaceholder.collection,
),
isPlaying: isPlaylistPlaying && playback.isPlaying, isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying, isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying,
onTap: () { onTap: () {
@ -56,7 +59,10 @@ class PlaylistCard extends HookConsumerWidget {
tracks: tracks, tracks: tracks,
id: playlist.id!, id: playlist.id!,
name: playlist.name!, name: playlist.name!,
thumbnail: TypeConversionUtils.image_X_UrlString(playlist.images), thumbnail: TypeConversionUtils.image_X_UrlString(
playlist.images,
placeholder: ImagePlaceholder.collection,
),
), ),
); );
}, },

View File

@ -33,7 +33,10 @@ class PlaylistView extends HookConsumerWidget {
tracks: tracks, tracks: tracks,
id: playlist.id!, id: playlist.id!,
name: playlist.name!, name: playlist.name!,
thumbnail: TypeConversionUtils.image_X_UrlString(playlist.images), thumbnail: TypeConversionUtils.image_X_UrlString(
playlist.images,
placeholder: ImagePlaceholder.collection,
),
), ),
tracks.indexWhere((s) => s.id == currentTrack?.id), tracks.indexWhere((s) => s.id == currentTrack?.id),
); );
@ -58,7 +61,10 @@ class PlaylistView extends HookConsumerWidget {
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!)); final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
final titleImage = useMemoized( final titleImage = useMemoized(
() => TypeConversionUtils.image_X_UrlString(playlist.images), () => TypeConversionUtils.image_X_UrlString(
playlist.images,
placeholder: ImagePlaceholder.collection,
),
[playlist.images]); [playlist.images]);
final color = usePaletteGenerator( final color = usePaletteGenerator(

View File

@ -110,7 +110,9 @@ class Search extends HookConsumerWidget {
duration: duration, duration: duration,
thumbnailUrl: thumbnailUrl:
TypeConversionUtils.image_X_UrlString( TypeConversionUtils.image_X_UrlString(
track.value.album?.images), track.value.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
isActive: playback.track?.id == track.value.id, isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async { onTrackPlayButtonPressed: (currentTrack) async {
var isPlaylistPlaying = var isPlaylistPlaying =
@ -126,6 +128,8 @@ class Search extends HookConsumerWidget {
thumbnail: TypeConversionUtils thumbnail: TypeConversionUtils
.image_X_UrlString( .image_X_UrlString(
currentTrack.album?.images, currentTrack.album?.images,
placeholder:
ImagePlaceholder.albumArt,
), ),
), ),
); );

View File

@ -1,5 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
class DownloadConfirmationDialog extends StatelessWidget { class DownloadConfirmationDialog extends StatelessWidget {
const DownloadConfirmationDialog({Key? key}) : super(key: key); const DownloadConfirmationDialog({Key? key}) : super(key: key);
@ -9,11 +9,11 @@ class DownloadConfirmationDialog extends StatelessWidget {
return AlertDialog( return AlertDialog(
contentPadding: const EdgeInsets.all(15), contentPadding: const EdgeInsets.all(15),
title: Row( title: Row(
children: [ children: const [
const Text("Are you sure?"), Text("Are you sure?"),
const SizedBox(width: 10), SizedBox(width: 10),
CachedNetworkImage( UniversalImage(
imageUrl: path:
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif", "https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
height: 40, height: 40,
width: 40, width: 40,

View File

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
class PlaybuttonCard extends StatelessWidget { class PlaybuttonCard extends StatelessWidget {
final void Function()? onTap; final void Function()? onTap;
@ -55,8 +56,8 @@ class PlaybuttonCard extends StatelessWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage( child: UniversalImage(
imageUrl: imageUrl, path: imageUrl,
placeholder: (context, url) => placeholder: (context, url) =>
Image.asset("assets/placeholder.png"), Image.asset("assets/placeholder.png"),
), ),

View File

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
@ -175,9 +176,7 @@ class TrackCollectionView extends HookConsumerWidget {
const BoxConstraints(maxHeight: 200), const BoxConstraints(maxHeight: 200),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: CachedNetworkImage( child: UniversalImage(path: titleImage),
imageUrl: titleImage,
),
), ),
), ),
Column( Column(

View File

@ -149,6 +149,7 @@ class TracksTableView extends HookConsumerWidget {
String? thumbnailUrl = TypeConversionUtils.image_X_UrlString( String? thumbnailUrl = TypeConversionUtils.image_X_UrlString(
track.value.album?.images, track.value.album?.images,
index: (track.value.album?.images?.length ?? 1) - 1, index: (track.value.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
); );
String duration = String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";

View File

@ -98,12 +98,9 @@ class Downloader with ChangeNotifier {
); );
final imageUri = TypeConversionUtils.image_X_UrlString( final imageUri = TypeConversionUtils.image_X_UrlString(
track.album?.images ?? [], track.album?.images ?? [],
placeholder: ImagePlaceholder.online,
); );
final response = await get( final response = await get(Uri.parse(imageUri));
Uri.parse(
imageUri,
),
);
await MetadataGod.writeMetadata( await MetadataGod.writeMetadata(
file, file,

View File

@ -210,6 +210,7 @@ class Playback extends PersistedChangeNotifier {
artUri: Uri.parse( artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString( TypeConversionUtils.image_X_UrlString(
track.album?.images, track.album?.images,
placeholder: ImagePlaceholder.online,
), ),
), ),
duration: track.ytTrack.duration, duration: track.ytTrack.duration,

View File

@ -133,7 +133,10 @@ final currentUserQuery = FutureProvider<User>(
Image() Image()
..height = 50 ..height = 50
..width = 50 ..width = 50
..url = TypeConversionUtils.image_X_UrlString(me.images), ..url = TypeConversionUtils.image_X_UrlString(
me.images,
placeholder: ImagePlaceholder.artist,
),
]; ];
} }
return me; return me;

View File

@ -298,7 +298,9 @@ class _MprisMediaPlayer2Player extends DBusObject {
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds), "mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
"mpris:artUrl": DBusString( "mpris:artUrl": DBusString(
TypeConversionUtils.image_X_UrlString( TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images), playback.track?.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
), ),
"xesam:album": DBusString(playback.track!.album!.name!), "xesam:album": DBusString(playback.track!.album!.name!),
"xesam:artist": DBusArray.string( "xesam:artist": DBusArray.string(

View File

@ -11,11 +11,29 @@ import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
enum ImagePlaceholder {
albumArt,
artist,
collection,
online,
}
abstract class TypeConversionUtils { abstract class TypeConversionUtils {
static String image_X_UrlString(List<Image>? images, {int index = 0}) { static String image_X_UrlString(
List<Image>? images, {
int index = 0,
required ImagePlaceholder placeholder,
}) {
final String placeholderUrl = {
ImagePlaceholder.albumArt: "assets/album-placeholder.png",
ImagePlaceholder.artist: "assets/user-placeholder.png",
ImagePlaceholder.collection: "assets/placeholder.png",
ImagePlaceholder.online:
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
}[placeholder]!;
return images != null && images.isNotEmpty return images != null && images.isNotEmpty
? images[0].url! ? images[0].url!
: "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png"; : placeholderUrl;
} }
static String artists_X_String<T extends ArtistSimple>(List<T> artists) { static String artists_X_String<T extends ArtistSimple>(List<T> artists) {