Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/models/artist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -51,6 +55,7 @@ class Artist {
name: json['name'],
imageUrl: json['image'],
canEdit: permissions is Map ? permissions['edit'] == true : false,
favorite: json['favorite'] == true,
);
}

Expand All @@ -60,6 +65,7 @@ class Artist {
String? imageUrl,
int? playCount,
bool canEdit = false,
bool favorite = false,
}) {
Faker faker = Faker();

Expand All @@ -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);
}

Expand All @@ -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;

Expand Down
17 changes: 17 additions & 0 deletions lib/providers/artist_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ class ArtistProvider with ChangeNotifier, StreamSubscriber {
return paginate();
}

Future<void> 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<void> update(Artist artist, {required String name}) async {
final response = await put('artists/${artist.id}', data: {
'name': name,
Expand Down
215 changes: 215 additions & 0 deletions lib/ui/screens/artist_action_sheet.dart
Original file line number Diff line number Diff line change
@@ -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<ArtistActionSheet> createState() => _ArtistActionSheetState();
}

class _ArtistActionSheetState extends State<ArtistActionSheet> {
Future<List<Playable>> _fetchSongs() {
return context.read<PlayableProvider>().fetchForArtist(widget.artist.id);
}

@override
Widget build(BuildContext context) {
final artist = widget.artist;
final artistProvider = context.read<ArtistProvider>();
// 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: <Widget>[
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((_) {});
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
),
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: <Widget>[
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<void> showArtistActionSheet(
BuildContext context, {
required Artist artist,
}) {
return showModalBottomSheet<void>(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (_) => ArtistActionSheet(artist: artist),
);
}
13 changes: 2 additions & 11 deletions lib/ui/screens/artists.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -202,8 +203,6 @@ class ArtistRow extends StatefulWidget {
}

class _ArtistRowState extends State<ArtistRow> {
Offset? _lastTapPosition;

@override
Widget build(BuildContext context) {
final artist = widget.artist;
Expand All @@ -214,15 +213,7 @@ class _ArtistRowState extends State<ArtistRow> {
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),
Expand Down
1 change: 1 addition & 0 deletions lib/ui/screens/screens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
46 changes: 0 additions & 46 deletions lib/ui/widgets/artist_actions_menu.dart

This file was deleted.

Loading
Loading