diff --git a/lib/models/artist.dart b/lib/models/artist.dart index 76527940..0d13afa1 100644 --- a/lib/models/artist.dart +++ b/lib/models/artist.dart @@ -17,11 +17,15 @@ class Artist { /// permissions (older koel) so the UI hides the action. bool canEdit; + /// Whether the current user has favorited this artist. + bool favorite; + Artist({ required this.id, required this.name, required this.imageUrl, this.canEdit = false, + this.favorite = false, }); ImageProvider get image { @@ -51,6 +55,7 @@ class Artist { name: json['name'], imageUrl: json['image'], canEdit: permissions is Map ? permissions['edit'] == true : false, + favorite: json['favorite'] == true, ); } @@ -60,6 +65,7 @@ class Artist { String? imageUrl, int? playCount, bool canEdit = false, + bool favorite = false, }) { Faker faker = Faker(); @@ -68,6 +74,7 @@ class Artist { name: name ?? faker.person.name(), imageUrl: imageUrl ?? faker.image.loremPicsum(width: 192, height: 192), canEdit: canEdit, + favorite: favorite, )..playCount = playCount ?? faker.randomGenerator.integer(1000); } @@ -76,7 +83,8 @@ class Artist { ..imageUrl = remote.imageUrl ..playCount = remote.playCount ?? 0 ..name = remote.name - ..canEdit = remote.canEdit; + ..canEdit = remote.canEdit + ..favorite = remote.favorite; _image = null; diff --git a/lib/providers/artist_provider.dart b/lib/providers/artist_provider.dart index b3b8170c..5de6ed5c 100644 --- a/lib/providers/artist_provider.dart +++ b/lib/providers/artist_provider.dart @@ -108,6 +108,23 @@ class ArtistProvider with ChangeNotifier, StreamSubscriber { return paginate(); } + Future toggleFavorite(Artist artist) async { + // Optimistic flip + restore on failure. + artist.favorite = !artist.favorite; + notifyListeners(); + + try { + await post('favorites/toggle', data: { + 'type': 'artist', + 'id': artist.id, + }); + } catch (_) { + artist.favorite = !artist.favorite; + notifyListeners(); + rethrow; + } + } + Future update(Artist artist, {required String name}) async { final response = await put('artists/${artist.id}', data: { 'name': name, diff --git a/lib/ui/screens/artist_action_sheet.dart b/lib/ui/screens/artist_action_sheet.dart new file mode 100644 index 00000000..8690768a --- /dev/null +++ b/lib/ui/screens/artist_action_sheet.dart @@ -0,0 +1,215 @@ +import 'package:app/main.dart'; +import 'package:app/models/models.dart'; +import 'package:app/providers/providers.dart'; +import 'package:app/ui/screens/edit_artist_sheet.dart'; +import 'package:app/ui/screens/playable_action_sheet.dart'; +import 'package:app/ui/widgets/widgets.dart'; +import 'package:app/utils/features.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ArtistActionSheet extends StatefulWidget { + final Artist artist; + + const ArtistActionSheet({Key? key, required this.artist}) : super(key: key); + + @override + State createState() => _ArtistActionSheetState(); +} + +class _ArtistActionSheetState extends State { + Future> _fetchSongs() { + return context.read().fetchForArtist(widget.artist.id); + } + + @override + Widget build(BuildContext context) { + final artist = widget.artist; + final artistProvider = context.read(); + // Favoriting non-song entities only landed in koel 7.11.0. + final showFavorite = Feature.favoriteEntities.isSupported(); + + return FrostedGlassBackground( + sigma: 40.0, + child: Container( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox.shrink(), + Column( + children: [ + ClipOval( + child: Image( + image: artist.image, + width: 192, + height: 192, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + artist.name, + textAlign: TextAlign.center, + softWrap: true, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: IntrinsicHeight( + child: Row( + children: [ + if (showFavorite) ...[ + PlayableQuickAction( + label: artist.favorite + ? 'Undo Favorite' + : 'Favorite', + icon: Icon(artist.favorite + ? CupertinoIcons.star_fill + : CupertinoIcons.star), + onTap: () { + Navigator.pop(context); + // toggleFavorite rethrows on failure (after + // rolling back the optimistic flip + // internally). The sheet has just been + // popped, so swallow here to avoid an + // unhandled async error — the UI auto- + // corrects from the rollback's + // notifyListeners. + artistProvider + .toggleFavorite(artist) + .catchError((_) {}); + }, + ), + const PlayableQuickActionDivider(), + ], + PlayableQuickAction( + label: 'Play All', + icon: const Icon(CupertinoIcons.play_fill), + onTap: () async { + Navigator.pop(context); + final songs = await _fetchSongs(); + if (songs.isEmpty) return; + await audioHandler.replaceQueue(songs); + }, + ), + const PlayableQuickActionDivider(), + PlayableQuickAction( + label: 'Shuffle All', + icon: const Icon(CupertinoIcons.shuffle), + onTap: () async { + Navigator.pop(context); + final songs = await _fetchSongs(); + if (songs.isEmpty) return; + await audioHandler.replaceQueue( + songs, + shuffle: true, + ); + }, + ), + ], + ), + ), + ), + const Divider(indent: 16, endIndent: 16), + ListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + children: [ + PlayableActionButton( + text: 'Play Next', + icon: const Icon( + CupertinoIcons.arrow_right_circle_fill, + color: Colors.white30, + ), + onTap: () async { + final songs = await _fetchSongs(); + // queueAfterCurrent inserts each song at the + // same 'after current' index, so iterating + // forward would reverse the artist's songs. + // Iterate backward so the resulting queue + // order matches the source order. + for (final song in songs.reversed) { + await audioHandler.queueAfterCurrent(song); + } + if (!mounted) return; + showOverlay( + context, + icon: CupertinoIcons.arrow_right_circle_fill, + caption: 'Queued', + message: 'To be played next.', + ); + }, + ), + PlayableActionButton( + text: 'Play Last', + icon: const Icon( + CupertinoIcons.arrow_down_right_circle_fill, + color: Colors.white30, + ), + onTap: () async { + final songs = await _fetchSongs(); + for (final song in songs) { + await audioHandler.queueToBottom(song); + } + if (!mounted) return; + showOverlay( + context, + icon: CupertinoIcons.arrow_down_right_circle_fill, + caption: 'Queued', + message: 'Queued to bottom.', + ); + }, + ), + if (artist.canEdit) ...[ + const Divider(indent: 16, endIndent: 16), + PlayableActionButton( + text: 'Edit…', + icon: const Icon( + CupertinoIcons.pencil, + color: Colors.white30, + ), + onTap: () { + Navigator.pop(context); + showEditArtistDialog(context, artist: artist); + }, + hideSheetOnTap: false, + ), + ], + ], + ), + ], + ), + ], + ), + ), + ); + } +} + +Future showArtistActionSheet( + BuildContext context, { + required Artist artist, +}) { + return showModalBottomSheet( + useRootNavigator: true, + context: context, + isScrollControlled: true, + builder: (_) => ArtistActionSheet(artist: artist), + ); +} diff --git a/lib/ui/screens/artists.dart b/lib/ui/screens/artists.dart index b9b871ec..4529c1e3 100644 --- a/lib/ui/screens/artists.dart +++ b/lib/ui/screens/artists.dart @@ -4,6 +4,7 @@ import 'package:app/models/models.dart'; import 'package:app/providers/providers.dart'; import 'package:app/router.dart'; import 'package:app/ui/placeholders/artists_screen_placeholder.dart'; +import 'package:app/ui/screens/artist_action_sheet.dart'; import 'package:app/ui/widgets/widgets.dart'; import 'package:app/values/values.dart'; import 'package:flutter/cupertino.dart'; @@ -202,8 +203,6 @@ class ArtistRow extends StatefulWidget { } class _ArtistRowState extends State { - Offset? _lastTapPosition; - @override Widget build(BuildContext context) { final artist = widget.artist; @@ -214,15 +213,7 @@ class _ArtistRowState extends State { context, artistId: artist.id, ), - onTapDown: (details) => _lastTapPosition = details.globalPosition, - onLongPress: () => showArtistActionsMenu( - context, - artist: artist, - position: _lastTapPosition ?? Offset.zero, - onUpdated: () { - if (mounted) setState(() {}); - }, - ), + onLongPress: () => showArtistActionSheet(context, artist: artist), child: ListTile( shape: Border(bottom: Divider.createBorderSide(context)), leading: AlbumArtistThumbnail.sm(entity: artist, asHero: true), diff --git a/lib/ui/screens/screens.dart b/lib/ui/screens/screens.dart index e4d2acc0..a43eac55 100644 --- a/lib/ui/screens/screens.dart +++ b/lib/ui/screens/screens.dart @@ -3,6 +3,7 @@ export 'add_to_playlist.dart'; export 'album_action_sheet.dart'; export 'album_details.dart'; export 'albums.dart'; +export 'artist_action_sheet.dart'; export 'artist_details.dart'; export 'artists.dart'; export 'create_playlist_folder_sheet.dart'; diff --git a/lib/ui/widgets/artist_actions_menu.dart b/lib/ui/widgets/artist_actions_menu.dart deleted file mode 100644 index f0f4e980..00000000 --- a/lib/ui/widgets/artist_actions_menu.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:app/models/models.dart'; -import 'package:app/ui/screens/edit_artist_sheet.dart'; -import 'package:app/ui/widgets/widgets.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; - -/// Shows the long-press context menu for an artist — currently only Edit -/// (the koel API doesn't expose a delete endpoint for artists). -/// -/// Returns immediately when the user isn't permitted to edit (no -/// haptic, no menu). Otherwise fires a medium haptic and opens the -/// menu at [position] in global screen coordinates. -/// -/// Pass [onUpdated] if the caller needs to rebuild after a successful -/// edit (the provider mutates the artist in place but doesn't notify -/// individual row/card widgets). -Future showArtistActionsMenu( - BuildContext context, { - required Artist artist, - required Offset position, - VoidCallback? onUpdated, -}) async { - if (!artist.canEdit) return; - - HapticFeedback.mediumImpact(); - - final selected = await showFrostedContextMenu( - context: context, - position: position, - items: const [ - FrostedMenuItem( - value: 'edit', - icon: CupertinoIcons.pencil, - label: 'Edit', - ), - ], - ); - - if (!context.mounted) return; - - if (selected == 'edit') { - await showEditArtistDialog(context, artist: artist); - if (!context.mounted) return; - onUpdated?.call(); - } -} diff --git a/lib/ui/widgets/artist_card.dart b/lib/ui/widgets/artist_card.dart index e31d19dc..79dc1ff4 100644 --- a/lib/ui/widgets/artist_card.dart +++ b/lib/ui/widgets/artist_card.dart @@ -1,7 +1,7 @@ import 'package:app/models/models.dart'; import 'package:app/router.dart'; +import 'package:app/ui/screens/artist_action_sheet.dart'; import 'package:app/ui/widgets/widgets.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class ArtistCard extends StatefulWidget { @@ -21,29 +21,18 @@ class ArtistCard extends StatefulWidget { class _ArtistCardState extends State { var _opacity = 1.0; final _cardWidth = 144.0; - Offset? _lastTapPosition; @override Widget build(BuildContext context) { return GestureDetector( - onTapDown: (details) { - _lastTapPosition = details.globalPosition; - setState(() => _opacity = 0.4); - }, + onTapDown: (_) => setState(() => _opacity = 0.4), onTapUp: (_) => setState(() => _opacity = 1.0), onTapCancel: () => setState(() => _opacity = 1.0), onTap: () => widget.router.gotoArtistDetailsScreen( context, artistId: widget.artist.id, ), - onLongPress: () => showArtistActionsMenu( - context, - artist: widget.artist, - position: _lastTapPosition ?? Offset.zero, - onUpdated: () { - if (mounted) setState(() {}); - }, - ), + onLongPress: () => showArtistActionSheet(context, artist: widget.artist), behavior: HitTestBehavior.opaque, child: AnimatedOpacity( duration: const Duration(milliseconds: 100), diff --git a/lib/ui/widgets/widgets.dart b/lib/ui/widgets/widgets.dart index 296e35e9..03385214 100644 --- a/lib/ui/widgets/widgets.dart +++ b/lib/ui/widgets/widgets.dart @@ -2,7 +2,6 @@ export 'album_artist_thumbnail.dart'; export 'alphabet_scrollbar.dart'; export 'album_card.dart'; export 'app_bar.dart'; -export 'artist_actions_menu.dart'; export 'artist_card.dart'; export 'bottom_space.dart'; export 'decorated_image_box.dart'; diff --git a/test/models/artist_test.dart b/test/models/artist_test.dart index 905601ea..03db58da 100644 --- a/test/models/artist_test.dart +++ b/test/models/artist_test.dart @@ -59,6 +59,33 @@ void main() { expect(Artist.fromJson(json).canEdit, isFalse); }); + + test('parses favorite from JSON', () { + final json = { + 'id': 1, + 'name': 'Loved', + 'image': null, + 'favorite': true, + }; + + expect(Artist.fromJson(json).favorite, isTrue); + }); + + test('defaults favorite to false when missing or non-bool', () { + expect( + Artist.fromJson({'id': 1, 'name': 'X', 'image': null}).favorite, + isFalse, + ); + expect( + Artist.fromJson({ + 'id': 1, + 'name': 'X', + 'image': null, + 'favorite': null, + }).favorite, + isFalse, + ); + }); }); group('Artist boolean properties', () { @@ -97,6 +124,14 @@ void main() { local.merge(remote); expect(local.canEdit, isTrue); }); + + test('merges favorite from remote', () { + final local = Artist.fake(favorite: false); + final remote = Artist.fake(favorite: true); + + local.merge(remote); + expect(local.favorite, isTrue); + }); }); group('Artist.fake', () { diff --git a/test/ui/screens/artist_action_sheet_test.dart b/test/ui/screens/artist_action_sheet_test.dart new file mode 100644 index 00000000..44ff1a53 --- /dev/null +++ b/test/ui/screens/artist_action_sheet_test.dart @@ -0,0 +1,221 @@ +import 'package:app/app_state.dart'; +import 'package:app/audio_handler.dart'; +import 'package:app/main.dart' as app; +import 'package:app/models/artist.dart'; +import 'package:app/models/song.dart'; +import 'package:app/providers/artist_provider.dart'; +import 'package:app/providers/playable_provider.dart'; +import 'package:app/ui/screens/artist_action_sheet.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:version/version.dart'; + +import '../../extensions/widget_tester_extension.dart'; +import 'artist_action_sheet_test.mocks.dart'; + +@GenerateMocks([KoelAudioHandler, ArtistProvider, PlayableProvider]) +void main() { + late MockKoelAudioHandler audioHandlerMock; + late MockArtistProvider artistProviderMock; + late MockPlayableProvider playableProviderMock; + late BehaviorSubject mediaItemSubject; + + setUp(() { + AppState.clear(); + // Default to a koel version where the per-entity favorite feature + // is supported; individual tests override when they need older. + AppState.set(['app', 'apiVersion'], Version.parse('7.11.0')); + + audioHandlerMock = MockKoelAudioHandler(); + artistProviderMock = MockArtistProvider(); + playableProviderMock = MockPlayableProvider(); + + mediaItemSubject = BehaviorSubject.seeded(null); + when(audioHandlerMock.mediaItem).thenAnswer((_) => mediaItemSubject); + when(audioHandlerMock.queued(any)).thenAnswer((_) async => false); + when(audioHandlerMock.queueAfterCurrent(any)).thenAnswer((_) async {}); + when(audioHandlerMock.queueToBottom(any)).thenAnswer((_) async {}); + when(audioHandlerMock.replaceQueue( + any, + shuffle: anyNamed('shuffle'), + autoPlay: anyNamed('autoPlay'), + )).thenAnswer((_) async {}); + + app.audioHandler = audioHandlerMock; + }); + + tearDown(() { + mediaItemSubject.close(); + }); + + Future mount(WidgetTester tester, Artist artist) async { + await tester.pumpAppWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: artistProviderMock, + ), + ChangeNotifierProvider.value( + value: playableProviderMock, + ), + ], + child: ArtistActionSheet(artist: artist), + ), + ); + } + + group('structure', () { + testWidgets('renders the artist name', (tester) async { + await mount(tester, Artist.fake(name: 'The Beatles')); + + expect(find.text('The Beatles'), findsOneWidget); + }); + + testWidgets('renders the three quick actions', (tester) async { + await mount(tester, Artist.fake(name: 'A')); + + expect(find.text('Favorite'), findsOneWidget); + expect(find.text('Play All'), findsOneWidget); + expect(find.text('Shuffle All'), findsOneWidget); + }); + + testWidgets('shows "Undo Favorite" when artist.favorite is true', + (tester) async { + await mount(tester, Artist.fake(name: 'Loved', favorite: true)); + + expect(find.text('Undo Favorite'), findsOneWidget); + expect(find.text('Favorite'), findsNothing); + }); + + testWidgets('shows Edit only when canEdit is true', (tester) async { + await mount(tester, Artist.fake(name: 'Editable', canEdit: true)); + expect(find.text('Edit…'), findsOneWidget); + }); + + testWidgets('hides Edit when canEdit is false', (tester) async { + await mount(tester, Artist.fake(name: 'Read-only')); + expect(find.text('Edit…'), findsNothing); + }); + + testWidgets('does not show a Go to Artist row', (tester) async { + await mount(tester, Artist.fake(name: 'Pink Floyd')); + expect(find.text('Go to Artist'), findsNothing); + }); + + testWidgets( + 'hides Favorite when the koel version is below 7.11.0', + (tester) async { + AppState.set(['app', 'apiVersion'], Version.parse('7.10.0')); + + await mount(tester, Artist.fake(name: 'A')); + + expect(find.text('Favorite'), findsNothing); + expect(find.text('Undo Favorite'), findsNothing); + // The other two stay. + expect(find.text('Play All'), findsOneWidget); + expect(find.text('Shuffle All'), findsOneWidget); + }, + ); + }); + + group('actions', () { + testWidgets('tapping Favorite delegates to ArtistProvider.toggleFavorite', + (tester) async { + final artist = Artist.fake(name: 'Loved'); + when(artistProviderMock.toggleFavorite(artist)) + .thenAnswer((_) async {}); + + await mount(tester, artist); + await tester.tap(find.text('Favorite')); + await tester.pump(); + + verify(artistProviderMock.toggleFavorite(artist)).called(1); + }); + + testWidgets('tapping Play All replaces the queue without shuffling', + (tester) async { + final artist = Artist.fake(name: 'A'); + final songs = Song.fakeMany(3); + when(playableProviderMock.fetchForArtist(artist.id)) + .thenAnswer((_) async => songs); + + await mount(tester, artist); + await tester.tap(find.text('Play All')); + // First pump runs the synchronous parts of onTap; second pump + // settles the async fetch and replaceQueue. + await tester.pumpAndSettle(); + + verify(playableProviderMock.fetchForArtist(artist.id)).called(1); + verify(audioHandlerMock.replaceQueue(songs)).called(1); + }); + + testWidgets('tapping Shuffle All replaces the queue with shuffle', + (tester) async { + final artist = Artist.fake(name: 'A'); + final songs = Song.fakeMany(3); + when(playableProviderMock.fetchForArtist(artist.id)) + .thenAnswer((_) async => songs); + + await mount(tester, artist); + await tester.tap(find.text('Shuffle All')); + await tester.pumpAndSettle(); + + verify(playableProviderMock.fetchForArtist(artist.id)).called(1); + verify(audioHandlerMock.replaceQueue(songs, shuffle: true)).called(1); + }); + + testWidgets( + 'tapping Play Next inserts songs in reverse so the queue ends up ' + 'in source order', + (tester) async { + final artist = Artist.fake(name: 'A'); + final songs = Song.fakeMany(3); + when(playableProviderMock.fetchForArtist(artist.id)) + .thenAnswer((_) async => songs); + + await mount(tester, artist); + await tester.tap(find.text('Play Next')); + await tester.pumpAndSettle(); + // Flush the showOverlay's auto-dismiss timer so the test + // tear-down doesn't complain about pending timers. + await tester.pump(const Duration(seconds: 3)); + + // queueAfterCurrent inserts at a fixed 'after current' index, + // so to land in source order [0, 1, 2] the implementation has + // to call them in reverse: 2, then 1, then 0. + verifyInOrder([ + audioHandlerMock.queueAfterCurrent(songs[2]), + audioHandlerMock.queueAfterCurrent(songs[1]), + audioHandlerMock.queueAfterCurrent(songs[0]), + ]); + verifyNever(audioHandlerMock.queueToBottom(any)); + }, + ); + + testWidgets( + 'tapping Play Last appends songs in source order', + (tester) async { + final artist = Artist.fake(name: 'A'); + final songs = Song.fakeMany(3); + when(playableProviderMock.fetchForArtist(artist.id)) + .thenAnswer((_) async => songs); + + await mount(tester, artist); + await tester.tap(find.text('Play Last')); + await tester.pumpAndSettle(); + await tester.pump(const Duration(seconds: 3)); + + verifyInOrder([ + audioHandlerMock.queueToBottom(songs[0]), + audioHandlerMock.queueToBottom(songs[1]), + audioHandlerMock.queueToBottom(songs[2]), + ]); + verifyNever(audioHandlerMock.queueAfterCurrent(any)); + }, + ); + }); +} diff --git a/test/ui/screens/artist_action_sheet_test.mocks.dart b/test/ui/screens/artist_action_sheet_test.mocks.dart new file mode 100644 index 00000000..d53bcca5 --- /dev/null +++ b/test/ui/screens/artist_action_sheet_test.mocks.dart @@ -0,0 +1,1465 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in app/test/ui/screens/artist_action_sheet_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i9; +import 'dart:ui' as _i12; + +import 'package:app/audio_handler.dart' as _i7; +import 'package:app/enums.dart' as _i11; +import 'package:app/models/models.dart' as _i5; +import 'package:app/providers/providers.dart' as _i2; +import 'package:app/values/values.dart' as _i6; +import 'package:audio_service/audio_service.dart' as _i8; +import 'package:just_audio/just_audio.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i10; +import 'package:rxdart/rxdart.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDownloadProvider_0 extends _i1.SmartFake + implements _i2.DownloadProvider { + _FakeDownloadProvider_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlayableProvider_1 extends _i1.SmartFake + implements _i2.PlayableProvider { + _FakePlayableProvider_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeAudioPlayer_2 extends _i1.SmartFake implements _i3.AudioPlayer { + _FakeAudioPlayer_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBehaviorSubject_3 extends _i1.SmartFake + implements _i4.BehaviorSubject { + _FakeBehaviorSubject_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePublishSubject_4 extends _i1.SmartFake + implements _i4.PublishSubject { + _FakePublishSubject_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeValueStream_5 extends _i1.SmartFake + implements _i4.ValueStream { + _FakeValueStream_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeArtist_6 extends _i1.SmartFake implements _i5.Artist { + _FakeArtist_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePaginationResult_7 extends _i1.SmartFake + implements _i6.PaginationResult { + _FakePaginationResult_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [KoelAudioHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockKoelAudioHandler extends _i1.Mock implements _i7.KoelAudioHandler { + MockKoelAudioHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.DownloadProvider get downloadProvider => (super.noSuchMethod( + Invocation.getter(#downloadProvider), + returnValue: _FakeDownloadProvider_0( + this, + Invocation.getter(#downloadProvider), + ), + ) as _i2.DownloadProvider); + + @override + _i2.PlayableProvider get playableProvider => (super.noSuchMethod( + Invocation.getter(#playableProvider), + returnValue: _FakePlayableProvider_1( + this, + Invocation.getter(#playableProvider), + ), + ) as _i2.PlayableProvider); + + @override + _i8.AudioServiceRepeatMode get repeatMode => (super.noSuchMethod( + Invocation.getter(#repeatMode), + returnValue: _i8.AudioServiceRepeatMode.none, + ) as _i8.AudioServiceRepeatMode); + + @override + _i3.AudioPlayer get player => (super.noSuchMethod( + Invocation.getter(#player), + returnValue: _FakeAudioPlayer_2( + this, + Invocation.getter(#player), + ), + ) as _i3.AudioPlayer); + + @override + bool get isRadioMode => (super.noSuchMethod( + Invocation.getter(#isRadioMode), + returnValue: false, + ) as bool); + + @override + int get currentQueueIndex => (super.noSuchMethod( + Invocation.getter(#currentQueueIndex), + returnValue: 0, + ) as int); + + @override + set downloadProvider(_i2.DownloadProvider? _downloadProvider) => + super.noSuchMethod( + Invocation.setter( + #downloadProvider, + _downloadProvider, + ), + returnValueForMissingStub: null, + ); + + @override + set playableProvider(_i2.PlayableProvider? _playableProvider) => + super.noSuchMethod( + Invocation.setter( + #playableProvider, + _playableProvider, + ), + returnValueForMissingStub: null, + ); + + @override + set repeatMode(_i8.AudioServiceRepeatMode? _repeatMode) => super.noSuchMethod( + Invocation.setter( + #repeatMode, + _repeatMode, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.BehaviorSubject<_i8.PlaybackState> get playbackState => + (super.noSuchMethod( + Invocation.getter(#playbackState), + returnValue: _FakeBehaviorSubject_3<_i8.PlaybackState>( + this, + Invocation.getter(#playbackState), + ), + ) as _i4.BehaviorSubject<_i8.PlaybackState>); + + @override + _i4.BehaviorSubject> get queue => (super.noSuchMethod( + Invocation.getter(#queue), + returnValue: _FakeBehaviorSubject_3>( + this, + Invocation.getter(#queue), + ), + ) as _i4.BehaviorSubject>); + + @override + _i4.BehaviorSubject get queueTitle => (super.noSuchMethod( + Invocation.getter(#queueTitle), + returnValue: _FakeBehaviorSubject_3( + this, + Invocation.getter(#queueTitle), + ), + ) as _i4.BehaviorSubject); + + @override + _i4.BehaviorSubject<_i8.MediaItem?> get mediaItem => (super.noSuchMethod( + Invocation.getter(#mediaItem), + returnValue: _FakeBehaviorSubject_3<_i8.MediaItem?>( + this, + Invocation.getter(#mediaItem), + ), + ) as _i4.BehaviorSubject<_i8.MediaItem?>); + + @override + _i4.BehaviorSubject<_i8.AndroidPlaybackInfo> get androidPlaybackInfo => + (super.noSuchMethod( + Invocation.getter(#androidPlaybackInfo), + returnValue: _FakeBehaviorSubject_3<_i8.AndroidPlaybackInfo>( + this, + Invocation.getter(#androidPlaybackInfo), + ), + ) as _i4.BehaviorSubject<_i8.AndroidPlaybackInfo>); + + @override + _i4.BehaviorSubject<_i8.RatingStyle> get ratingStyle => (super.noSuchMethod( + Invocation.getter(#ratingStyle), + returnValue: _FakeBehaviorSubject_3<_i8.RatingStyle>( + this, + Invocation.getter(#ratingStyle), + ), + ) as _i4.BehaviorSubject<_i8.RatingStyle>); + + @override + _i4.PublishSubject get customEvent => (super.noSuchMethod( + Invocation.getter(#customEvent), + returnValue: _FakePublishSubject_4( + this, + Invocation.getter(#customEvent), + ), + ) as _i4.PublishSubject); + + @override + _i4.BehaviorSubject get customState => (super.noSuchMethod( + Invocation.getter(#customState), + returnValue: _FakeBehaviorSubject_3( + this, + Invocation.getter(#customState), + ), + ) as _i4.BehaviorSubject); + + @override + dynamic init({ + required _i2.PlayableProvider? playableProvider, + required _i2.DownloadProvider? downloadProvider, + }) => + super.noSuchMethod(Invocation.method( + #init, + [], + { + #playableProvider: playableProvider, + #downloadProvider: downloadProvider, + }, + )); + + @override + void enterRadioMode(_i3.AudioPlayer? radioPlayer) => super.noSuchMethod( + Invocation.method( + #enterRadioMode, + [radioPlayer], + ), + returnValueForMissingStub: null, + ); + + @override + void exitRadioMode() => super.noSuchMethod( + Invocation.method( + #exitRadioMode, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void updateRadioPlaybackState({ + required bool? playing, + required _i8.AudioProcessingState? processingState, + }) => + super.noSuchMethod( + Invocation.method( + #updateRadioPlaybackState, + [], + { + #playing: playing, + #processingState: processingState, + }, + ), + returnValueForMissingStub: null, + ); + + @override + num? getPlaybackPositionFromState(String? playableId) => + (super.noSuchMethod(Invocation.method( + #getPlaybackPositionFromState, + [playableId], + )) as num?); + + @override + void setPlaybackPositionToState( + String? playableId, + num? position, + ) => + super.noSuchMethod( + Invocation.method( + #setPlaybackPositionToState, + [ + playableId, + position, + ], + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Future play() => (super.noSuchMethod( + Invocation.method( + #play, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future stop() => (super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future queueAndPlay(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #queueAndPlay, + [playable], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future maybeQueueAndPlay( + _i5.Playable? playable, { + dynamic position = 0, + }) => + (super.noSuchMethod( + Invocation.method( + #maybeQueueAndPlay, + [playable], + {#position: position}, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future queueAfterCurrent(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #queueAfterCurrent, + [playable], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future playOrPause() => (super.noSuchMethod( + Invocation.method( + #playOrPause, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future skipToNext() => (super.noSuchMethod( + Invocation.method( + #skipToNext, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future seek(Duration? position) => (super.noSuchMethod( + Invocation.method( + #seek, + [position], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future skipToPrevious() => (super.noSuchMethod( + Invocation.method( + #skipToPrevious, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future queued(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #queued, + [playable], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + _i9.Future removeQueueItemAt(int? index) => (super.noSuchMethod( + Invocation.method( + #removeQueueItemAt, + [index], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + void moveQueueItem( + int? oldIndex, + int? newIndex, + ) => + super.noSuchMethod( + Invocation.method( + #moveQueueItem, + [ + oldIndex, + newIndex, + ], + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Future clearQueue() => (super.noSuchMethod( + Invocation.method( + #clearQueue, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future setVolume(double? value) => (super.noSuchMethod( + Invocation.method( + #setVolume, + [value], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future replaceQueue( + List<_i5.Playable>? playables, { + bool? shuffle = false, + bool? autoPlay = true, + }) => + (super.noSuchMethod( + Invocation.method( + #replaceQueue, + [playables], + { + #shuffle: shuffle, + #autoPlay: autoPlay, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future<_i8.AudioServiceRepeatMode> rotateRepeatMode() => + (super.noSuchMethod( + Invocation.method( + #rotateRepeatMode, + [], + ), + returnValue: _i9.Future<_i8.AudioServiceRepeatMode>.value( + _i8.AudioServiceRepeatMode.none), + ) as _i9.Future<_i8.AudioServiceRepeatMode>); + + @override + _i9.Future setRepeatMode(_i8.AudioServiceRepeatMode? repeatMode) => + (super.noSuchMethod( + Invocation.method( + #setRepeatMode, + [repeatMode], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future cleanUpUponLogout() => (super.noSuchMethod( + Invocation.method( + #cleanUpUponLogout, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future queueToBottom(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #queueToBottom, + [playable], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future removeFromQueue(_i5.Playable? playable) => + (super.noSuchMethod( + Invocation.method( + #removeFromQueue, + [playable], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future prepare() => (super.noSuchMethod( + Invocation.method( + #prepare, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future prepareFromMediaId( + String? mediaId, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #prepareFromMediaId, + [ + mediaId, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future prepareFromSearch( + String? query, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #prepareFromSearch, + [ + query, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future prepareFromUri( + Uri? uri, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #prepareFromUri, + [ + uri, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future playFromMediaId( + String? mediaId, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #playFromMediaId, + [ + mediaId, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future playFromSearch( + String? query, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #playFromSearch, + [ + query, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future playFromUri( + Uri? uri, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #playFromUri, + [ + uri, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future playMediaItem(_i8.MediaItem? mediaItem) => + (super.noSuchMethod( + Invocation.method( + #playMediaItem, + [mediaItem], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future click([_i8.MediaButton? button = _i8.MediaButton.media]) => + (super.noSuchMethod( + Invocation.method( + #click, + [button], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future addQueueItem(_i8.MediaItem? mediaItem) => + (super.noSuchMethod( + Invocation.method( + #addQueueItem, + [mediaItem], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future addQueueItems(List<_i8.MediaItem>? mediaItems) => + (super.noSuchMethod( + Invocation.method( + #addQueueItems, + [mediaItems], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future insertQueueItem( + int? index, + _i8.MediaItem? mediaItem, + ) => + (super.noSuchMethod( + Invocation.method( + #insertQueueItem, + [ + index, + mediaItem, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future updateQueue(List<_i8.MediaItem>? queue) => + (super.noSuchMethod( + Invocation.method( + #updateQueue, + [queue], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future updateMediaItem(_i8.MediaItem? mediaItem) => + (super.noSuchMethod( + Invocation.method( + #updateMediaItem, + [mediaItem], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future removeQueueItem(_i8.MediaItem? mediaItem) => + (super.noSuchMethod( + Invocation.method( + #removeQueueItem, + [mediaItem], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future fastForward() => (super.noSuchMethod( + Invocation.method( + #fastForward, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future rewind() => (super.noSuchMethod( + Invocation.method( + #rewind, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future skipToQueueItem(int? index) => (super.noSuchMethod( + Invocation.method( + #skipToQueueItem, + [index], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future setRating( + _i8.Rating? rating, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #setRating, + [ + rating, + extras, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future setCaptioningEnabled(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setCaptioningEnabled, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future setShuffleMode(_i8.AudioServiceShuffleMode? shuffleMode) => + (super.noSuchMethod( + Invocation.method( + #setShuffleMode, + [shuffleMode], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future seekBackward(bool? begin) => (super.noSuchMethod( + Invocation.method( + #seekBackward, + [begin], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future seekForward(bool? begin) => (super.noSuchMethod( + Invocation.method( + #seekForward, + [begin], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future setSpeed(double? speed) => (super.noSuchMethod( + Invocation.method( + #setSpeed, + [speed], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future customAction( + String? name, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #customAction, + [ + name, + extras, + ], + ), + returnValue: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future onTaskRemoved() => (super.noSuchMethod( + Invocation.method( + #onTaskRemoved, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future onNotificationDeleted() => (super.noSuchMethod( + Invocation.method( + #onNotificationDeleted, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future> getChildren( + String? parentMediaId, [ + Map? options, + ]) => + (super.noSuchMethod( + Invocation.method( + #getChildren, + [ + parentMediaId, + options, + ], + ), + returnValue: _i9.Future>.value(<_i8.MediaItem>[]), + ) as _i9.Future>); + + @override + _i4.ValueStream> subscribeToChildren( + String? parentMediaId) => + (super.noSuchMethod( + Invocation.method( + #subscribeToChildren, + [parentMediaId], + ), + returnValue: _FakeValueStream_5>( + this, + Invocation.method( + #subscribeToChildren, + [parentMediaId], + ), + ), + ) as _i4.ValueStream>); + + @override + _i9.Future<_i8.MediaItem?> getMediaItem(String? mediaId) => + (super.noSuchMethod( + Invocation.method( + #getMediaItem, + [mediaId], + ), + returnValue: _i9.Future<_i8.MediaItem?>.value(), + ) as _i9.Future<_i8.MediaItem?>); + + @override + _i9.Future> search( + String? query, [ + Map? extras, + ]) => + (super.noSuchMethod( + Invocation.method( + #search, + [ + query, + extras, + ], + ), + returnValue: _i9.Future>.value(<_i8.MediaItem>[]), + ) as _i9.Future>); + + @override + _i9.Future androidAdjustRemoteVolume( + _i8.AndroidVolumeDirection? direction) => + (super.noSuchMethod( + Invocation.method( + #androidAdjustRemoteVolume, + [direction], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future androidSetRemoteVolume(int? volumeIndex) => + (super.noSuchMethod( + Invocation.method( + #androidSetRemoteVolume, + [volumeIndex], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [ArtistProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockArtistProvider extends _i1.Mock implements _i2.ArtistProvider { + MockArtistProvider() { + _i1.throwOnMissingStub(this); + } + + @override + List<_i5.Artist> get artists => (super.noSuchMethod( + Invocation.getter(#artists), + returnValue: <_i5.Artist>[], + ) as List<_i5.Artist>); + + @override + String get sortField => (super.noSuchMethod( + Invocation.getter(#sortField), + returnValue: _i10.dummyValue( + this, + Invocation.getter(#sortField), + ), + ) as String); + + @override + _i11.SortOrder get sortOrder => (super.noSuchMethod( + Invocation.getter(#sortOrder), + returnValue: _i11.SortOrder.asc, + ) as _i11.SortOrder); + + @override + set artists(List<_i5.Artist>? _artists) => super.noSuchMethod( + Invocation.setter( + #artists, + _artists, + ), + returnValueForMissingStub: null, + ); + + @override + set sortField(String? field) => super.noSuchMethod( + Invocation.setter( + #sortField, + field, + ), + returnValueForMissingStub: null, + ); + + @override + set sortOrder(_i11.SortOrder? order) => super.noSuchMethod( + Invocation.setter( + #sortOrder, + order, + ), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + List<_i5.Artist> byIds(List? ids) => (super.noSuchMethod( + Invocation.method( + #byIds, + [ids], + ), + returnValue: <_i5.Artist>[], + ) as List<_i5.Artist>); + + @override + _i9.Future<_i5.Artist> resolve( + dynamic id, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #resolve, + [id], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i9.Future<_i5.Artist>.value(_FakeArtist_6( + this, + Invocation.method( + #resolve, + [id], + {#forceRefresh: forceRefresh}, + ), + )), + ) as _i9.Future<_i5.Artist>); + + @override + List<_i5.Artist> syncWithVault(dynamic _artists) => (super.noSuchMethod( + Invocation.method( + #syncWithVault, + [_artists], + ), + returnValue: <_i5.Artist>[], + ) as List<_i5.Artist>); + + @override + _i9.Future paginate() => (super.noSuchMethod( + Invocation.method( + #paginate, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future refresh() => (super.noSuchMethod( + Invocation.method( + #refresh, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future toggleFavorite(_i5.Artist? artist) => (super.noSuchMethod( + Invocation.method( + #toggleFavorite, + [artist], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future update( + _i5.Artist? artist, { + required String? name, + }) => + (super.noSuchMethod( + Invocation.method( + #update, + [artist], + {#name: name}, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void unsubscribeAll() => super.noSuchMethod( + Invocation.method( + #unsubscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void subscribe(_i9.StreamSubscription? sub) => super.noSuchMethod( + Invocation.method( + #subscribe, + [sub], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PlayableProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlayableProvider extends _i1.Mock implements _i2.PlayableProvider { + MockPlayableProvider() { + _i1.throwOnMissingStub(this); + } + + @override + List<_i5.Playable> get playables => (super.noSuchMethod( + Invocation.getter(#playables), + returnValue: <_i5.Playable>[], + ) as List<_i5.Playable>); + + @override + set playables(List<_i5.Playable>? _playables) => super.noSuchMethod( + Invocation.setter( + #playables, + _playables, + ), + returnValueForMissingStub: null, + ); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + List<_i5.Playable> syncWithVault(dynamic _playables) => + (super.noSuchMethod( + Invocation.method( + #syncWithVault, + [_playables], + ), + returnValue: <_i5.Playable>[], + ) as List<_i5.Playable>); + + @override + _i5.Playable? byId(String? id) => + (super.noSuchMethod(Invocation.method( + #byId, + [id], + )) as _i5.Playable?); + + @override + _i9.Future<_i6.PaginationResult<_i5.Playable>> paginate( + _i2.PlayablePaginationConfig? config) => + (super.noSuchMethod( + Invocation.method( + #paginate, + [config], + ), + returnValue: + _i9.Future<_i6.PaginationResult<_i5.Playable>>.value( + _FakePaginationResult_7<_i5.Playable>( + this, + Invocation.method( + #paginate, + [config], + ), + )), + ) as _i9.Future<_i6.PaginationResult<_i5.Playable>>); + + @override + _i9.Future>> fetchForArtist( + dynamic artistId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #fetchForArtist, + [artistId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + _i9.Future<_i6.PaginationResult<_i5.Playable>> paginateByGenre( + String? genreId, { + int? page = 1, + String? sort = 'title', + _i11.SortOrder? order = _i11.SortOrder.asc, + }) => + (super.noSuchMethod( + Invocation.method( + #paginateByGenre, + [genreId], + { + #page: page, + #sort: sort, + #order: order, + }, + ), + returnValue: + _i9.Future<_i6.PaginationResult<_i5.Playable>>.value( + _FakePaginationResult_7<_i5.Playable>( + this, + Invocation.method( + #paginateByGenre, + [genreId], + { + #page: page, + #sort: sort, + #order: order, + }, + ), + )), + ) as _i9.Future<_i6.PaginationResult<_i5.Playable>>); + + @override + _i9.Future>> fetchForAlbum( + dynamic albumId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #fetchForAlbum, + [albumId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + _i9.Future>> fetchForPlaylist( + dynamic playlistId, { + bool? forceRefresh = false, + }) => + (super.noSuchMethod( + Invocation.method( + #fetchForPlaylist, + [playlistId], + {#forceRefresh: forceRefresh}, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + _i9.Future>> fetchForPodcast( + String? podcastId, { + bool? forceRefresh = false, + bool? getUpdates = false, + }) => + (super.noSuchMethod( + Invocation.method( + #fetchForPodcast, + [podcastId], + { + #forceRefresh: forceRefresh, + #getUpdates: getUpdates, + }, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + _i9.Future>> fetchRandom({int? limit = 500}) => + (super.noSuchMethod( + Invocation.method( + #fetchRandom, + [], + {#limit: limit}, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + _i9.Future>> fetchInOrder({ + String? sortField = 'title', + _i11.SortOrder? order = _i11.SortOrder.asc, + int? limit = 500, + }) => + (super.noSuchMethod( + Invocation.method( + #fetchInOrder, + [], + { + #sortField: sortField, + #order: order, + #limit: limit, + }, + ), + returnValue: _i9.Future>>.value( + <_i5.Playable>[]), + ) as _i9.Future>>); + + @override + List<_i5.Playable> parseFromJson(dynamic json) => + (super.noSuchMethod( + Invocation.method( + #parseFromJson, + [json], + ), + returnValue: <_i5.Playable>[], + ) as List<_i5.Playable>); + + @override + void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void unsubscribeAll() => super.noSuchMethod( + Invocation.method( + #unsubscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void subscribe(_i9.StreamSubscription? sub) => super.noSuchMethod( + Invocation.method( + #subscribe, + [sub], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/ui/widgets/artist_card_test.dart b/test/ui/widgets/artist_card_test.dart index 7027f59a..c2169147 100644 --- a/test/ui/widgets/artist_card_test.dart +++ b/test/ui/widgets/artist_card_test.dart @@ -1,17 +1,15 @@ import 'package:app/models/artist.dart'; -import 'package:app/providers/artist_provider.dart'; import 'package:app/router.dart'; -import 'package:app/ui/widgets/artist_card.dart'; import 'package:app/ui/widgets/album_artist_thumbnail.dart'; +import 'package:app/ui/widgets/artist_card.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:provider/provider.dart'; import '../../extensions/widget_tester_extension.dart'; import 'artist_card_test.mocks.dart'; -@GenerateMocks([AppRouter, ArtistProvider]) +@GenerateMocks([AppRouter]) void main() { late Artist artist; @@ -37,35 +35,4 @@ void main() { await tester.tap(find.text('Banana')); verify(router.gotoArtistDetailsScreen(any, artistId: artist.id)).called(1); }); - - group('long-press context menu', () { - Future mountWithProvider(WidgetTester tester, Artist artist) async { - await tester.pumpAppWidget( - ChangeNotifierProvider.value( - value: MockArtistProvider(), - child: ArtistCard(artist: artist), - ), - ); - } - - testWidgets('shows Edit when canEdit', (tester) async { - final editable = Artist.fake(name: 'Editable', canEdit: true); - await mountWithProvider(tester, editable); - - await tester.longPress(find.byType(ArtistCard)); - await tester.pumpAndSettle(); - - expect(find.text('Edit'), findsOneWidget); - }); - - testWidgets('no menu when canEdit is false', (tester) async { - final readonly = Artist.fake(name: 'Read-only'); - await mountWithProvider(tester, readonly); - - await tester.longPress(find.byType(ArtistCard)); - await tester.pumpAndSettle(); - - expect(find.text('Edit'), findsNothing); - }); - }); } diff --git a/test/ui/widgets/artist_card_test.mocks.dart b/test/ui/widgets/artist_card_test.mocks.dart index eebcc9de..fb3289ed 100644 --- a/test/ui/widgets/artist_card_test.mocks.dart +++ b/test/ui/widgets/artist_card_test.mocks.dart @@ -3,16 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; -import 'dart:ui' as _i9; +import 'dart:async' as _i3; -import 'package:app/enums.dart' as _i8; -import 'package:app/models/models.dart' as _i2; -import 'package:app/providers/providers.dart' as _i6; -import 'package:app/router.dart' as _i3; -import 'package:flutter/cupertino.dart' as _i5; +import 'package:app/models/models.dart' as _i5; +import 'package:app/router.dart' as _i2; +import 'package:flutter/cupertino.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,27 +24,17 @@ import 'package:mockito/src/dummies.dart' as _i7; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeArtist_0 extends _i1.SmartFake implements _i2.Artist { - _FakeArtist_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [AppRouter]. /// /// See the documentation for Mockito's code generation for more information. -class MockAppRouter extends _i1.Mock implements _i3.AppRouter { +class MockAppRouter extends _i1.Mock implements _i2.AppRouter { MockAppRouter() { _i1.throwOnMissingStub(this); } @override - _i4.Future gotoAlbumDetailsScreen( - _i5.BuildContext? context, { + _i3.Future gotoAlbumDetailsScreen( + _i4.BuildContext? context, { required dynamic albumId, }) => (super.noSuchMethod( @@ -57,13 +43,13 @@ class MockAppRouter extends _i1.Mock implements _i3.AppRouter { [context], {#albumId: albumId}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future gotoArtistDetailsScreen( - _i5.BuildContext? context, { + _i3.Future gotoArtistDetailsScreen( + _i4.BuildContext? context, { required dynamic artistId, }) => (super.noSuchMethod( @@ -72,13 +58,13 @@ class MockAppRouter extends _i1.Mock implements _i3.AppRouter { [context], {#artistId: artistId}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override dynamic gotoPodcastDetailsScreen( - _i5.BuildContext? context, { + _i4.BuildContext? context, { required String? podcastId, }) => super.noSuchMethod(Invocation.method( @@ -89,8 +75,8 @@ class MockAppRouter extends _i1.Mock implements _i3.AppRouter { @override dynamic gotoGenreDetailsScreen( - _i5.BuildContext? context, { - required _i2.Genre? genre, + _i4.BuildContext? context, { + required _i5.Genre? genre, }) => super.noSuchMethod(Invocation.method( #gotoGenreDetailsScreen, @@ -99,42 +85,42 @@ class MockAppRouter extends _i1.Mock implements _i3.AppRouter { )); @override - _i4.Future openNowPlayingScreen(_i5.BuildContext? context) => + _i3.Future openNowPlayingScreen(_i4.BuildContext? context) => (super.noSuchMethod( Invocation.method( #openNowPlayingScreen, [context], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future showCreatePlaylistSheet(_i5.BuildContext? context) => + _i3.Future showCreatePlaylistSheet(_i4.BuildContext? context) => (super.noSuchMethod( Invocation.method( #showCreatePlaylistSheet, [context], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future showCreatePlaylistFolderSheet(_i5.BuildContext? context) => + _i3.Future showCreatePlaylistFolderSheet(_i4.BuildContext? context) => (super.noSuchMethod( Invocation.method( #showCreatePlaylistFolderSheet, [context], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future showPlayableActionSheet( - _i5.BuildContext? context, { - required _i2.Playable? playable, + _i3.Future showPlayableActionSheet( + _i4.BuildContext? context, { + required _i5.Playable? playable, }) => (super.noSuchMethod( Invocation.method( @@ -142,209 +128,18 @@ class MockAppRouter extends _i1.Mock implements _i3.AppRouter { [context], {#playable: playable}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future showAddPodcastSheet(_i5.BuildContext? context) => + _i3.Future showAddPodcastSheet(_i4.BuildContext? context) => (super.noSuchMethod( Invocation.method( #showAddPodcastSheet, [context], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); -} - -/// A class which mocks [ArtistProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockArtistProvider extends _i1.Mock implements _i6.ArtistProvider { - MockArtistProvider() { - _i1.throwOnMissingStub(this); - } - - @override - List<_i2.Artist> get artists => (super.noSuchMethod( - Invocation.getter(#artists), - returnValue: <_i2.Artist>[], - ) as List<_i2.Artist>); - - @override - String get sortField => (super.noSuchMethod( - Invocation.getter(#sortField), - returnValue: _i7.dummyValue( - this, - Invocation.getter(#sortField), - ), - ) as String); - - @override - _i8.SortOrder get sortOrder => (super.noSuchMethod( - Invocation.getter(#sortOrder), - returnValue: _i8.SortOrder.asc, - ) as _i8.SortOrder); - - @override - set artists(List<_i2.Artist>? _artists) => super.noSuchMethod( - Invocation.setter( - #artists, - _artists, - ), - returnValueForMissingStub: null, - ); - - @override - set sortField(String? field) => super.noSuchMethod( - Invocation.setter( - #sortField, - field, - ), - returnValueForMissingStub: null, - ); - - @override - set sortOrder(_i8.SortOrder? order) => super.noSuchMethod( - Invocation.setter( - #sortOrder, - order, - ), - returnValueForMissingStub: null, - ); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - List<_i2.Artist> byIds(List? ids) => (super.noSuchMethod( - Invocation.method( - #byIds, - [ids], - ), - returnValue: <_i2.Artist>[], - ) as List<_i2.Artist>); - - @override - _i4.Future<_i2.Artist> resolve( - dynamic id, { - bool? forceRefresh = false, - }) => - (super.noSuchMethod( - Invocation.method( - #resolve, - [id], - {#forceRefresh: forceRefresh}, - ), - returnValue: _i4.Future<_i2.Artist>.value(_FakeArtist_0( - this, - Invocation.method( - #resolve, - [id], - {#forceRefresh: forceRefresh}, - ), - )), - ) as _i4.Future<_i2.Artist>); - - @override - List<_i2.Artist> syncWithVault(dynamic _artists) => (super.noSuchMethod( - Invocation.method( - #syncWithVault, - [_artists], - ), - returnValue: <_i2.Artist>[], - ) as List<_i2.Artist>); - - @override - _i4.Future paginate() => (super.noSuchMethod( - Invocation.method( - #paginate, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future refresh() => (super.noSuchMethod( - Invocation.method( - #refresh, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future update( - _i2.Artist? artist, { - required String? name, - }) => - (super.noSuchMethod( - Invocation.method( - #update, - [artist], - {#name: name}, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void addListener(_i9.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i9.VoidCallback? listener) => super.noSuchMethod( - Invocation.method( - #removeListener, - [listener], - ), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void unsubscribeAll() => super.noSuchMethod( - Invocation.method( - #unsubscribeAll, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void subscribe(_i4.StreamSubscription? sub) => super.noSuchMethod( - Invocation.method( - #subscribe, - [sub], - ), - returnValueForMissingStub: null, - ); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); } diff --git a/test/ui/widgets/artist_row_test.dart b/test/ui/widgets/artist_row_test.dart deleted file mode 100644 index 5d408e1b..00000000 --- a/test/ui/widgets/artist_row_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:app/models/artist.dart'; -import 'package:app/providers/artist_provider.dart'; -import 'package:app/ui/screens/artists.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:provider/provider.dart'; - -import '../../extensions/widget_tester_extension.dart'; -import 'artist_card_test.mocks.dart'; - -void main() { - Future mountWithProvider(WidgetTester tester, Artist artist) async { - await tester.pumpAppWidget( - ChangeNotifierProvider.value( - value: MockArtistProvider(), - child: ArtistRow(artist: artist, router: MockAppRouter()), - ), - ); - } - - testWidgets('shows Edit on long-press when canEdit', (tester) async { - final artist = Artist.fake(name: 'Editable', canEdit: true); - await mountWithProvider(tester, artist); - - await tester.longPress(find.byType(ArtistRow)); - await tester.pumpAndSettle(); - - expect(find.text('Edit'), findsOneWidget); - }); - - testWidgets('no menu on long-press when canEdit is false', (tester) async { - final artist = Artist.fake(name: 'Read-only'); - await mountWithProvider(tester, artist); - - await tester.longPress(find.byType(ArtistRow)); - await tester.pumpAndSettle(); - - expect(find.text('Edit'), findsNothing); - }); -}