diff --git a/lib/app/lists/albums_grid.dart b/lib/app/lists/albums_grid.dart index b7abe73..16b8903 100644 --- a/lib/app/lists/albums_grid.dart +++ b/lib/app/lists/albums_grid.dart @@ -10,7 +10,7 @@ import '../state/database.dart'; import '../state/settings.dart'; import 'list_items.dart'; -const kPageSize = 30; +const kPageSize = 60; class AlbumsGrid extends HookConsumerWidget { const AlbumsGrid({super.key}); diff --git a/lib/app/lists/artists_list.dart b/lib/app/lists/artists_list.dart new file mode 100644 index 0000000..a5a5385 --- /dev/null +++ b/lib/app/lists/artists_list.dart @@ -0,0 +1,81 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +import '../../sources/models.dart'; +import '../hooks/use_paging_controller.dart'; +import '../state/database.dart'; +import '../state/settings.dart'; +import 'list_items.dart'; + +const kPageSize = 30; + +typedef _ArtistItem = ({Artist artist, int? albumCount}); + +class ArtistsList extends HookConsumerWidget { + const ArtistsList({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final db = ref.watch(databaseProvider); + final sourceId = ref.watch(sourceIdProvider); + + final controller = usePagingController( + getNextPageKey: (state) => + state.lastPageIsEmpty ? null : state.nextIntPageKey, + fetchPage: (pageKey) async { + final albumCount = db.albums.id.count(); + + final query = + db.artists.select().join([ + leftOuterJoin( + db.albums, + db.albums.artistId.equalsExp(db.artists.id), + ), + ]) + ..addColumns([albumCount]) + ..where( + db.artists.sourceId.equals(sourceId) & + db.albums.sourceId.equals(sourceId), + ) + ..groupBy([db.artists.sourceId, db.artists.id]) + ..orderBy([OrderingTerm.asc(db.artists.name)]) + ..limit(kPageSize, offset: (pageKey - 1) * kPageSize); + + return (await query.get()) + .map( + (row) => ( + artist: row.readTable(db.artists), + albumCount: row.read(albumCount), + ), + ) + .toList(); + }, + ); + + return PagingListener( + controller: controller, + builder: (context, state, fetchNextPage) { + return PagedSliverList( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate<_ArtistItem>( + itemBuilder: (context, item, index) { + final (:artist, :albumCount) = item; + + return ArtistListTile( + artist: artist, + albumCount: albumCount, + onTap: () async { + context.push('/artist/${artist.id}'); + }, + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/app/lists/list_items.dart b/lib/app/lists/list_items.dart index ae91741..80339c8 100644 --- a/lib/app/lists/list_items.dart +++ b/lib/app/lists/list_items.dart @@ -1,4 +1,3 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,19 +32,28 @@ class AlbumGridTile extends HookConsumerWidget { } class ArtistListTile extends StatelessWidget { - const ArtistListTile({super.key}); + const ArtistListTile({ + super.key, + required this.artist, + this.albumCount, + this.onTap, + }); + + final Artist artist; + final int? albumCount; + final void Function()? onTap; @override Widget build(BuildContext context) { return ListTile( leading: CircleClip( - child: CachedNetworkImage( - imageUrl: 'https://placehold.net/400x400.png', - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => Icon(Icons.error), - ), + child: artist.coverArt != null + ? CoverArtImage(coverArt: artist.coverArt) + : CachedImage(artist.smallImage), ), - title: Text('Some Artist'), + title: Text(artist.name), + subtitle: albumCount != null ? Text('$albumCount albums') : null, + onTap: onTap, ); } } diff --git a/lib/app/screens/library_screen.dart b/lib/app/screens/library_screen.dart index 173c627..a1d139e 100644 --- a/lib/app/screens/library_screen.dart +++ b/lib/app/screens/library_screen.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; -import '../lists/albums_grid.dart'; +import '../lists/artists_list.dart'; import '../state/services.dart'; import '../util/custom_scroll_fix.dart'; @@ -210,7 +210,7 @@ class _NewWidgetState extends State ), SliverPadding( padding: const EdgeInsets.all(8.0), - sliver: AlbumsGrid(), + sliver: ArtistsList(), ), ], ); diff --git a/lib/database/database.dart b/lib/database/database.dart index 2f9695b..fbca038 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -437,6 +437,9 @@ extension ArtistToDb on models.Artist { id: id, name: name, starred: Value(starred), + coverArt: Value(coverArt), + smallImage: Value(smallImage), + largeImage: Value(largeImage), ); } diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index 0f18018..3ff00b8 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -708,8 +708,45 @@ class Artists extends Table with TableInfo { requiredDuringInsert: false, $customConstraints: '', ); + static const VerificationMeta _coverArtMeta = const VerificationMeta( + 'coverArt', + ); + late final GeneratedColumn coverArt = GeneratedColumn( + 'cover_art', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ); + late final GeneratedColumnWithTypeConverter smallImage = + GeneratedColumn( + 'small_image', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ).withConverter(Artists.$convertersmallImagen); + late final GeneratedColumnWithTypeConverter largeImage = + GeneratedColumn( + 'large_image', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: '', + ).withConverter(Artists.$converterlargeImagen); @override - List get $columns => [sourceId, id, name, starred]; + List get $columns => [ + sourceId, + id, + name, + starred, + coverArt, + smallImage, + largeImage, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -749,6 +786,12 @@ class Artists extends Table with TableInfo { starred.isAcceptableOrUnknown(data['starred']!, _starredMeta), ); } + if (data.containsKey('cover_art')) { + context.handle( + _coverArtMeta, + coverArt.isAcceptableOrUnknown(data['cover_art']!, _coverArtMeta), + ); + } return context; } @@ -770,6 +813,22 @@ class Artists extends Table with TableInfo { DriftSqlType.dateTime, data['${effectivePrefix}starred'], ), + coverArt: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cover_art'], + ), + smallImage: Artists.$convertersmallImagen.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}small_image'], + ), + ), + largeImage: Artists.$converterlargeImagen.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}large_image'], + ), + ), ); } @@ -778,6 +837,12 @@ class Artists extends Table with TableInfo { return Artists(attachedDatabase, alias); } + static TypeConverter $convertersmallImage = const UriConverter(); + static TypeConverter $convertersmallImagen = + NullAwareTypeConverter.wrap($convertersmallImage); + static TypeConverter $converterlargeImage = const UriConverter(); + static TypeConverter $converterlargeImagen = + NullAwareTypeConverter.wrap($converterlargeImage); @override List get customConstraints => const [ 'PRIMARY KEY(source_id, id)', @@ -792,12 +857,18 @@ class ArtistsCompanion extends UpdateCompanion { final Value id; final Value name; final Value starred; + final Value coverArt; + final Value smallImage; + final Value largeImage; final Value rowid; const ArtistsCompanion({ this.sourceId = const Value.absent(), this.id = const Value.absent(), this.name = const Value.absent(), this.starred = const Value.absent(), + this.coverArt = const Value.absent(), + this.smallImage = const Value.absent(), + this.largeImage = const Value.absent(), this.rowid = const Value.absent(), }); ArtistsCompanion.insert({ @@ -805,6 +876,9 @@ class ArtistsCompanion extends UpdateCompanion { required String id, required String name, this.starred = const Value.absent(), + this.coverArt = const Value.absent(), + this.smallImage = const Value.absent(), + this.largeImage = const Value.absent(), this.rowid = const Value.absent(), }) : sourceId = Value(sourceId), id = Value(id), @@ -814,6 +888,9 @@ class ArtistsCompanion extends UpdateCompanion { Expression? id, Expression? name, Expression? starred, + Expression? coverArt, + Expression? smallImage, + Expression? largeImage, Expression? rowid, }) { return RawValuesInsertable({ @@ -821,6 +898,9 @@ class ArtistsCompanion extends UpdateCompanion { if (id != null) 'id': id, if (name != null) 'name': name, if (starred != null) 'starred': starred, + if (coverArt != null) 'cover_art': coverArt, + if (smallImage != null) 'small_image': smallImage, + if (largeImage != null) 'large_image': largeImage, if (rowid != null) 'rowid': rowid, }); } @@ -830,6 +910,9 @@ class ArtistsCompanion extends UpdateCompanion { Value? id, Value? name, Value? starred, + Value? coverArt, + Value? smallImage, + Value? largeImage, Value? rowid, }) { return ArtistsCompanion( @@ -837,6 +920,9 @@ class ArtistsCompanion extends UpdateCompanion { id: id ?? this.id, name: name ?? this.name, starred: starred ?? this.starred, + coverArt: coverArt ?? this.coverArt, + smallImage: smallImage ?? this.smallImage, + largeImage: largeImage ?? this.largeImage, rowid: rowid ?? this.rowid, ); } @@ -856,6 +942,19 @@ class ArtistsCompanion extends UpdateCompanion { if (starred.present) { map['starred'] = Variable(starred.value); } + if (coverArt.present) { + map['cover_art'] = Variable(coverArt.value); + } + if (smallImage.present) { + map['small_image'] = Variable( + Artists.$convertersmallImagen.toSql(smallImage.value), + ); + } + if (largeImage.present) { + map['large_image'] = Variable( + Artists.$converterlargeImagen.toSql(largeImage.value), + ); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -869,6 +968,9 @@ class ArtistsCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('name: $name, ') ..write('starred: $starred, ') + ..write('coverArt: $coverArt, ') + ..write('smallImage: $smallImage, ') + ..write('largeImage: $largeImage, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2864,6 +2966,9 @@ typedef $ArtistsCreateCompanionBuilder = required String id, required String name, Value starred, + Value coverArt, + Value smallImage, + Value largeImage, Value rowid, }); typedef $ArtistsUpdateCompanionBuilder = @@ -2872,6 +2977,9 @@ typedef $ArtistsUpdateCompanionBuilder = Value id, Value name, Value starred, + Value coverArt, + Value smallImage, + Value largeImage, Value rowid, }); @@ -2902,6 +3010,23 @@ class $ArtistsFilterComposer extends Composer<_$SubtracksDatabase, Artists> { column: $table.starred, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get coverArt => $composableBuilder( + column: $table.coverArt, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters get smallImage => + $composableBuilder( + column: $table.smallImage, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters get largeImage => + $composableBuilder( + column: $table.largeImage, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); } class $ArtistsOrderingComposer extends Composer<_$SubtracksDatabase, Artists> { @@ -2931,6 +3056,21 @@ class $ArtistsOrderingComposer extends Composer<_$SubtracksDatabase, Artists> { column: $table.starred, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get coverArt => $composableBuilder( + column: $table.coverArt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get smallImage => $composableBuilder( + column: $table.smallImage, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get largeImage => $composableBuilder( + column: $table.largeImage, + builder: (column) => ColumnOrderings(column), + ); } class $ArtistsAnnotationComposer @@ -2953,6 +3093,21 @@ class $ArtistsAnnotationComposer GeneratedColumn get starred => $composableBuilder(column: $table.starred, builder: (column) => column); + + GeneratedColumn get coverArt => + $composableBuilder(column: $table.coverArt, builder: (column) => column); + + GeneratedColumnWithTypeConverter get smallImage => + $composableBuilder( + column: $table.smallImage, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter get largeImage => + $composableBuilder( + column: $table.largeImage, + builder: (column) => column, + ); } class $ArtistsTableManager @@ -2990,12 +3145,18 @@ class $ArtistsTableManager Value id = const Value.absent(), Value name = const Value.absent(), Value starred = const Value.absent(), + Value coverArt = const Value.absent(), + Value smallImage = const Value.absent(), + Value largeImage = const Value.absent(), Value rowid = const Value.absent(), }) => ArtistsCompanion( sourceId: sourceId, id: id, name: name, starred: starred, + coverArt: coverArt, + smallImage: smallImage, + largeImage: largeImage, rowid: rowid, ), createCompanionCallback: @@ -3004,12 +3165,18 @@ class $ArtistsTableManager required String id, required String name, Value starred = const Value.absent(), + Value coverArt = const Value.absent(), + Value smallImage = const Value.absent(), + Value largeImage = const Value.absent(), Value rowid = const Value.absent(), }) => ArtistsCompanion.insert( sourceId: sourceId, id: id, name: name, starred: starred, + coverArt: coverArt, + smallImage: smallImage, + largeImage: largeImage, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/database/tables.drift b/lib/database/tables.drift index a191a65..a661d71 100644 --- a/lib/database/tables.drift +++ b/lib/database/tables.drift @@ -65,6 +65,9 @@ CREATE TABLE artists( id TEXT NOT NULL, name TEXT NOT NULL COLLATE NOCASE, starred DATETIME, + cover_art TEXT, + small_image TEXT MAPPED BY `const UriConverter()`, + large_image TEXT MAPPED BY `const UriConverter()`, PRIMARY KEY (source_id, id), FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE ) WITH Artist; diff --git a/lib/images/images.dart b/lib/images/images.dart index ca8224f..51f7040 100644 --- a/lib/images/images.dart +++ b/lib/images/images.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:octo_image/octo_image.dart'; +import '../app/state/settings.dart'; import '../app/state/source.dart'; class CoverArtImage extends HookConsumerWidget { @@ -20,13 +21,15 @@ class CoverArtImage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final source = ref.watch(sourceProvider); + final sourceId = ref.watch(sourceIdProvider); - final imageProviderKeys = [source, coverArt, thumbnail]; + final imageProviderKeys = [source, sourceId, coverArt, thumbnail]; final buildImageProvider = useCallback( () => CachedNetworkImageProvider( coverArt != null ? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString() : 'https://placehold.net/400x400.png', + cacheKey: '$sourceId$coverArt$thumbnail', ), imageProviderKeys, ); @@ -40,11 +43,63 @@ class CoverArtImage extends HookConsumerWidget { imageProviderKeys, ); - return OctoImage( + return BaseImage( image: imageProvider.value, - placeholderBuilder: (context) => Icon(Symbols.album_rounded), + ); + } +} + +class CachedImage extends HookConsumerWidget { + const CachedImage( + this.uri, { + super.key, + }); + + final Uri? uri; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final imageProviderKeys = [uri]; + final buildImageProvider = useCallback( + () { + final imageUrl = uri?.toString() ?? 'https://placehold.net/400x400.png'; + return CachedNetworkImageProvider(imageUrl, cacheKey: imageUrl); + }, + imageProviderKeys, + ); + + final imageProvider = useState(buildImageProvider()); + useEffect( + () { + imageProvider.value = buildImageProvider(); + return; + }, + imageProviderKeys, + ); + + return BaseImage( + image: imageProvider.value, + ); + } +} + +class BaseImage extends HookConsumerWidget { + const BaseImage({ + super.key, + required this.image, + this.fit = BoxFit.cover, + }); + + final ImageProvider image; + final BoxFit fit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return OctoImage( + image: image, + placeholderBuilder: (context) => Icon(Symbols.cached_rounded), errorBuilder: (context, error, trace) => Icon(Icons.error), - fit: BoxFit.cover, + fit: fit, fadeOutDuration: Duration(milliseconds: 100), fadeInDuration: Duration(milliseconds: 200), ); diff --git a/lib/main.dart b/lib/main.dart index 66bbb05..112b77c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,9 +22,12 @@ void main() async { .insertOnConflictUpdate( SubsonicSettingsCompanion.insert( sourceId: Value(1), - address: Uri.parse('http://10.0.2.2:4533'), - username: 'admin', - password: 'password', + address: Uri.parse('http://demo.subsonic.org'), + username: 'guest1', + password: 'guest', + // address: Uri.parse('http://10.0.2.2:4533'), + // username: 'admin', + // password: 'password', useTokenAuth: Value(true), ), ); diff --git a/lib/sources/models.dart b/lib/sources/models.dart index 0174b8c..45bd88f 100644 --- a/lib/sources/models.dart +++ b/lib/sources/models.dart @@ -8,6 +8,7 @@ abstract class Artist with _$Artist { required String id, required String name, DateTime? starred, + String? coverArt, Uri? smallImage, Uri? largeImage, }) = _Artist; diff --git a/lib/sources/models.freezed.dart b/lib/sources/models.freezed.dart index 875c323..2eb6c41 100644 --- a/lib/sources/models.freezed.dart +++ b/lib/sources/models.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$Artist { - String get id; String get name; DateTime? get starred; Uri? get smallImage; Uri? get largeImage; + String get id; String get name; DateTime? get starred; String? get coverArt; Uri? get smallImage; Uri? get largeImage; /// Create a copy of Artist /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $ArtistCopyWith get copyWith => _$ArtistCopyWithImpl(this as Art @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)); } @override -int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage); +int get hashCode => Object.hash(runtimeType,id,name,starred,coverArt,smallImage,largeImage); @override String toString() { - return 'Artist(id: $id, name: $name, starred: $starred, smallImage: $smallImage, largeImage: $largeImage)'; + return 'Artist(id: $id, name: $name, starred: $starred, coverArt: $coverArt, smallImage: $smallImage, largeImage: $largeImage)'; } @@ -45,7 +45,7 @@ abstract mixin class $ArtistCopyWith<$Res> { factory $ArtistCopyWith(Artist value, $Res Function(Artist) _then) = _$ArtistCopyWithImpl; @useResult $Res call({ - String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage + String id, String name, DateTime? starred, String? coverArt, Uri? smallImage, Uri? largeImage }); @@ -62,12 +62,13 @@ class _$ArtistCopyWithImpl<$Res> /// Create a copy of Artist /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? coverArt = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable -as DateTime?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable +as DateTime?,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable +as String?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable as Uri?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable as Uri?, )); @@ -154,10 +155,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, DateTime? starred, String? coverArt, Uri? smallImage, Uri? largeImage)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Artist() when $default != null: -return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _: +return $default(_that.id,_that.name,_that.starred,_that.coverArt,_that.smallImage,_that.largeImage);case _: return orElse(); } @@ -175,10 +176,10 @@ return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeIm /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String name, DateTime? starred, String? coverArt, Uri? smallImage, Uri? largeImage) $default,) {final _that = this; switch (_that) { case _Artist(): -return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _: +return $default(_that.id,_that.name,_that.starred,_that.coverArt,_that.smallImage,_that.largeImage);case _: throw StateError('Unexpected subclass'); } @@ -195,10 +196,10 @@ return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeIm /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, DateTime? starred, String? coverArt, Uri? smallImage, Uri? largeImage)? $default,) {final _that = this; switch (_that) { case _Artist() when $default != null: -return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeImage);case _: +return $default(_that.id,_that.name,_that.starred,_that.coverArt,_that.smallImage,_that.largeImage);case _: return null; } @@ -210,12 +211,13 @@ return $default(_that.id,_that.name,_that.starred,_that.smallImage,_that.largeIm class _Artist implements Artist { - const _Artist({required this.id, required this.name, this.starred, this.smallImage, this.largeImage}); + const _Artist({required this.id, required this.name, this.starred, this.coverArt, this.smallImage, this.largeImage}); @override final String id; @override final String name; @override final DateTime? starred; +@override final String? coverArt; @override final Uri? smallImage; @override final Uri? largeImage; @@ -229,16 +231,16 @@ _$ArtistCopyWith<_Artist> get copyWith => __$ArtistCopyWithImpl<_Artist>(this, _ @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Artist&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.starred, starred) || other.starred == starred)&&(identical(other.coverArt, coverArt) || other.coverArt == coverArt)&&(identical(other.smallImage, smallImage) || other.smallImage == smallImage)&&(identical(other.largeImage, largeImage) || other.largeImage == largeImage)); } @override -int get hashCode => Object.hash(runtimeType,id,name,starred,smallImage,largeImage); +int get hashCode => Object.hash(runtimeType,id,name,starred,coverArt,smallImage,largeImage); @override String toString() { - return 'Artist(id: $id, name: $name, starred: $starred, smallImage: $smallImage, largeImage: $largeImage)'; + return 'Artist(id: $id, name: $name, starred: $starred, coverArt: $coverArt, smallImage: $smallImage, largeImage: $largeImage)'; } @@ -249,7 +251,7 @@ abstract mixin class _$ArtistCopyWith<$Res> implements $ArtistCopyWith<$Res> { factory _$ArtistCopyWith(_Artist value, $Res Function(_Artist) _then) = __$ArtistCopyWithImpl; @override @useResult $Res call({ - String id, String name, DateTime? starred, Uri? smallImage, Uri? largeImage + String id, String name, DateTime? starred, String? coverArt, Uri? smallImage, Uri? largeImage }); @@ -266,12 +268,13 @@ class __$ArtistCopyWithImpl<$Res> /// Create a copy of Artist /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? starred = freezed,Object? coverArt = freezed,Object? smallImage = freezed,Object? largeImage = freezed,}) { return _then(_Artist( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,starred: freezed == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable -as DateTime?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable +as DateTime?,coverArt: freezed == coverArt ? _self.coverArt : coverArt // ignore: cast_nullable_to_non_nullable +as String?,smallImage: freezed == smallImage ? _self.smallImage : smallImage // ignore: cast_nullable_to_non_nullable as Uri?,largeImage: freezed == largeImage ? _self.largeImage : largeImage // ignore: cast_nullable_to_non_nullable as Uri?, )); diff --git a/lib/sources/subsonic/mapping.dart b/lib/sources/subsonic/mapping.dart index d816557..1ec5de4 100644 --- a/lib/sources/subsonic/mapping.dart +++ b/lib/sources/subsonic/mapping.dart @@ -2,12 +2,21 @@ import 'package:xml/xml.dart'; import '../models.dart'; +Uri? uriOrNullParse(String? value) { + if (value == null || value.trim().isEmpty) { + return null; + } + + return Uri.tryParse(value); +} + Artist mapArtist(XmlElement e, XmlElement? info) => Artist( id: e.getAttribute('id')!, name: e.getAttribute('name')!, starred: DateTime.tryParse(e.getAttribute('starred').toString()), - smallImage: Uri.tryParse(info?.getElement('smallImageUrl')?.innerText ?? ''), - largeImage: Uri.tryParse(info?.getElement('largeImageUrl')?.innerText ?? ''), + coverArt: e.getAttribute('coverArt'), + smallImage: uriOrNullParse(info?.getElement('smallImageUrl')?.innerText), + largeImage: uriOrNullParse(info?.getElement('largeImageUrl')?.innerText), ); Album mapAlbum(