From bb7eef57213fd38bb050c7656dd97ebee377ae90 Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Mon, 18 Nov 2024 21:27:11 +0100 Subject: [PATCH] feat: show option bottom sheets for audio tiles on mobile (#1032) * feat: show option bottom sheets for audio tiles on mobile --- android/app/src/debug/AndroidManifest.xml | 51 ++-- lib/common/view/audio_tile.dart | 12 +- lib/common/view/audio_tile_bottom_sheet.dart | 252 ++++++++++++++++++ lib/common/view/audio_tile_option_button.dart | 135 +++------- lib/common/view/icons.dart | 6 +- lib/common/view/meta_data_dialog.dart | 82 ++++++ lib/common/view/modals.dart | 18 ++ lib/common/view/sliver_audio_tile_list.dart | 1 - .../view/stream_provider_share_button.dart | 18 ++ .../view/local_audio_control_panel.dart | 3 + .../view/add_to_playlist_dialog.dart | 237 ++-------------- .../view/add_to_playlist_navigator.dart | 215 +++++++++++++++ lib/playlists/view/playlist_page.dart | 1 - .../podcast_collection_control_panel.dart | 3 + lib/radio/view/radio_lib_page.dart | 3 + .../view/sliver_podcast_filter_bar.dart | 3 + .../view/sliver_search_type_filter_bar.dart | 3 + pubspec.lock | 8 +- pubspec.yaml | 2 +- 19 files changed, 702 insertions(+), 351 deletions(-) create mode 100644 lib/common/view/audio_tile_bottom_sheet.dart create mode 100644 lib/common/view/meta_data_dialog.dart create mode 100644 lib/common/view/modals.dart create mode 100644 lib/playlists/view/add_to_playlist_navigator.dart diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 6f862a41d..9392a6a76 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -15,15 +14,23 @@ - - - - - + + + - + + + + + + + + @@ -32,7 +39,7 @@ @@ -42,8 +49,7 @@ android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" - android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:hardwareAccelerated="true"> + - - - + android:foregroundServiceType="mediaPlayback" + android:exported="true" tools:ignore="Instantiatable"> + + + - - - - + + + + + + + \ No newline at end of file diff --git a/lib/common/view/audio_tile.dart b/lib/common/view/audio_tile.dart index 1e53a26ef..e0b5d45c5 100644 --- a/lib/common/view/audio_tile.dart +++ b/lib/common/view/audio_tile.dart @@ -21,7 +21,6 @@ class AudioTile extends StatefulWidget with WatchItStatefulWidgetMixin { const AudioTile({ super.key, required this.pageId, - this.insertIntoQueue, required this.selected, required this.audio, required this.isPlayerPlaying, @@ -42,7 +41,6 @@ class AudioTile extends StatefulWidget with WatchItStatefulWidgetMixin { final void Function()? onTap; final void Function()? onTitleTap; final void Function(String text)? onSubTitleTap; - final void Function(Audio audio)? insertIntoQueue; final bool showLeading; final Color? selectedColor; @@ -145,7 +143,6 @@ class _AudioTileState extends State { isPlayerPlaying: widget.isPlayerPlaying, pageId: widget.pageId, audioPageType: widget.audioPageType, - insertIntoQueue: widget.insertIntoQueue, selectedColor: selectedColor, ), ), @@ -166,7 +163,6 @@ class _AudioTileTrail extends StatelessWidget with WatchItMixin { required this.isPlayerPlaying, required this.pageId, required this.audioPageType, - this.insertIntoQueue, required this.hovered, required this.liked, required this.selectedColor, @@ -177,7 +173,6 @@ class _AudioTileTrail extends StatelessWidget with WatchItMixin { final bool isPlayerPlaying; final String pageId; final AudioPageType audioPageType; - final void Function(Audio audio)? insertIntoQueue; final bool hovered; final bool liked; final Color selectedColor; @@ -193,10 +188,9 @@ class _AudioTileTrail extends StatelessWidget with WatchItMixin { selected: selected && isPlayerPlaying, playlistId: pageId, audio: audio, - allowRemove: audioPageType == AudioPageType.playlist || - audioPageType == AudioPageType.likedAudio, - insertIntoQueue: - insertIntoQueue != null ? () => insertIntoQueue!(audio) : null, + allowRemove: (audioPageType == AudioPageType.playlist || + audioPageType == AudioPageType.likedAudio) && + audio.audioType != AudioType.radio, ), ), const SizedBox( diff --git a/lib/common/view/audio_tile_bottom_sheet.dart b/lib/common/view/audio_tile_bottom_sheet.dart new file mode 100644 index 000000000..d779650b5 --- /dev/null +++ b/lib/common/view/audio_tile_bottom_sheet.dart @@ -0,0 +1,252 @@ +import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; +import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; +import '../../player/player_model.dart'; +import '../../playlists/view/add_to_playlist_dialog.dart'; +import '../data/audio.dart'; +import 'audio_tile_image.dart'; +import 'icons.dart'; +import 'like_icon.dart'; +import 'meta_data_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'snackbars.dart'; +import 'spaced_divider.dart'; +import 'stream_provider_share_button.dart'; +import 'theme.dart'; + +class AudioTileBottomSheet extends StatelessWidget { + const AudioTileBottomSheet({ + super.key, + required this.audio, + required this.allowRemove, + required this.playlistId, + }); + + final Audio audio; + final bool allowRemove; + final String playlistId; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final libraryModel = di(); + return BottomSheet( + enableDrag: false, + onClosing: () {}, + builder: (context) { + var searchTerm = '${audio.artist ?? ''} - ${audio.title ?? ''}'; + return SizedBox( + height: 460, + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.only( + left: 15, + right: 15, + top: 5, + ), + title: Text(audio.title ?? ''), + subtitle: Text(audio.artist ?? ''), + leading: AudioTileImage( + size: kAudioTrackWidth, + audio: audio, + ), + trailing: switch (audio.audioType) { + AudioType.radio => RadioLikeIcon( + audio: audio, + ), + AudioType.local => LikeIcon( + audio: audio, + ), + _ => null, + }, + ), + const SpacedDivider( + bottom: 20, + top: 10, + left: 0, + right: 0, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: SizedBox( + height: 100, + child: Row( + children: space( + widthGap: 10, + children: [ + if (audio.audioType != AudioType.radio) + Column( + children: [ + _Button( + icon: Icon(Iconz.plus), + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (context) => + AddToPlaylistDialog(audio: audio), + ); + }, + ), + _ButtonLabel(label: l10n.addToPlaylist), + ], + ), + if (audio.audioType != AudioType.radio) + Column( + children: [ + _Button( + onPressed: () { + di().insertIntoQueue(audio); + Navigator.of(context).pop(); + showSnackBar( + context: context, + content: Text( + '${l10n.addedTo} ${l10n.queue}: ${audio.artist} - ${audio.title}', + ), + ); + }, + icon: Icon(Iconz.insertIntoQueue), + ), + _ButtonLabel(label: l10n.playNext), + ], + ), + if (allowRemove) + Column( + children: [ + _Button( + onPressed: () { + playlistId == kLikedAudiosPageId + ? libraryModel.removeLikedAudio(audio) + : libraryModel.removeAudioFromPlaylist( + playlistId, + audio, + ); + Navigator.of(context).pop(); + }, + icon: Icon(Iconz.remove), + ), + _ButtonLabel( + label: + '${l10n.removeFrom} ${playlistId == kLikedAudiosPageId ? l10n.likedSongs : playlistId}', + ), + ], + ), + Column( + children: [ + _Button( + onPressed: () { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: (context) { + return MetaDataDialog(audio: audio); + }, + ); + }, + icon: Icon(Iconz.info), + ), + _ButtonLabel(label: l10n.showMetaData), + ], + ), + ].map((e) => Expanded(child: e)).toList(), + ), + ), + ), + ), + if (audio.audioType != AudioType.radio) + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: ListView( + shrinkWrap: true, + children: [ + StreamProviderShareButton( + streamProvider: StreamProvider.youTubeMusic, + text: searchTerm, + tile: true, + ), + StreamProviderShareButton( + text: searchTerm, + tile: true, + streamProvider: StreamProvider.spotify, + ), + StreamProviderShareButton( + text: searchTerm, + tile: true, + streamProvider: StreamProvider.appleMusic, + ), + StreamProviderShareButton( + text: searchTerm, + tile: true, + streamProvider: StreamProvider.amazonMusic, + ), + StreamProviderShareButton( + text: searchTerm, + tile: true, + streamProvider: StreamProvider.amazon, + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _Button extends StatelessWidget { + const _Button({ + required this.icon, + required this.onPressed, + }); + + final Widget icon; + final void Function() onPressed; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 50, + child: IconButton.filledTonal( + color: context.colorScheme.onSurface, + style: IconButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + backgroundColor: context.colorScheme.onSurface.withOpacity(0.1), + ), + onPressed: onPressed, + icon: icon, + ), + ); + } +} + +class _ButtonLabel extends StatelessWidget { + const _ButtonLabel({ + required this.label, + }); + + final String label; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + label, + style: context.textTheme.labelMedium, + overflow: TextOverflow.ellipsis, + maxLines: 3, + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/common/view/audio_tile_option_button.dart b/lib/common/view/audio_tile_option_button.dart index 2ce78b3e9..3610adb27 100644 --- a/lib/common/view/audio_tile_option_button.dart +++ b/lib/common/view/audio_tile_option_button.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; -import '../../app_config.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; +import '../../player/player_model.dart'; import '../../playlists/view/add_to_playlist_dialog.dart'; import '../data/audio.dart'; +import 'audio_tile_bottom_sheet.dart'; import 'icons.dart'; +import 'meta_data_dialog.dart'; +import 'modals.dart'; import 'snackbars.dart'; import 'stream_provider_share_button.dart'; @@ -18,14 +21,12 @@ class AudioTileOptionButton extends StatelessWidget { super.key, required this.audio, required this.playlistId, - required this.insertIntoQueue, required this.allowRemove, required this.selected, }); final String playlistId; final Audio audio; - final void Function()? insertIntoQueue; final bool allowRemove; final bool selected; @@ -36,6 +37,14 @@ class AudioTileOptionButton extends StatelessWidget { final l10n = context.l10n; final libraryModel = di(); + if (isMobile) { + return AudioTileBottomSheetButton( + audio: audio, + allowRemove: allowRemove, + playlistId: playlistId, + ); + } + return PopupMenuButton( tooltip: l10n.moreOptions, padding: EdgeInsets.zero, @@ -44,7 +53,7 @@ class AudioTileOptionButton extends StatelessWidget { if (audio.audioType != AudioType.radio) PopupMenuItem( onTap: () { - insertIntoQueue?.call(); + di().insertIntoQueue(audio); showSnackBar( context: context, content: Text( @@ -57,29 +66,23 @@ class AudioTileOptionButton extends StatelessWidget { title: Text(l10n.playNext), ), ), - if (audio.audioType != AudioType.radio) - if (allowRemove) - PopupMenuItem( - onTap: () => playlistId == kLikedAudiosPageId - ? libraryModel.removeLikedAudio(audio) - : libraryModel.removeAudioFromPlaylist(playlistId, audio), - child: YaruTile( - leading: Icon(Iconz.remove), - title: Text( - '${l10n.removeFrom} ${playlistId == kLikedAudiosPageId ? l10n.likedSongs : playlistId}', - ), + if (allowRemove) + PopupMenuItem( + onTap: () => playlistId == kLikedAudiosPageId + ? libraryModel.removeLikedAudio(audio) + : libraryModel.removeAudioFromPlaylist(playlistId, audio), + child: YaruTile( + leading: Icon(Iconz.remove), + title: Text( + '${l10n.removeFrom} ${playlistId == kLikedAudiosPageId ? l10n.likedSongs : playlistId}', ), ), + ), if (audio.audioType != AudioType.radio) PopupMenuItem( onTap: () => showDialog( context: context, - builder: (context) { - return AddToPlaylistDialog( - audio: audio, - libraryModel: libraryModel, - ); - }, + builder: (context) => AddToPlaylistDialog(audio: audio), ), child: YaruTile( leading: Icon(Iconz.plus), @@ -127,79 +130,29 @@ class AudioTileOptionButton extends StatelessWidget { } } -class MetaDataDialog extends StatelessWidget { - const MetaDataDialog({super.key, required this.audio}); +class AudioTileBottomSheetButton extends StatelessWidget { + const AudioTileBottomSheetButton({ + super.key, + required this.audio, + required this.allowRemove, + required this.playlistId, + }); final Audio audio; + final bool allowRemove; + final String playlistId; @override - Widget build(BuildContext context) { - final radio = audio.audioType == AudioType.radio; - final l10n = context.l10n; - final items = <(String, String)>{ - ( - radio ? l10n.stationName : l10n.title, - '${audio.title}', - ), - ( - radio ? l10n.tags : l10n.album, - '${radio ? audio.album?.replaceAll(',', ', ') : audio.album}', - ), - ( - radio ? l10n.language : l10n.artist, - '${radio ? audio.language : audio.artist}', - ), - ( - radio ? l10n.quality : l10n.albumArtists, - '${audio.albumArtist}', - ), - if (!radio) - ( - l10n.trackNumber, - '${audio.trackNumber}', - ), - if (!radio) - ( - l10n.diskNumber, - '${audio.discNumber}', - ), - ( - radio ? l10n.clicks : l10n.totalDisks, - '${radio ? audio.clicks : audio.discTotal}', - ), - if (!radio) - ( - l10n.genre, - '${audio.genre}', + Widget build(BuildContext context) => IconButton( + tooltip: context.l10n.moreOptions, + onPressed: () => showModal( + context: context, + content: AudioTileBottomSheet( + audio: audio, + allowRemove: allowRemove, + playlistId: playlistId, + ), ), - ( - l10n.url, - (audio.url ?? ''), - ), - }; - - return AlertDialog( - title: yaruStyled - ? YaruDialogTitleBar( - title: Text(l10n.metadata), - ) - : Center(child: Text(l10n.metadata)), - titlePadding: - yaruStyled ? EdgeInsets.zero : const EdgeInsets.only(top: 10), - contentPadding: const EdgeInsets.only(bottom: 12), - scrollable: true, - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: items - .map( - (e) => ListTile( - dense: true, - title: Text(e.$1), - subtitle: Text(e.$2), - ), - ) - .toList(), - ), - ); - } + icon: Icon(Iconz.viewMore), + ); } diff --git a/lib/common/view/icons.dart b/lib/common/view/icons.dart index 6525bc91e..d13c678cf 100644 --- a/lib/common/view/icons.dart +++ b/lib/common/view/icons.dart @@ -426,10 +426,10 @@ class Iconz { : Icons.cleaning_services_rounded; static IconData get insertIntoQueue => yaruStyled - ? YaruIcons.music_queue + ? YaruIcons.playlist_play : appleStyled - ? CupertinoIcons.plus_app - : Icons.queue_rounded; + ? CupertinoIcons.play_circle + : Icons.playlist_add; static IconData get sleep => yaruStyled ? YaruIcons.clear_night : appleStyled diff --git a/lib/common/view/meta_data_dialog.dart b/lib/common/view/meta_data_dialog.dart new file mode 100644 index 000000000..df16c7f90 --- /dev/null +++ b/lib/common/view/meta_data_dialog.dart @@ -0,0 +1,82 @@ +import '../../app_config.dart'; +import '../../l10n/l10n.dart'; +import '../data/audio.dart'; +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + +class MetaDataDialog extends StatelessWidget { + const MetaDataDialog({super.key, required this.audio}); + + final Audio audio; + + @override + Widget build(BuildContext context) { + final radio = audio.audioType == AudioType.radio; + final l10n = context.l10n; + final items = <(String, String)>{ + ( + radio ? l10n.stationName : l10n.title, + '${audio.title}', + ), + ( + radio ? l10n.tags : l10n.album, + '${radio ? audio.album?.replaceAll(',', ', ') : audio.album}', + ), + ( + radio ? l10n.language : l10n.artist, + '${radio ? audio.language : audio.artist}', + ), + ( + radio ? l10n.quality : l10n.albumArtists, + '${audio.albumArtist}', + ), + if (!radio) + ( + l10n.trackNumber, + '${audio.trackNumber}', + ), + if (!radio) + ( + l10n.diskNumber, + '${audio.discNumber}', + ), + ( + radio ? l10n.clicks : l10n.totalDisks, + '${radio ? audio.clicks : audio.discTotal}', + ), + if (!radio) + ( + l10n.genre, + '${audio.genre}', + ), + ( + l10n.url, + (audio.url ?? ''), + ), + }; + + return AlertDialog( + title: yaruStyled + ? YaruDialogTitleBar( + title: Text(l10n.metadata), + ) + : Center(child: Text(l10n.metadata)), + titlePadding: + yaruStyled ? EdgeInsets.zero : const EdgeInsets.only(top: 10), + contentPadding: const EdgeInsets.only(bottom: 12), + scrollable: true, + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .map( + (e) => ListTile( + dense: true, + title: Text(e.$1), + subtitle: Text(e.$2), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/common/view/modals.dart b/lib/common/view/modals.dart new file mode 100644 index 000000000..a84dc58b0 --- /dev/null +++ b/lib/common/view/modals.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:yaru/theme.dart'; + +Future showModal({ + required BuildContext context, + required Widget content, +}) async { + Widget builder(context) => content; + + if (isMobile) { + showModalBottomSheet(context: context, builder: builder); + } else { + showDialog( + context: context, + builder: builder, + ); + } +} diff --git a/lib/common/view/sliver_audio_tile_list.dart b/lib/common/view/sliver_audio_tile_list.dart index d1998baff..244f45fa6 100644 --- a/lib/common/view/sliver_audio_tile_list.dart +++ b/lib/common/view/sliver_audio_tile_list.dart @@ -51,7 +51,6 @@ class SliverAudioTileList extends StatelessWidget with WatchItMixin { ), selected: audioSelected, audio: audio, - insertIntoQueue: playerModel.insertIntoQueue, pageId: pageId, selectedColor: selectedColor, ), diff --git a/lib/common/view/stream_provider_share_button.dart b/lib/common/view/stream_provider_share_button.dart index d0b31db14..22eff9e75 100644 --- a/lib/common/view/stream_provider_share_button.dart +++ b/lib/common/view/stream_provider_share_button.dart @@ -14,12 +14,14 @@ class StreamProviderShareButton extends StatelessWidget { required this.text, required this.streamProvider, this.color, + this.tile = false, }); final void Function()? onSearch; final String? text; final StreamProvider streamProvider; final Color? color; + final bool tile; @override Widget build(BuildContext context) { @@ -44,6 +46,22 @@ class StreamProviderShareButton extends StatelessWidget { String address = buildAddress(clearedText); + if (tile) { + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 5, + ), + leading: Icon(iconData), + title: Text('$tooltip ${context.l10n.search}'), + onTap: () => launchUrl( + Uri.parse( + address, + ), + ), + ); + } + return IconButton( tooltip: onSearch != null ? context.l10n.search diff --git a/lib/local_audio/view/local_audio_control_panel.dart b/lib/local_audio/view/local_audio_control_panel.dart index cb4d67644..53928b7e7 100644 --- a/lib/local_audio/view/local_audio_control_panel.dart +++ b/lib/local_audio/view/local_audio_control_panel.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../common/view/icons.dart'; import '../../common/view/theme.dart'; import '../../l10n/l10n.dart'; import '../local_audio_model.dart'; @@ -17,6 +18,8 @@ class LocalAudioControlPanel extends StatelessWidget with WatchItMixin { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: YaruChoiceChipBar( + goNextIcon: Icon(Iconz.goNext), + goPreviousIcon: Icon(Iconz.goBack), chipHeight: chipHeight, style: YaruChoiceChipBarStyle.stack, selectedFirst: false, diff --git a/lib/playlists/view/add_to_playlist_dialog.dart b/lib/playlists/view/add_to_playlist_dialog.dart index 305716a95..d2a4e78b9 100644 --- a/lib/playlists/view/add_to_playlist_dialog.dart +++ b/lib/playlists/view/add_to_playlist_dialog.dart @@ -1,234 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../app_config.dart'; import '../../common/data/audio.dart'; -import '../../common/view/common_widgets.dart'; -import '../../common/view/global_keys.dart'; -import '../../common/view/icons.dart'; -import '../../common/view/side_bar_fall_back_image.dart'; -import '../../common/view/theme.dart'; -import '../../constants.dart'; import '../../l10n/l10n.dart'; -import '../../library/library_model.dart'; -import 'add_to_playlist_snack_bar.dart'; +import 'add_to_playlist_navigator.dart'; class AddToPlaylistDialog extends StatelessWidget { - const AddToPlaylistDialog({ - super.key, - required this.audio, - required this.libraryModel, - }); + const AddToPlaylistDialog({super.key, required this.audio}); final Audio audio; - final LibraryModel libraryModel; @override - Widget build(BuildContext context) { - final nav = Navigator( - // ignore: deprecated_member_use - onPopPage: (route, result) => route.didPop(result), - key: playlistNavigatorKey, - initialRoute: '/', - onGenerateRoute: (settings) { - return PageRouteBuilder( - pageBuilder: (_, __, ___) => settings.name == '/new' - ? _NewView( - libraryModel: libraryModel, - audio: audio, - ) - : _PlaylistTilesList(audio: audio), - transitionDuration: const Duration(milliseconds: 500), - ); - }, - ); - - return AlertDialog( - title: yaruStyled - ? YaruDialogTitleBar( - title: Text(context.l10n.addToPlaylist), - ) - : Text(context.l10n.addToPlaylist), - titlePadding: yaruStyled - ? EdgeInsets.zero - : const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), - content: SizedBox(height: 200, width: 400, child: nav), - contentPadding: const EdgeInsets.symmetric(vertical: 20), - ); - } -} - -class _PlaylistTilesList extends StatelessWidget with WatchItMixin { - const _PlaylistTilesList({ - required this.audio, - }); - - final Audio audio; - - @override - Widget build(BuildContext context) { - final playlistNames = watchPropertyValue( - (LibraryModel m) => m.playlists.keys.map((e) => e.toString()), - ); - - final children = [ - ListTile( - onTap: () => playlistNavigatorKey.currentState?.pushNamed('/new'), - leading: SideBarFallBackImage( - color: Colors.transparent, - child: Icon(Iconz.plus), - ), - title: Text(context.l10n.createNewPlaylist), - ), - _PlaylistTile( - playlistId: kLikedAudiosPageId, - title: context.l10n.likedSongs, - iconData: Iconz.heartFilled, - libraryModel: di(), - audio: audio, - ), - ...playlistNames.map( - (playlistId) => Builder( - builder: (context) { - return _PlaylistTile( - playlistId: playlistId, - libraryModel: di(), - audio: audio, - ); - }, - ), - ), - ]; - - return ListView.separated( - shrinkWrap: true, - itemCount: children.length, - separatorBuilder: (context, index) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) => children.elementAt(index), - ); - } -} - -class _PlaylistTile extends StatelessWidget { - const _PlaylistTile({ - required this.libraryModel, - required this.audio, - required this.playlistId, - this.title, - this.iconData, - }); - - final LibraryModel libraryModel; - final Audio audio; - final String playlistId; - final String? title; - final IconData? iconData; - - @override - Widget build(BuildContext context) { - return ListTile( - onTap: () { - libraryModel.addAudioToPlaylist(playlistId, audio); - Navigator.of(context, rootNavigator: true).maybePop(); - showAddedToPlaylistSnackBar( - context: context, - libraryModel: libraryModel, - id: playlistId, - ); - }, - leading: SideBarFallBackImage( - color: getAlphabetColor(playlistId), - child: Icon(iconData ?? Iconz.starFilled), - ), - title: Text(title ?? playlistId), - ); - } -} - -class _NewView extends StatefulWidget { - const _NewView({ - required this.libraryModel, - required this.audio, - }); - - final LibraryModel libraryModel; - final Audio audio; - - @override - State<_NewView> createState() => _NewViewState(); -} - -class _NewViewState extends State<_NewView> { - late TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).dialogBackgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: _controller, - ), - const SizedBox( - height: 20, - ), - Align( - alignment: Alignment.centerRight, - child: Wrap( - spacing: 20, - runSpacing: 10, - children: [ - OutlinedButton( - onPressed: () => Navigator.pop(context), - child: Text( - context.l10n.cancel, - ), - ), - ImportantButton( - onPressed: () { - Navigator.of(context).pop(); - widget.libraryModel.addPlaylist( - _controller.text, - [widget.audio], - ); - showAddedToPlaylistSnackBar( - context: context, - libraryModel: widget.libraryModel, - id: _controller.text, - ); - }, - child: Text( - context.l10n.add, - ), - ), - ], - ), - ), - ], + Widget build(BuildContext context) => AlertDialog( + title: yaruStyled + ? YaruDialogTitleBar( + title: Text(context.l10n.addToPlaylist), + ) + : Text(context.l10n.addToPlaylist), + titlePadding: yaruStyled + ? EdgeInsets.zero + : const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), + content: SizedBox( + height: 200, + width: 400, + child: AddToPlaylistNavigator(audio: audio), ), - ), - ); - } + contentPadding: const EdgeInsets.symmetric(vertical: 20), + ); } diff --git a/lib/playlists/view/add_to_playlist_navigator.dart b/lib/playlists/view/add_to_playlist_navigator.dart new file mode 100644 index 000000000..7e2a5d9af --- /dev/null +++ b/lib/playlists/view/add_to_playlist_navigator.dart @@ -0,0 +1,215 @@ +import '../../common/data/audio.dart'; +import '../../common/view/common_widgets.dart'; +import '../../common/view/global_keys.dart'; +import '../../common/view/icons.dart'; +import '../../common/view/side_bar_fall_back_image.dart'; +import '../../common/view/theme.dart'; +import '../../constants.dart'; +import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; +import 'add_to_playlist_snack_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; + +class AddToPlaylistNavigator extends StatelessWidget { + const AddToPlaylistNavigator({ + super.key, + required this.audio, + }); + + final Audio audio; + + @override + Widget build(BuildContext context) { + return Navigator( + key: playlistNavigatorKey, + onDidRemovePage: (page) {}, + initialRoute: '/', + onGenerateRoute: (settings) { + return PageRouteBuilder( + pageBuilder: (_, __, ___) => settings.name == '/new' + ? _NewView(audio: audio) + : _PlaylistTilesList(audio: audio), + transitionDuration: const Duration(milliseconds: 500), + ); + }, + ); + } +} + +class _PlaylistTilesList extends StatelessWidget with WatchItMixin { + const _PlaylistTilesList({ + required this.audio, + }); + + final Audio audio; + + @override + Widget build(BuildContext context) { + final playlistNames = watchPropertyValue( + (LibraryModel m) => m.playlists.keys.map((e) => e.toString()), + ); + + final children = [ + ListTile( + contentPadding: _PlaylistTile.padding, + onTap: () => playlistNavigatorKey.currentState?.pushNamed('/new'), + leading: SideBarFallBackImage( + color: Colors.transparent, + child: Icon(Iconz.plus), + ), + title: Text(context.l10n.createNewPlaylist), + ), + _PlaylistTile( + playlistId: kLikedAudiosPageId, + title: context.l10n.likedSongs, + iconData: Iconz.heartFilled, + libraryModel: di(), + audio: audio, + ), + ...playlistNames.map( + (playlistId) => Builder( + builder: (context) { + return _PlaylistTile( + playlistId: playlistId, + libraryModel: di(), + audio: audio, + ); + }, + ), + ), + ]; + + return ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 10), + shrinkWrap: true, + itemCount: children.length, + separatorBuilder: (context, index) => const SizedBox(height: 5), + itemBuilder: (context, index) => children.elementAt(index), + ); + } +} + +class _PlaylistTile extends StatelessWidget { + const _PlaylistTile({ + required this.libraryModel, + required this.audio, + required this.playlistId, + this.title, + this.iconData, + }); + + final LibraryModel libraryModel; + final Audio audio; + final String playlistId; + final String? title; + final IconData? iconData; + + static EdgeInsets get padding => + const EdgeInsets.symmetric(horizontal: 10, vertical: 5); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: padding, + onTap: () { + libraryModel.addAudioToPlaylist(playlistId, audio); + Navigator.of(context, rootNavigator: true).maybePop(); + showAddedToPlaylistSnackBar( + context: context, + libraryModel: libraryModel, + id: playlistId, + ); + }, + leading: SideBarFallBackImage( + color: getAlphabetColor(playlistId), + child: Icon(iconData ?? Iconz.starFilled), + ), + title: Text(title ?? playlistId), + ); + } +} + +class _NewView extends StatefulWidget { + const _NewView({ + required this.audio, + }); + + final Audio audio; + + @override + State<_NewView> createState() => _NewViewState(); +} + +class _NewViewState extends State<_NewView> { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final libraryModel = di(); + return Container( + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _controller, + ), + const SizedBox( + height: 20, + ), + Align( + alignment: Alignment.centerRight, + child: Wrap( + spacing: 20, + runSpacing: 10, + children: [ + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: Text( + context.l10n.cancel, + ), + ), + ImportantButton( + onPressed: () { + Navigator.of(context).pop(); + libraryModel.addPlaylist( + _controller.text, + [widget.audio], + ); + showAddedToPlaylistSnackBar( + context: context, + libraryModel: libraryModel, + id: _controller.text, + ); + }, + child: Text( + context.l10n.add, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/playlists/view/playlist_page.dart b/lib/playlists/view/playlist_page.dart index efe6d6aee..835369758 100644 --- a/lib/playlists/view/playlist_page.dart +++ b/lib/playlists/view/playlist_page.dart @@ -290,7 +290,6 @@ class _PlaylistPageBody extends StatelessWidget with WatchItMixin { ), selected: audioSelected, audio: audio, - insertIntoQueue: playerModel.insertIntoQueue, pageId: pageId, audioPageType: AudioPageType.playlist, ), diff --git a/lib/podcasts/view/podcast_collection_control_panel.dart b/lib/podcasts/view/podcast_collection_control_panel.dart index 9def3a6a4..1f33e2cb7 100644 --- a/lib/podcasts/view/podcast_collection_control_panel.dart +++ b/lib/podcasts/view/podcast_collection_control_panel.dart @@ -3,6 +3,7 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../app/connectivity_model.dart'; +import '../../common/view/icons.dart'; import '../../common/view/offline_page.dart'; import '../../l10n/l10n.dart'; import '../podcast_model.dart'; @@ -24,6 +25,8 @@ class PodcastCollectionControlPanel extends StatelessWidget with WatchItMixin { watchPropertyValue((PodcastModel m) => m.downloadsOnly); return YaruChoiceChipBar( + goNextIcon: Icon(Iconz.goNext), + goPreviousIcon: Icon(Iconz.goBack), style: YaruChoiceChipBarStyle.wrap, clearOnSelect: false, selectedFirst: false, diff --git a/lib/radio/view/radio_lib_page.dart b/lib/radio/view/radio_lib_page.dart index 76272d1c7..901b064a3 100644 --- a/lib/radio/view/radio_lib_page.dart +++ b/lib/radio/view/radio_lib_page.dart @@ -11,6 +11,7 @@ import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_card.dart'; import '../../common/view/audio_card_bottom.dart'; +import '../../common/view/icons.dart'; import '../../common/view/no_search_result_page.dart'; import '../../common/view/side_bar_fall_back_image.dart'; import '../../common/view/theme.dart'; @@ -44,6 +45,8 @@ class RadioLibPage extends StatelessWidget with WatchItMixin { margin: filterPanelPadding, height: context.theme.appBarTheme.toolbarHeight, child: YaruChoiceChipBar( + goNextIcon: Icon(Iconz.goNext), + goPreviousIcon: Icon(Iconz.goBack), selectedFirst: false, clearOnSelect: false, onSelected: (index) => radioModel diff --git a/lib/search/view/sliver_podcast_filter_bar.dart b/lib/search/view/sliver_podcast_filter_bar.dart index 30a4d7693..40eb48f11 100644 --- a/lib/search/view/sliver_podcast_filter_bar.dart +++ b/lib/search/view/sliver_podcast_filter_bar.dart @@ -3,6 +3,7 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../common/data/podcast_genre.dart'; +import '../../common/view/icons.dart'; import '../../l10n/l10n.dart'; import '../../settings/settings_model.dart'; import '../search_model.dart'; @@ -26,6 +27,8 @@ class SliverPodcastFilterBar extends StatelessWidget with WatchItMixin { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: YaruChoiceChipBar( + goNextIcon: Icon(Iconz.goNext), + goPreviousIcon: Icon(Iconz.goBack), style: YaruChoiceChipBarStyle.stack, labels: genres .map( diff --git a/lib/search/view/sliver_search_type_filter_bar.dart b/lib/search/view/sliver_search_type_filter_bar.dart index e0a3a19fe..9e358247b 100644 --- a/lib/search/view/sliver_search_type_filter_bar.dart +++ b/lib/search/view/sliver_search_type_filter_bar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../common/view/icons.dart'; import '../../l10n/l10n.dart'; import '../../local_audio/local_audio_service.dart'; import '../search_model.dart'; @@ -22,6 +23,8 @@ class SearchTypeFilterBar extends StatelessWidget with WatchItMixin { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: YaruChoiceChipBar( + goNextIcon: Icon(Iconz.goNext), + goPreviousIcon: Icon(Iconz.goBack), style: YaruChoiceChipBarStyle.stack, clearOnSelect: false, selectedFirst: false, diff --git a/pubspec.lock b/pubspec.lock index 016ffcdd9..e05f36e45 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1137,10 +1137,10 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + sha256: "6b9cb54b7135073841a35513fba39e598b421702d5f4d92319992fd6eb5532a9" url: "https://pub.dev" source: hosted - version: "0.1.3+2" + version: "0.1.3+4" permission_handler_platform_interface: dependency: transitive description: @@ -1169,8 +1169,8 @@ packages: dependency: "direct main" description: path: "." - ref: "57d1af53ff48300b2cb059d5da2a8b0978db6402" - resolved-ref: "57d1af53ff48300b2cb059d5da2a8b0978db6402" + ref: e357331e9acffc5c82627df8107e47a452ee16b9 + resolved-ref: e357331e9acffc5c82627df8107e47a452ee16b9 url: "https://github.com/ubuntu-flutter-community/phoenix_theme" source: git version: "1.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 6abcc6eac..e8f521894 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: phoenix_theme: git: url: https://github.com/ubuntu-flutter-community/phoenix_theme - ref: 57d1af53ff48300b2cb059d5da2a8b0978db6402 + ref: e357331e9acffc5c82627df8107e47a452ee16b9 pls: ^1.1.0 podcast_search: ^0.7.3 radio_browser_api: ^2.0.0