From 5293dcaba689238c15e0c03e9a57c460bb4385f5 Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Sat, 26 Oct 2024 16:21:45 +0200 Subject: [PATCH] feat: add playlists to localaudio view and search (#980) --- lib/app/view/scaffold.dart | 4 +- lib/common/view/audio_card.dart | 21 ++--- lib/local_audio/local_audio_service.dart | 12 ++- lib/local_audio/view/local_audio_body.dart | 6 ++ .../view/local_audio_control_panel.dart | 45 ++++++----- lib/local_audio/view/local_audio_page.dart | 3 + lib/local_audio/view/local_audio_view.dart | 4 +- lib/local_audio/view/playlists_view.dart | 78 +++++++++++++++++++ lib/player/view/volume_popup.dart | 3 +- lib/search/search_model.dart | 17 +++- lib/search/search_type.dart | 2 + lib/search/view/search_page.dart | 2 +- .../view/sliver_local_search_results.dart | 6 +- .../view/sliver_search_type_filter_bar.dart | 7 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 16 files changed, 170 insertions(+), 46 deletions(-) create mode 100644 lib/local_audio/view/playlists_view.dart diff --git a/lib/app/view/scaffold.dart b/lib/app/view/scaffold.dart index ea798fcf9..65f0d554a 100644 --- a/lib/app/view/scaffold.dart +++ b/lib/app/view/scaffold.dart @@ -113,7 +113,9 @@ class _DiscordConnectContent extends StatelessWidget { ), Icon( TablerIcons.brand_discord_filled, - color: context.theme.primaryColor, + color: context.theme.snackBarTheme.backgroundColor != null + ? contrastColor(context.theme.snackBarTheme.backgroundColor!) + : null, ), ], ), diff --git a/lib/common/view/audio_card.dart b/lib/common/view/audio_card.dart index cfb2e1cdf..b5460ccce 100644 --- a/lib/common/view/audio_card.dart +++ b/lib/common/view/audio_card.dart @@ -73,20 +73,13 @@ class _AudioCardState extends State { Positioned( bottom: 10, right: 10, - child: CircleAvatar( - radius: avatarIconRadius, - backgroundColor: theme.colorScheme.primary, - child: IconButton( - onPressed: widget.onPlay, - icon: Padding( - padding: appleStyled - ? const EdgeInsets.only(left: 3) - : EdgeInsets.zero, - child: Icon( - Iconz.playFilled, - color: contrastColor(theme.colorScheme.primary), - ), - ), + child: FloatingActionButton.small( + onPressed: widget.onPlay, + elevation: 0.5, + backgroundColor: Colors.white, + child: Icon( + Iconz.playFilled, + color: Colors.black, ), ), ), diff --git a/lib/local_audio/local_audio_service.dart b/lib/local_audio/local_audio_service.dart index 7b513fd95..980f4f147 100644 --- a/lib/local_audio/local_audio_service.dart +++ b/lib/local_audio/local_audio_service.dart @@ -16,6 +16,7 @@ typedef LocalSearchResult = ({ List? artists, List? albums, List? genres, + List? playlists, }); class LocalAudioService { @@ -178,7 +179,15 @@ class LocalAudioService { LocalSearchResult? search(String? query) { if (query == null) return null; - if (query.isEmpty) return (titles: [], artists: [], albums: [], genres: []); + if (query.isEmpty) { + return ( + titles: [], + artists: [], + albums: [], + genres: [], + playlists: [], + ); + } final allAlbumsFindings = allAlbums?.where((e) => e.toLowerCase().contains(query.toLowerCase())); @@ -222,6 +231,7 @@ class LocalAudioService { artists: allArtists ?.where((a) => a.toLowerCase().contains(query.toLowerCase())) .toList(), + playlists: [], ); } diff --git a/lib/local_audio/view/local_audio_body.dart b/lib/local_audio/view/local_audio_body.dart index 775120891..7c71e327f 100644 --- a/lib/local_audio/view/local_audio_body.dart +++ b/lib/local_audio/view/local_audio_body.dart @@ -5,6 +5,7 @@ import 'album_view.dart'; import 'artists_view.dart'; import 'genres_view.dart'; import 'local_audio_view.dart'; +import 'playlists_view.dart'; import 'titles_view.dart'; class LocalAudioBody extends StatelessWidget { @@ -15,6 +16,7 @@ class LocalAudioBody extends StatelessWidget { required this.artists, required this.albums, required this.genres, + required this.playlists, this.noResultMessage, this.noResultIcon, }); @@ -24,6 +26,7 @@ class LocalAudioBody extends StatelessWidget { final List? artists; final List? albums; final List? genres; + final List? playlists; final Widget? noResultMessage, noResultIcon; @override @@ -49,6 +52,9 @@ class LocalAudioBody extends StatelessWidget { noResultMessage: noResultMessage, noResultIcon: noResultIcon, ), + LocalAudioView.playlists => PlaylistsView( + playlists: playlists ?? [], + ) }; } } diff --git a/lib/local_audio/view/local_audio_control_panel.dart b/lib/local_audio/view/local_audio_control_panel.dart index ee0260028..9ec9b3532 100644 --- a/lib/local_audio/view/local_audio_control_panel.dart +++ b/lib/local_audio/view/local_audio_control_panel.dart @@ -18,25 +18,32 @@ class LocalAudioControlPanel extends StatelessWidget with WatchItMixin { return Align( alignment: Alignment.center, - child: YaruChoiceChipBar( - chipBackgroundColor: chipColor(theme), - selectedChipBackgroundColor: chipSelectionColor(theme, false), - borderColor: chipBorder(theme, false), - yaruChoiceChipBarStyle: YaruChoiceChipBarStyle.wrap, - selectedFirst: false, - clearOnSelect: false, - labels: LocalAudioView.values - .map( - (e) => Text( - e.localize(context.l10n), - style: chipTextStyle(theme), - ), - ) - .toList(), - isSelected: LocalAudioView.values - .map((e) => e == LocalAudioView.values[index]) - .toList(), - onSelected: (index) => di().localAudioindex = index, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: YaruChoiceChipBar( + chipBackgroundColor: chipColor(theme), + selectedChipBackgroundColor: chipSelectionColor(theme, false), + borderColor: chipBorder(theme, false), + yaruChoiceChipBarStyle: YaruChoiceChipBarStyle.wrap, + selectedFirst: false, + clearOnSelect: false, + labels: LocalAudioView.values + .map( + (e) => Text( + e.localize(context.l10n), + style: chipTextStyle(theme), + ), + ) + .toList(), + isSelected: LocalAudioView.values + .map((e) => e == LocalAudioView.values[index]) + .toList(), + onSelected: (index) => + di().localAudioindex = index, + ), + ), ), ); } diff --git a/lib/local_audio/view/local_audio_page.dart b/lib/local_audio/view/local_audio_page.dart index d8a979e83..500b1e4e6 100644 --- a/lib/local_audio/view/local_audio_page.dart +++ b/lib/local_audio/view/local_audio_page.dart @@ -53,6 +53,8 @@ class _LocalAudioPageState extends State { final allArtists = watchPropertyValue((LocalAudioModel m) => m.allArtists); final allAlbums = watchPropertyValue((LocalAudioModel m) => m.allAlbums); final allGenres = watchPropertyValue((LocalAudioModel m) => m.allGenres); + final playlists = + watchPropertyValue((LibraryModel m) => m.playlists.keys.toList()); final index = watchPropertyValue((LocalAudioModel m) => m.localAudioindex); final localAudioView = LocalAudioView.values[index]; @@ -101,6 +103,7 @@ class _LocalAudioPageState extends State { albums: allAlbums, artists: allArtists, genres: allGenres, + playlists: playlists, noResultIcon: const AnimatedEmoji(AnimatedEmojis.bird), noResultMessage: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/local_audio/view/local_audio_view.dart b/lib/local_audio/view/local_audio_view.dart index a0b96da9f..fd6f16e96 100644 --- a/lib/local_audio/view/local_audio_view.dart +++ b/lib/local_audio/view/local_audio_view.dart @@ -4,7 +4,8 @@ enum LocalAudioView { titles, artists, albums, - genres; + genres, + playlists; String localize(AppLocalizations l10n) { return switch (this) { @@ -12,6 +13,7 @@ enum LocalAudioView { artists => l10n.artists, albums => l10n.albums, genres => l10n.genres, + playlists => l10n.playlists, }; } } diff --git a/lib/local_audio/view/playlists_view.dart b/lib/local_audio/view/playlists_view.dart new file mode 100644 index 000000000..860419ab0 --- /dev/null +++ b/lib/local_audio/view/playlists_view.dart @@ -0,0 +1,78 @@ +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/round_image_container.dart'; +import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; +import '../../library/library_model.dart'; +import '../../playlists/view/manual_add_dialog.dart'; +import '../../playlists/view/playlist_page.dart'; + +class PlaylistsView extends StatelessWidget { + const PlaylistsView({ + super.key, + this.noResultMessage, + this.noResultIcon, + required this.playlists, + }); + + final List? playlists; + final Widget? noResultMessage, noResultIcon; + + @override + Widget build(BuildContext context) { + final lists = [ + kNewPlaylistPageId, + ...(playlists ?? []), + ]; + + return SliverGrid.builder( + itemCount: lists.length, + gridDelegate: kDiskGridDelegate, + itemBuilder: (context, index) { + final id = lists.elementAt(index); + return YaruSelectableContainer( + selected: false, + onTap: () => id == kNewPlaylistPageId + ? showDialog( + context: context, + builder: (context) => const ManualAddDialog(), + ) + : di().push( + builder: (_) => PlaylistPage( + pageId: id, + ), + pageId: id, + ), + borderRadius: BorderRadius.circular(300), + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: double.infinity, + height: double.infinity, + child: id == kNewPlaylistPageId + ? Container( + decoration: BoxDecoration( + color: context.colorScheme.surface.scale( + lightness: context.colorScheme.isDark ? 0.1 : -0.1, + ), + shape: BoxShape.circle, + ), + child: Icon(Iconz.plus), + ) + : RoundImageContainer( + images: const {}, + fallBackText: id, + ), + ), + if (id != kNewPlaylistPageId) ArtistVignette(text: id), + ], + ), + ); + }, + ); + } +} diff --git a/lib/player/view/volume_popup.dart b/lib/player/view/volume_popup.dart index 912a239f0..32d37bd90 100644 --- a/lib/player/view/volume_popup.dart +++ b/lib/player/view/volume_popup.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:phoenix_theme/phoenix_theme.dart' - hide CustomTrackShape, BuildContextX; +import 'package:phoenix_theme/phoenix_theme.dart' hide CustomTrackShape; import 'package:watch_it/watch_it.dart'; import '../../common/view/icons.dart'; diff --git a/lib/search/search_model.dart b/lib/search/search_model.dart index 28230927f..ead514b56 100644 --- a/lib/search/search_model.dart +++ b/lib/search/search_model.dart @@ -143,7 +143,20 @@ class SearchModel extends SafeChangeNotifier { Future localSearch(String? query) async { await Future.delayed(const Duration(microseconds: 1)); - return _localAudioService.search(_searchQuery); + final search = _localAudioService.search(_searchQuery); + return ( + albums: search?.albums, + titles: search?.titles, + genres: search?.genres, + artists: search?.artists, + playlists: (query != null && query.isNotEmpty) + ? _libraryService.playlists.keys.toList() + : _libraryService.playlists.keys + .where( + (e) => e.toLowerCase().contains(query?.toLowerCase() ?? ''), + ) + .toList() + ); } static const _podcastDefaultLimit = 32; @@ -299,6 +312,8 @@ class SearchModel extends SafeChangeNotifier { setSearchType(SearchType.localArtist); } else if (localSearchResult?.genres?.isNotEmpty == true) { setSearchType(SearchType.localGenreName); + } else if (localSearchResult?.playlists?.isNotEmpty == true) { + setSearchType(SearchType.localPlaylists); } } }).then( diff --git a/lib/search/search_type.dart b/lib/search/search_type.dart index 373f5cd0a..71c81f50a 100644 --- a/lib/search/search_type.dart +++ b/lib/search/search_type.dart @@ -6,6 +6,7 @@ enum SearchType { localArtist, localAlbum, localGenreName, + localPlaylists, radioName, radioTag, radioCountry, @@ -17,6 +18,7 @@ enum SearchType { localArtist => l10n.artists, localAlbum => l10n.albums, localGenreName => l10n.genres, + localPlaylists => l10n.playlists, radioName => l10n.name, radioTag => l10n.tag, podcastTitle => l10n.title, diff --git a/lib/search/view/search_page.dart b/lib/search/view/search_page.dart index 02157e5e6..65fc6c56a 100644 --- a/lib/search/view/search_page.dart +++ b/lib/search/view/search_page.dart @@ -88,7 +88,7 @@ class SearchPage extends StatelessWidget with WatchItMixin { }, title: switch (audioType) { AudioType.podcast => const SliverPodcastFilterBar(), - _ => const SliverSearchTypeFilterBar(), + _ => const SearchTypeFilterBar(), }, ), SliverPadding( diff --git a/lib/search/view/sliver_local_search_results.dart b/lib/search/view/sliver_local_search_results.dart index 4ce5186b5..2b95d488d 100644 --- a/lib/search/view/sliver_local_search_results.dart +++ b/lib/search/view/sliver_local_search_results.dart @@ -43,7 +43,8 @@ class _SliverLocalSearchResultState extends State { SearchType.localAlbum => LocalAudioView.albums, SearchType.localArtist => LocalAudioView.artists, SearchType.localTitle => LocalAudioView.titles, - _ => LocalAudioView.genres, + SearchType.localGenreName => LocalAudioView.genres, + _ => LocalAudioView.playlists, }, ); @@ -55,6 +56,8 @@ class _SliverLocalSearchResultState extends State { watchPropertyValue((SearchModel m) => m.localSearchResult?.albums); final genresResult = watchPropertyValue((SearchModel m) => m.localSearchResult?.genres); + final playlistsResult = + watchPropertyValue((SearchModel m) => m.localSearchResult?.playlists); final searchQuery = watchPropertyValue((SearchModel m) => m.searchQuery); @@ -73,6 +76,7 @@ class _SliverLocalSearchResultState extends State { artists: artists, albums: albums, genres: genresResult, + playlists: playlistsResult, ); } } diff --git a/lib/search/view/sliver_search_type_filter_bar.dart b/lib/search/view/sliver_search_type_filter_bar.dart index d5a98a82c..4a315d830 100644 --- a/lib/search/view/sliver_search_type_filter_bar.dart +++ b/lib/search/view/sliver_search_type_filter_bar.dart @@ -12,8 +12,8 @@ import '../../radio/view/radio_reconnect_button.dart'; import '../search_model.dart'; import '../search_type.dart'; -class SliverSearchTypeFilterBar extends StatelessWidget with WatchItMixin { - const SliverSearchTypeFilterBar({super.key}); +class SearchTypeFilterBar extends StatelessWidget with WatchItMixin { + const SearchTypeFilterBar({super.key}); @override Widget build(BuildContext context) { @@ -27,6 +27,7 @@ class SliverSearchTypeFilterBar extends StatelessWidget with WatchItMixin { final searchQuery = watchPropertyValue((SearchModel m) => m.searchQuery); return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, @@ -89,6 +90,8 @@ class SliverSearchTypeFilterBar extends StatelessWidget with WatchItMixin { '(${localSearchResult?.artists?.length ?? '0'})', SearchType.localGenreName => '(${localSearchResult?.genres?.length ?? '0'})', + SearchType.localPlaylists => + '(${localSearchResult?.playlists?.length ?? '0'})', _ => '' }}'; } diff --git a/pubspec.lock b/pubspec.lock index ad68e0bca..e3dd5cf7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1097,8 +1097,8 @@ packages: dependency: "direct main" description: path: "." - ref: fe7b369cdf03a15c735c866584a251da68f3a716 - resolved-ref: fe7b369cdf03a15c735c866584a251da68f3a716 + ref: "0a122e798c87366d74a08bc1f1104e8aa0910ee3" + resolved-ref: "0a122e798c87366d74a08bc1f1104e8aa0910ee3" url: "https://github.com/ubuntu-flutter-community/phoenix_theme" source: git version: "1.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index dd0cd5466..8ce424638 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: phoenix_theme: git: url: https://github.com/ubuntu-flutter-community/phoenix_theme - ref: fe7b369cdf03a15c735c866584a251da68f3a716 + ref: 0a122e798c87366d74a08bc1f1104e8aa0910ee3 pls: ^1.1.0 podcast_search: ^0.7.3 radio_browser_api: ^2.0.0