diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index 955dfa0ac..d8058a794 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -32,8 +32,10 @@ class DownloadButton extends ConsumerWidget { .select((value) => value.valueOrNull?.isOffline)) ?? true; String? parentTooltip; - if (status == DownloadItemStatus.incidental || - status == DownloadItemStatus.incidentalOutdated) { + if (status == null) { + return const SizedBox.shrink(); + } + if (status.isIncidental) { var parent = downloadsService.getFirstRequiringItem(item); if (parent != null) { var parentName = AppLocalizations.of(context)! @@ -42,9 +44,6 @@ class DownloadButton extends ConsumerWidget { AppLocalizations.of(context)!.incidentalDownloadTooltip(parentName); } } - if (status == null) { - return const SizedBox.shrink(); - } String viewId; if (isLibrary) { viewId = item.id; diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 6c184bbc8..62f1c7885 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -148,7 +148,7 @@ class _SongMenuState extends State { final currentTrack = _queueService.getCurrentTrack(); if (isBaseItemInQueueItem(widget.item, currentTrack)) { - setFavourite(currentTrack!, context); + await setFavourite(currentTrack!, context); Vibrate.feedback(FeedbackType.success); return; } @@ -187,563 +187,567 @@ class _SongMenuState extends State { Theme.of(context).iconTheme.color ?? Colors.white; - final downloadsService = GetIt.instance(); - final bool isDownloadRequired = downloadsService - .getStatus( - DownloadStub.fromItem( - type: DownloadItemType.song, item: widget.item), - null) - .isRequired; + final menuEntries = _menuEntries(context, iconColor); + var stackHeight = widget.showPlaybackControls ? 255 : 155; + stackHeight += menuEntries + .where((element) => + switch (element) { Visibility e => e.visible, _ => true }) + .length * + 56; return Stack(children: [ - DraggableScrollableSheet( - snap: true, - initialChildSize: 0.9, - maxChildSize: 0.9, - minChildSize: 0.3, - expand: false, - builder: (context, scrollController) { - return Stack( - children: [ - if (FinampSettingsHelper - .finampSettings.showCoverAsPlayerBackground) - BlurredPlayerScreenBackground( - customImageProvider: _imageProvider, - opacityFactor: - Theme.of(context).brightness == Brightness.dark - ? 1.0 - : 1.0), - CustomScrollView( - controller: scrollController, - physics: const ClampingScrollPhysics(), - slivers: [ - SliverPersistentHeader( - delegate: SongMenuSliverAppBar( - item: widget.item, - theme: _imageTheme, - imageProviderCallback: (ImageProvider provider) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _imageProvider = provider; - }); - } - }); - }, - imageThemeCallback: (ColorScheme colorScheme) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _imageTheme = colorScheme; - }); - } - }); - }, + LayoutBuilder(builder: (context, constraints) { + var size = (stackHeight / constraints.maxHeight).clamp(0.4, 1.0); + return DraggableScrollableSheet( + snap: true, + initialChildSize: size, + minChildSize: size * 0.9, + expand: false, + builder: (context, scrollController) { + return Stack( + children: [ + if (FinampSettingsHelper + .finampSettings.showCoverAsPlayerBackground) + BlurredPlayerScreenBackground( + customImageProvider: _imageProvider, + opacityFactor: + Theme.of(context).brightness == Brightness.dark + ? 1.0 + : 1.0), + CustomScrollView( + controller: scrollController, + physics: const ClampingScrollPhysics(), + slivers: [ + SliverPersistentHeader( + delegate: SongMenuSliverAppBar( + item: widget.item, + theme: _imageTheme, + imageProviderCallback: (ImageProvider provider) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _imageProvider = provider; + }); + } + }); + }, + imageThemeCallback: (ColorScheme colorScheme) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _imageTheme = colorScheme; + }); + } + }); + }, + ), + pinned: true, ), - pinned: true, - ), - if (widget.showPlaybackControls) - SongMenuMask( - child: StreamBuilder( - stream: Rx.combineLatest2( - _queueService.getPlaybackOrderStream(), - _queueService.getLoopModeStream(), - (a, b) => PlaybackBehaviorInfo(a, b)), - builder: (context, snapshot) { - if (!snapshot.hasData) - return const SliverToBoxAdapter(); - - final playbackBehavior = snapshot.data!; - const playbackOrderIcons = { - FinampPlaybackOrder.linear: TablerIcons.arrows_right, - FinampPlaybackOrder.shuffled: - TablerIcons.arrows_shuffle, - }; - final playbackOrderTooltips = { - FinampPlaybackOrder.linear: - AppLocalizations.of(context) - ?.playbackOrderLinearButtonLabel ?? - "Playing in order", - FinampPlaybackOrder.shuffled: - AppLocalizations.of(context) - ?.playbackOrderShuffledButtonLabel ?? - "Shuffling", - }; - const loopModeIcons = { - FinampLoopMode.none: TablerIcons.repeat, - FinampLoopMode.one: TablerIcons.repeat_once, - FinampLoopMode.all: TablerIcons.repeat, - }; - final loopModeTooltips = { - FinampLoopMode.none: AppLocalizations.of(context) - ?.loopModeNoneButtonLabel ?? - "Looping off", - FinampLoopMode.one: AppLocalizations.of(context) - ?.loopModeOneButtonLabel ?? - "Looping this song", - FinampLoopMode.all: AppLocalizations.of(context) - ?.loopModeAllButtonLabel ?? - "Looping all", - }; - - return SliverCrossAxisGroup( - // return SliverGrid.count( - // crossAxisCount: 3, - // mainAxisSpacing: 40, - // children: [ - slivers: [ - PlaybackAction( - icon: playbackOrderIcons[playbackBehavior.order]!, - onPressed: () async { - _queueService.togglePlaybackOrder(); - }, - tooltip: playbackOrderTooltips[ - playbackBehavior.order]!, - iconColor: playbackBehavior.order == - FinampPlaybackOrder.shuffled - ? iconColor - : Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - ), - ValueListenableBuilder( - valueListenable: _audioHandler.sleepTimer, - builder: (context, timerValue, child) { - final remainingMinutes = (_audioHandler - .sleepTimerRemaining.inSeconds / - 60.0) - .ceil(); - return PlaybackAction( - icon: timerValue != null - ? TablerIcons.hourglass_high - : TablerIcons.hourglass_empty, - onPressed: () async { - if (timerValue != null) { - showDialog( - context: context, - builder: (context) => - const SleepTimerCancelDialog(), - ); - } else { - await showDialog( - context: context, - builder: (context) => - const SleepTimerDialog(), - ); - } - }, - tooltip: timerValue != null - ? AppLocalizations.of(context) - ?.sleepTimerRemainingTime( - remainingMinutes) ?? - "Sleeping in $remainingMinutes minutes" - : AppLocalizations.of(context)! - .sleepTimerTooltip, - iconColor: timerValue != null - ? iconColor - : Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - ); - }, - ), - PlaybackAction( - icon: loopModeIcons[playbackBehavior.loop]!, - onPressed: () async { - _queueService.toggleLoopMode(); - }, - tooltip: loopModeTooltips[playbackBehavior.loop]!, - iconColor: - playbackBehavior.loop == FinampLoopMode.none - ? Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white - : iconColor, - ), - ], - ); - }, - )), - SongMenuMask( - child: SliverPadding( - padding: const EdgeInsets.only(left: 8.0), - sliver: SliverList( - delegate: SliverChildListDelegate([ - Visibility( - visible: !widget.isOffline, - child: ListTile( - leading: Icon( - Icons.playlist_add, - color: iconColor, - ), - title: Text(AppLocalizations.of(context)! - .addToPlaylistTitle), - enabled: !widget.isOffline, - onTap: () { - Navigator.pop(context); // close menu - Navigator.of(context).pushNamed( - AddToPlaylistScreen.routeName, - arguments: widget.item.id); - }, - ), - ), - Visibility( - visible: _queueService.getQueue().nextUp.isNotEmpty, - child: ListTile( - leading: Icon( - TablerIcons.corner_right_down, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.playNext), - onTap: () async { - await _queueService.addNext( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.nextUp, - name: const QueueItemSourceName( - type: - QueueItemSourceNameType.nextUp), - id: widget.item.id)); - - if (!mounted) return; - - GlobalSnackbar.message( - (context) => AppLocalizations.of(context)! - .confirmPlayNext("track"), - isConfirmation: true); - Navigator.pop(context); - }, - ), - ), - ListTile( - leading: Icon( - TablerIcons.corner_right_down_double, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.addToNextUp), - onTap: () async { - await _queueService.addToNextUp( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.nextUp, - name: const QueueItemSourceName( - type: QueueItemSourceNameType.nextUp), - id: widget.item.id)); - - if (!mounted) return; - - GlobalSnackbar.message( - (context) => AppLocalizations.of(context)! - .confirmAddToNextUp("track"), - isConfirmation: true); - Navigator.pop(context); - }, - ), - ListTile( - leading: Icon( - TablerIcons.playlist, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.addToQueue), - onTap: () async { - await _queueService.addToQueue( - items: [widget.item], - source: QueueItemSource( - type: QueueItemSourceType.queue, - name: const QueueItemSourceName( - type: QueueItemSourceNameType.queue), - id: widget.item.id)); - - if (!mounted) return; - - GlobalSnackbar.message( - (context) => AppLocalizations.of(context)! - .addedToQueue, - isConfirmation: true); - Navigator.pop(context); - }, - ), - Visibility( - visible: widget.isInPlaylist && - widget.parentItem != null && - !widget.isOffline, - child: ListTile( - leading: Icon( - Icons.playlist_remove, - color: iconColor, - ), - title: Text(AppLocalizations.of(context)! - .removeFromPlaylistTitle), - enabled: widget.isInPlaylist && - widget.parentItem != null && - !widget.isOffline, - onTap: () async { - try { - await _jellyfinApiHelper - .removeItemsFromPlaylist( - playlistId: widget.parentItem!.id, - entryIds: [ - widget.item.playlistItemId! - ]); - - if (!mounted) return; - - await _jellyfinApiHelper.getItems( - parentItem: await _jellyfinApiHelper - .getItemById(widget.item.parentId!), - sortBy: - "ParentIndexNumber,IndexNumber,SortName", - includeItemTypes: "Audio", - ); + if (widget.showPlaybackControls) + SongMenuMask( + child: StreamBuilder( + stream: Rx.combineLatest2( + _queueService.getPlaybackOrderStream(), + _queueService.getLoopModeStream(), + (a, b) => PlaybackBehaviorInfo(a, b)), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SliverToBoxAdapter(); + } - if (!mounted) return; - - if (widget.onRemoveFromList != null) - widget.onRemoveFromList!(); - - GlobalSnackbar.message( - (context) => AppLocalizations.of(context)! - .removedFromPlaylist, - isConfirmation: true); - Navigator.pop(context); - } catch (e) { - GlobalSnackbar.error(e); - } - }, - ), - ), - Visibility( - visible: !widget.isOffline, - child: ListTile( - leading: Icon( - Icons.explore, - color: iconColor, - ), - title: Text( - AppLocalizations.of(context)!.instantMix), - enabled: !widget.isOffline, - onTap: () async { - await _audioServiceHelper - .startInstantMixForItem(widget.item); - - if (!mounted) return; - - GlobalSnackbar.message( - (context) => AppLocalizations.of(context)! - .startingInstantMix, - isConfirmation: true); - Navigator.pop(context); - }, - ), - ), - Visibility( - visible: isDownloadRequired, - // TODO add some sort of disabled state with tooltip saying to delete the parent - // Need to do on other delete buttons too - // Do we want to try showing lock on right clicks? - // Currently only download or delete are shown. - child: ListTile( - leading: Icon( - Icons.delete_outlined, - color: iconColor, + final playbackBehavior = snapshot.data!; + const playbackOrderIcons = { + FinampPlaybackOrder.linear: + TablerIcons.arrows_right, + FinampPlaybackOrder.shuffled: + TablerIcons.arrows_shuffle, + }; + final playbackOrderTooltips = { + FinampPlaybackOrder.linear: + AppLocalizations.of(context) + ?.playbackOrderLinearButtonLabel ?? + "Playing in order", + FinampPlaybackOrder.shuffled: + AppLocalizations.of(context) + ?.playbackOrderShuffledButtonLabel ?? + "Shuffling", + }; + const loopModeIcons = { + FinampLoopMode.none: TablerIcons.repeat, + FinampLoopMode.one: TablerIcons.repeat_once, + FinampLoopMode.all: TablerIcons.repeat, + }; + final loopModeTooltips = { + FinampLoopMode.none: AppLocalizations.of(context) + ?.loopModeNoneButtonLabel ?? + "Looping off", + FinampLoopMode.one: AppLocalizations.of(context) + ?.loopModeOneButtonLabel ?? + "Looping this song", + FinampLoopMode.all: AppLocalizations.of(context) + ?.loopModeAllButtonLabel ?? + "Looping all", + }; + + return SliverCrossAxisGroup( + // return SliverGrid.count( + // crossAxisCount: 3, + // mainAxisSpacing: 40, + // children: [ + slivers: [ + PlaybackAction( + icon: + playbackOrderIcons[playbackBehavior.order]!, + onPressed: () async { + _queueService.togglePlaybackOrder(); + }, + tooltip: playbackOrderTooltips[ + playbackBehavior.order]!, + iconColor: playbackBehavior.order == + FinampPlaybackOrder.shuffled + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, ), - title: Text( - AppLocalizations.of(context)!.deleteItem), - enabled: !widget.isOffline && isDownloadRequired, - onTap: () async { - var item = DownloadStub.fromItem( - type: DownloadItemType.song, - item: widget.item); - unawaited(downloadsService.deleteDownload( - stub: item)); - if (mounted) { - Navigator.pop(context); - } - }, - ), - ), - Visibility( - visible: !widget.isOffline && !isDownloadRequired, - child: ListTile( - leading: Icon( - Icons.file_download_outlined, - color: iconColor, + ValueListenableBuilder( + valueListenable: _audioHandler.sleepTimer, + builder: (context, timerValue, child) { + final remainingMinutes = (_audioHandler + .sleepTimerRemaining.inSeconds / + 60.0) + .ceil(); + return PlaybackAction( + icon: timerValue != null + ? TablerIcons.hourglass_high + : TablerIcons.hourglass_empty, + onPressed: () async { + if (timerValue != null) { + await showDialog( + context: context, + builder: (context) => + const SleepTimerCancelDialog(), + ); + } else { + await showDialog( + context: context, + builder: (context) => + const SleepTimerDialog(), + ); + } + }, + tooltip: timerValue != null + ? AppLocalizations.of(context) + ?.sleepTimerRemainingTime( + remainingMinutes) ?? + "Sleeping in $remainingMinutes minutes" + : AppLocalizations.of(context)! + .sleepTimerTooltip, + iconColor: timerValue != null + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, + ); + }, ), - title: Text( - AppLocalizations.of(context)!.downloadItem), - enabled: !widget.isOffline && !isDownloadRequired, - onTap: () async { - var item = DownloadStub.fromItem( - type: DownloadItemType.song, - item: widget.item); - await DownloadDialog.show(context, item, null); - if (mounted) { - Navigator.pop(context); - } - }, - ), - ), - ListTile( - enabled: !widget.isOffline, - leading: widget.item.userData!.isFavorite - ? Icon( - Icons.favorite, - color: widget.isOffline - ? iconColor.withOpacity(0.3) - : iconColor, - ) - : Icon( - Icons.favorite_border, - color: widget.isOffline - ? iconColor.withOpacity(0.3) + PlaybackAction( + icon: loopModeIcons[playbackBehavior.loop]!, + onPressed: () async { + _queueService.toggleLoopMode(); + }, + tooltip: + loopModeTooltips[playbackBehavior.loop]!, + iconColor: + playbackBehavior.loop == FinampLoopMode.none + ? Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white : iconColor, - ), - title: Text(widget.item.userData!.isFavorite - ? AppLocalizations.of(context)!.removeFavourite - : AppLocalizations.of(context)!.addFavourite), - onTap: () async { - await toggleFavorite(); - if (mounted) Navigator.pop(context); - }, - ), - Visibility( - visible: widget.canGoToAlbum, - child: ListTile( - leading: Icon( - Icons.album, - color: iconColor, ), - title: - Text(AppLocalizations.of(context)!.goToAlbum), - enabled: widget.canGoToAlbum, - onTap: () async { - late BaseItemDto album; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - album = (await downloadsService - .getCollectionInfo( - id: widget.item.albumId!))! - .baseItem!; - } else { - album = await _jellyfinApiHelper - .getItemById(widget.item.albumId!); - } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - AlbumScreen.routeName, - arguments: album); - } - }, - ), - ), - Visibility( - visible: widget.canGoToArtist, - child: ListTile( - leading: Icon( - Icons.person, - color: iconColor, - ), - title: Text( - AppLocalizations.of(context)!.goToArtist), - enabled: widget.canGoToArtist, - onTap: () async { - late BaseItemDto artist; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - artist = (await downloadsService - .getCollectionInfo( - id: widget.item.artistItems! - .first.id))! - .baseItem!; - } else { - artist = - await _jellyfinApiHelper.getItemById( - widget.item.artistItems!.first.id); - } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - ArtistScreen.routeName, - arguments: artist); - } - }, - ), - ), - Visibility( - visible: widget.canGoToGenre, - child: ListTile( - leading: Icon( - Icons.category_outlined, - color: iconColor, - ), - title: - Text(AppLocalizations.of(context)!.goToGenre), - enabled: widget.canGoToGenre, - onTap: () async { - late BaseItemDto genre; - try { - if (FinampSettingsHelper - .finampSettings.isOffline) { - final downloadsService = - GetIt.instance(); - genre = (await downloadsService - .getCollectionInfo( - id: widget.item.genreItems! - .first.id))! - .baseItem!; - } else { - genre = - await _jellyfinApiHelper.getItemById( - widget.item.genreItems!.first.id); - } - } catch (e) { - GlobalSnackbar.error(e); - return; - } - if (mounted) { - Navigator.pop(context); - Navigator.of(context).pushNamed( - ArtistScreen.routeName, - arguments: genre); - } - }, - ), - ), - ]), + ], + ); + }, + )), + SongMenuMask( + child: SliverPadding( + padding: const EdgeInsets.only(left: 8.0), + sliver: SliverList( + delegate: SliverChildListDelegate(menuEntries), + ), ), - ), - ) - ], + ) + ], + ), + ], + ); + }, + ); + }), + ]); + } + + List _menuEntries(BuildContext context, Color iconColor) { + final downloadsService = GetIt.instance(); + final downloadStatus = downloadsService.getStatus( + DownloadStub.fromItem(type: DownloadItemType.song, item: widget.item), + null); + + String? parentTooltip; + if (downloadStatus.isIncidental) { + var parent = downloadsService.getFirstRequiringItem(DownloadStub.fromItem( + type: DownloadItemType.song, item: widget.item)); + if (parent != null) { + var parentName = AppLocalizations.of(context)! + .itemTypeSubtitle(parent.baseItemType.name, parent.name); + parentTooltip = + AppLocalizations.of(context)!.incidentalDownloadTooltip(parentName); + } + } + + return [ + Visibility( + visible: !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.playlist_add, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.addToPlaylistTitle), + enabled: !widget.isOffline, + onTap: () { + Navigator.pop(context); // close menu + Navigator.of(context).pushNamed(AddToPlaylistScreen.routeName, + arguments: widget.item.id); + }, + ), + ), + Visibility( + visible: _queueService.getQueue().nextUp.isNotEmpty, + child: ListTile( + leading: Icon( + TablerIcons.corner_right_down, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.playNext), + onTap: () async { + await _queueService.addNext( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.nextUp, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.nextUp), + id: widget.item.id)); + + if (!context.mounted) return; + + GlobalSnackbar.message( + (context) => + AppLocalizations.of(context)!.confirmPlayNext("track"), + isConfirmation: true); + Navigator.pop(context); + }, + ), + ), + ListTile( + leading: Icon( + TablerIcons.corner_right_down_double, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.addToNextUp), + onTap: () async { + await _queueService.addToNextUp( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.nextUp, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.nextUp), + id: widget.item.id)); + + if (!context.mounted) return; + + GlobalSnackbar.message( + (context) => + AppLocalizations.of(context)!.confirmAddToNextUp("track"), + isConfirmation: true); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon( + TablerIcons.playlist, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.addToQueue), + onTap: () async { + await _queueService.addToQueue( + items: [widget.item], + source: QueueItemSource( + type: QueueItemSourceType.queue, + name: const QueueItemSourceName( + type: QueueItemSourceNameType.queue), + id: widget.item.id)); + + if (!context.mounted) return; + + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)!.addedToQueue, + isConfirmation: true); + Navigator.pop(context); + }, + ), + Visibility( + visible: widget.isInPlaylist && + widget.parentItem != null && + !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.playlist_remove, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.removeFromPlaylistTitle), + enabled: widget.isInPlaylist && + widget.parentItem != null && + !widget.isOffline, + onTap: () async { + try { + await _jellyfinApiHelper.removeItemsFromPlaylist( + playlistId: widget.parentItem!.id, + entryIds: [widget.item.playlistItemId!]); + + if (!context.mounted) return; + + await _jellyfinApiHelper.getItems( + parentItem: + await _jellyfinApiHelper.getItemById(widget.item.parentId!), + sortBy: "ParentIndexNumber,IndexNumber,SortName", + includeItemTypes: "Audio", + ); + + if (!context.mounted) return; + + if (widget.onRemoveFromList != null) widget.onRemoveFromList!(); + + GlobalSnackbar.message( + (context) => + AppLocalizations.of(context)!.removedFromPlaylist, + isConfirmation: true); + Navigator.pop(context); + } catch (e) { + GlobalSnackbar.error(e); + } + }, + ), + ), + Visibility( + visible: !widget.isOffline, + child: ListTile( + leading: Icon( + Icons.explore, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.instantMix), + enabled: !widget.isOffline, + onTap: () async { + await _audioServiceHelper.startInstantMixForItem(widget.item); + + if (!context.mounted) return; + + GlobalSnackbar.message( + (context) => AppLocalizations.of(context)!.startingInstantMix, + isConfirmation: true); + Navigator.pop(context); + }, + ), + ), + Visibility( + visible: downloadStatus.isRequired, + child: ListTile( + leading: Icon( + Icons.delete_outlined, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.deleteItem), + enabled: downloadStatus.isRequired, + onTap: () async { + var item = DownloadStub.fromItem( + type: DownloadItemType.song, item: widget.item); + unawaited(downloadsService.deleteDownload(stub: item)); + if (mounted) { + Navigator.pop(context); + } + }, + ), + ), + Visibility( + visible: downloadStatus == DownloadItemStatus.notNeeded, + child: ListTile( + leading: Icon( + Icons.file_download_outlined, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.downloadItem), + enabled: !widget.isOffline && + downloadStatus == DownloadItemStatus.notNeeded, + onTap: () async { + var item = DownloadStub.fromItem( + type: DownloadItemType.song, item: widget.item); + await DownloadDialog.show(context, item, null); + if (context.mounted) { + Navigator.pop(context); + } + }, + ), + ), + Visibility( + visible: downloadStatus.isIncidental, + child: Tooltip( + message: parentTooltip ?? "Widget shouldn't be visible", + child: ListTile( + leading: Icon( + Icons.lock_outlined, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.lockDownload), + enabled: !widget.isOffline && downloadStatus.isIncidental, + onTap: () async { + var item = DownloadStub.fromItem( + type: DownloadItemType.song, item: widget.item); + await DownloadDialog.show(context, item, null); + if (context.mounted) { + Navigator.pop(context); + } + }, + ), + ), + ), + ListTile( + enabled: !widget.isOffline, + leading: widget.item.userData!.isFavorite + ? Icon( + Icons.favorite, + color: + widget.isOffline ? iconColor.withOpacity(0.3) : iconColor, + ) + : Icon( + Icons.favorite_border, + color: + widget.isOffline ? iconColor.withOpacity(0.3) : iconColor, ), - ], - ); + title: Text(widget.item.userData!.isFavorite + ? AppLocalizations.of(context)!.removeFavourite + : AppLocalizations.of(context)!.addFavourite), + onTap: () async { + await toggleFavorite(); + if (context.mounted) Navigator.pop(context); }, ), - ]); + Visibility( + visible: widget.canGoToAlbum, + child: ListTile( + leading: Icon( + Icons.album, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.goToAlbum), + enabled: widget.canGoToAlbum, + onTap: () async { + late BaseItemDto album; + try { + if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsService = GetIt.instance(); + album = (await downloadsService.getCollectionInfo( + id: widget.item.albumId!))! + .baseItem!; + } else { + album = + await _jellyfinApiHelper.getItemById(widget.item.albumId!); + } + } catch (e) { + GlobalSnackbar.error(e); + return; + } + if (context.mounted) { + Navigator.pop(context); + await Navigator.of(context) + .pushNamed(AlbumScreen.routeName, arguments: album); + } + }, + ), + ), + Visibility( + visible: widget.canGoToArtist, + child: ListTile( + leading: Icon( + Icons.person, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.goToArtist), + enabled: widget.canGoToArtist, + onTap: () async { + late BaseItemDto artist; + try { + if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsService = GetIt.instance(); + artist = (await downloadsService.getCollectionInfo( + id: widget.item.artistItems!.first.id))! + .baseItem!; + } else { + artist = await _jellyfinApiHelper + .getItemById(widget.item.artistItems!.first.id); + } + } catch (e) { + GlobalSnackbar.error(e); + return; + } + if (context.mounted) { + Navigator.pop(context); + await Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: artist); + } + }, + ), + ), + Visibility( + visible: widget.canGoToGenre, + child: ListTile( + leading: Icon( + Icons.category_outlined, + color: iconColor, + ), + title: Text(AppLocalizations.of(context)!.goToGenre), + enabled: widget.canGoToGenre, + onTap: () async { + late BaseItemDto genre; + try { + if (FinampSettingsHelper.finampSettings.isOffline) { + final downloadsService = GetIt.instance(); + genre = (await downloadsService.getCollectionInfo( + id: widget.item.genreItems!.first.id))! + .baseItem!; + } else { + genre = await _jellyfinApiHelper + .getItemById(widget.item.genreItems!.first.id); + } + } catch (e) { + GlobalSnackbar.error(e); + return; + } + if (context.mounted) { + Navigator.pop(context); + await Navigator.of(context) + .pushNamed(ArtistScreen.routeName, arguments: genre); + } + }, + ), + ), + ]; } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 686d9dee0..41761aaab 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1252,5 +1252,9 @@ "redownloadSubtitle": "Automatically redownload songs which are expected to be at a different quality due to parent collection changes.", "@redownloadSubtitle": { "description": "subtitle for download transcode setting which redownloads songs with higher allowed qualities" + }, + "lockDownload": "Keep on Device", + "@lockDownload": { + "description": "Text shown on button to require an item which is already downloaded by a parent collection, preventing it from being deleted if the parent is removed." } } diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index dcb18ef84..6af20391e 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -1087,16 +1087,17 @@ enum DownloadItemState { /// The status of a download, as used to determine download button state. /// Obtain via downloadsService statusProvider. enum DownloadItemStatus { - notNeeded(false, false), - incidental(false, false), - incidentalOutdated(false, true), - required(true, false), - requiredOutdated(true, true); + notNeeded(false, false, false), + incidental(false, false, true), + incidentalOutdated(false, true, true), + required(true, false, false), + requiredOutdated(true, true, false); - const DownloadItemStatus(this.isRequired, this.outdated); + const DownloadItemStatus(this.isRequired, this.outdated, this.isIncidental); final bool isRequired; final bool outdated; + final bool isIncidental; } /// The type of a BaseItemDto as determined from its type field.