Skip to content

Commit

Permalink
Merge pull request #720 from Komodo5197/redesign-playlist-remove
Browse files Browse the repository at this point in the history
[Redesign] Playlist removal from queue
  • Loading branch information
Chaphasilor authored May 14, 2024
2 parents 641dd8f + 21ca257 commit 955229e
Show file tree
Hide file tree
Showing 57 changed files with 3,149 additions and 1,891 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ jobs:
- run: flutter pub get
- run: flutter gen-l10n
# - run: flutter test
- run: flutter build windows
# TODO pack in redistributables?
- uses: actions/upload-artifact@v4
with:
name: finamp-windows-zip
path: build/windows/x64/runner/Release/
- run: dart run msix:create --install-certificate false
#TODO would be nice to have an old-school installer here that can take the .exe + libraries and install them to the device + create a shortcut
- uses: actions/upload-artifact@v4
Expand Down
75 changes: 75 additions & 0 deletions lib/components/AddToPlaylistScreen/add_to_playlist_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import 'package:Finamp/components/PlayerScreen/queue_source_helper.dart';
import 'package:Finamp/components/global_snackbar.dart';
import 'package:Finamp/models/finamp_models.dart';
import 'package:Finamp/models/jellyfin_models.dart';
import 'package:Finamp/services/favorite_provider.dart';
import 'package:Finamp/services/finamp_settings_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'playlist_actions_menu.dart';

class AddToPlaylistButton extends ConsumerStatefulWidget {
const AddToPlaylistButton({
super.key,
required this.item,
this.queueItem,
this.color,
this.size,
this.visualDensity,
});

final BaseItemDto? item;
final FinampQueueItem? queueItem;
final Color? color;
final double? size;
final VisualDensity? visualDensity;

@override
ConsumerState<AddToPlaylistButton> createState() =>
_AddToPlaylistButtonState();
}

class _AddToPlaylistButtonState extends ConsumerState<AddToPlaylistButton> {
@override
Widget build(BuildContext context) {
if (widget.item == null) {
return const SizedBox.shrink();
}

bool isFav = ref
.watch(isFavoriteProvider(widget.item?.id, DefaultValue(widget.item)));
return GestureDetector(
onLongPress: () async {
ref
.read(isFavoriteProvider(widget.item?.id, DefaultValue()).notifier)
.updateFavorite(!isFav);
},
child: IconButton(
icon: Icon(
isFav ? Icons.favorite : Icons.favorite_outline,
size: widget.size ?? 24.0,
),
color: widget.color ?? IconTheme.of(context).color,
disabledColor:
(widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3),
visualDensity: widget.visualDensity ?? VisualDensity.compact,
// tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip,
onPressed: () async {
if (FinampSettingsHelper.finampSettings.isOffline) {
return GlobalSnackbar.message((context) =>
AppLocalizations.of(context)!.notAvailableInOfflineMode);
}

bool inPlaylist = queueItemInPlaylist(widget.queueItem);
await showPlaylistActionsMenu(
context: context,
item: widget.item!,
parentPlaylist: inPlaylist ? widget.queueItem!.source.item : null,
usePlayerTheme: true,
);
}),
);
}
}
257 changes: 201 additions & 56 deletions lib/components/AddToPlaylistScreen/add_to_playlist_list.dart
Original file line number Diff line number Diff line change
@@ -1,93 +1,238 @@
import 'dart:async';

import 'package:Finamp/models/finamp_models.dart';
import 'package:Finamp/services/downloads_service.dart';
import 'package:collection/collection.dart';
import 'package:Finamp/components/Buttons/cta_medium.dart';
import 'package:Finamp/components/PlayerScreen/queue_source_helper.dart';
import 'package:Finamp/components/album_image.dart';
import 'package:Finamp/services/finamp_settings_helper.dart';
import 'package:Finamp/services/jellyfin_api_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
import 'package:get_it/get_it.dart';

import '../../models/jellyfin_models.dart';
import '../../services/jellyfin_api_helper.dart';
import '../MusicScreen/album_item.dart';
import '../global_snackbar.dart';
import 'new_playlist_dialog.dart';
import 'playlist_actions_menu.dart';

class AddToPlaylistList extends StatefulWidget {
const AddToPlaylistList({
Key? key,
required this.itemToAddId,
}) : super(key: key);
super.key,
required this.itemToAdd,
required this.playlistsFuture,
});

final String itemToAddId;
final BaseItemDto itemToAdd;
final Future<List<BaseItemDto>> playlistsFuture;

@override
State<AddToPlaylistList> createState() => _AddToPlaylistListState();
}

class _AddToPlaylistListState extends State<AddToPlaylistList> {
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
late Future<List<BaseItemDto>?> addToPlaylistListFuture;

@override
void initState() {
super.initState();
addToPlaylistListFuture = jellyfinApiHelper.getItems(
includeItemTypes: "Playlist",
sortBy: "SortName",
);
playlistsFuture = widget.playlistsFuture.then(
(value) => value.map((e) => (e, false, null as String?)).toList());
}

// playlist, isLoading, playlistItemId
late Future<List<(BaseItemDto, bool, String?)>> playlistsFuture;

@override
Widget build(BuildContext context) {
return FutureBuilder<List<BaseItemDto>?>(
future: addToPlaylistListFuture,
return FutureBuilder(
future: playlistsFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return AlbumItem(
album: snapshot.data![index],
parentType: snapshot.data![index].type,
isPlaylist: true,
onTap: () async {
try {
await jellyfinApiHelper.addItemstoPlaylist(
playlistId: snapshot.data![index].id,
ids: [widget.itemToAddId],
);
final downloadsService = GetIt.instance<DownloadsService>();
unawaited(downloadsService.resync(
DownloadStub.fromItem(
type: DownloadItemType.collection,
item: snapshot.data![index]),
null,
keepSlow: true));

if (!context.mounted) return;
GlobalSnackbar.message(
(scaffold) => AppLocalizations.of(context)!
.confirmAddedToPlaylist,
isConfirmation: true);
Navigator.pop(context);
} catch (e) {
GlobalSnackbar.error(e);
return;
}
},
);
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == snapshot.data!.length) {
return createNewPlaylistButton(context);
}
final (playlist, isLoading, playListItemId) =
snapshot.data![index];
return AddToPlaylistTile(
playlist: playlist,
song: widget.itemToAdd,
playlistItemId: playListItemId,
isLoading: isLoading);
},
);
childCount: snapshot.data!.length + 1,
));
} else if (snapshot.hasError) {
GlobalSnackbar.error(snapshot.error);
return const Center(
child: Icon(Icons.error, size: 64),
return const SliverToBoxAdapter(
child: Center(
heightFactor: 3.0,
child: Icon(Icons.error, size: 64),
),
);
} else {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == 1) {
return createNewPlaylistButton(context);
} else {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
}, childCount: 2));
}
},
);
}

Widget createNewPlaylistButton(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CTAMedium(
text: AppLocalizations.of(context)!.newPlaylist,
icon: TablerIcons.plus,
//accentColor: Theme.of(context).colorScheme.primary,
onPressed: () async {
var dialogResult = await showDialog<(Future<String>, String?)?>(
context: context,
builder: (context) =>
NewPlaylistDialog(itemToAdd: widget.itemToAdd.id),
);
if (dialogResult != null) {
var oldFuture = playlistsFuture;
setState(() {
var loadingItem = [
(
BaseItemDto(id: "pending", name: dialogResult.$2),
true,
null as String?
)
];
playlistsFuture =
oldFuture.then((value) => value + loadingItem);
});
try {
var newId = await dialogResult.$1;
// Give the server time to calculate an initial playlist image
await Future.delayed(const Duration(seconds: 1));
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
var playlist = await jellyfinApiHelper.getItemById(newId);
var playlistItems = await jellyfinApiHelper.getItems(
parentItem: playlist, fields: "");
var song = playlistItems?.firstWhere(
(element) => element.id == widget.itemToAdd.id);
setState(() {
var newItem = [(playlist, false, song?.playlistItemId)];
playlistsFuture =
oldFuture.then((value) => value + newItem);
});
} catch (e) {
GlobalSnackbar.error(e);
}
}
},
),
],
),
);
}
}

class AddToPlaylistTile extends StatefulWidget {
const AddToPlaylistTile(
{super.key,
required this.playlist,
this.playlistItemId,
required this.song,
this.isLoading = false});

final BaseItemDto playlist;
final BaseItemDto song;
final String? playlistItemId;
final bool isLoading;

@override
State<AddToPlaylistTile> createState() => _AddToPlaylistTileState();
}

class _AddToPlaylistTileState extends State<AddToPlaylistTile> {
String? playlistItemId;
int? childCount;
bool knownMissing = false;

@override
void initState() {
super.initState();
if (!widget.isLoading) {
playlistItemId = widget.playlistItemId;
childCount = widget.playlist.childCount;
}
}

@override
void didUpdateWidget(AddToPlaylistTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.isLoading) {
playlistItemId = widget.playlistItemId;
childCount = widget.playlist.childCount;
}
}

@override
Widget build(BuildContext context) {
final isOffline = FinampSettingsHelper.finampSettings.isOffline;
return ToggleableListTile(
forceLoading: widget.isLoading,
title: widget.playlist.name ?? AppLocalizations.of(context)!.unknownName,
subtitle: AppLocalizations.of(context)!.songCount(childCount ?? 0),
leading: AlbumImage(item: widget.playlist),
positiveIcon: TablerIcons.circle_check_filled,
negativeIcon: knownMissing
? TablerIcons.circle_plus
// we don't actually know if the track is part of the playlist
: TablerIcons.circle_dashed_plus,
initialState: playlistItemId != null,
onToggle: (bool currentState) async {
if (currentState) {
if (playlistItemId == null) {
throw "Cannot remove item from playlist, missing playlistItemId";
}
// part of playlist, remove
bool removed = await removeFromPlaylist(
context, widget.song, widget.playlist, playlistItemId!,
confirm: false);
if (removed) {
setState(() {
childCount = childCount == null ? null : childCount! - 1;
knownMissing = true;
});
}
return !removed;
} else {
// add to playlist
bool added =
await addItemToPlaylist(context, widget.song, widget.playlist);
if (added) {
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
var newItems = await jellyfinApiHelper.getItems(
parentItem: widget.playlist, fields: "");
setState(() {
childCount = newItems?.length ?? 0;
playlistItemId = newItems
?.firstWhereOrNull((x) => x.id == widget.song.id)
?.playlistItemId;
});
return playlistItemId != null;
}
return false;
}
},
enabled: !isOffline,
);
}
}
Loading

0 comments on commit 955229e

Please sign in to comment.