From a3a6c7cc0f5f3ada11bbc0692e5b9719fef7f8dd Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Mon, 17 Apr 2023 22:27:58 +0200 Subject: [PATCH] migration to Material 3; fix #67 (#72) --- CHANGELOG.md | 2 + src/assets/icons/link_both.svg | 93 ++ src/assets/icons/link_next.svg | 93 ++ src/assets/icons/link_prev.svg | 98 ++ src/fonts/MuckeIcons.ttf | Bin 3796 -> 4680 bytes .../repositories/music_data_repository.dart | 1 + src/lib/presentation/gradients.dart | 2 +- .../forms/artistofday_form_page.dart | 136 ++- .../home_widgets/forms/history_form_page.dart | 174 ++-- .../forms/playlists_form_page.dart | 336 ++++--- .../forms/shuffle_all_form_page.dart | 136 ++- src/lib/presentation/mucke_icons.dart | 9 +- .../pages/album_details_page.dart | 277 +++--- .../pages/artist_details_page.dart | 4 +- .../pages/blocked_files_page.dart | 74 +- .../pages/cover_customization_page.dart | 178 ++-- .../presentation/pages/currently_playing.dart | 31 +- src/lib/presentation/pages/home_page.dart | 136 ++- .../pages/home_settings_page.dart | 172 ++-- .../pages/library_folders_page.dart | 76 +- .../pages/library_tab_container.dart | 68 +- .../pages/playlist_form_page.dart | 243 +++-- src/lib/presentation/pages/playlist_page.dart | 28 +- .../presentation/pages/playlists_page.dart | 3 + src/lib/presentation/pages/queue_page.dart | 338 ++++--- src/lib/presentation/pages/search_page.dart | 697 +++++++-------- src/lib/presentation/pages/settings_page.dart | 262 +++--- .../pages/smart_list_form_page.dart | 843 +++++++++--------- .../presentation/pages/smart_list_page.dart | 28 +- .../pages/smart_lists_artists_page.dart | 100 +-- src/lib/presentation/pages/songs_page.dart | 7 +- .../presentation/state/music_data_store.dart | 5 + src/lib/presentation/theming.dart | 34 +- src/lib/presentation/utils.dart | 26 +- .../widgets/album_background.dart | 2 +- .../presentation/widgets/artist_header.dart | 8 +- .../widgets/artist_highlighted_songs.dart | 3 +- .../widgets/cover_customization_card.dart | 54 ++ .../widgets/cover_sliver_appbar.dart | 44 +- .../widgets/currently_playing_bar.dart | 9 +- .../presentation/widgets/playlist_cover.dart | 1 + .../widgets/song_bottom_sheet.dart | 161 ++-- .../widgets/song_customization_buttons.dart | 50 +- .../presentation/widgets/song_list_tile.dart | 158 ++-- .../widgets/song_list_tile_numbered.dart | 63 +- .../widgets/switch_text_listtile.dart | 2 +- .../datasources/drift/music_data_dao.dart | 7 + .../music_data_source_contract.dart | 1 + .../music_data_repository_impl.dart | 7 + src/pubspec.lock | 8 + src/pubspec.yaml | 1 + 51 files changed, 2779 insertions(+), 2510 deletions(-) create mode 100644 src/assets/icons/link_both.svg create mode 100644 src/assets/icons/link_next.svg create mode 100644 src/assets/icons/link_prev.svg create mode 100644 src/lib/presentation/widgets/cover_customization_card.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 966affe..cac0021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - Fixed bug in "Append to manually queued songs" - Fixed bug in queue when moving a song directly before the currently playing song +- Migration to Material 3 widgets including extensive UI changes +- New Icons for linked songs - Added German translation (#51) ## 1.2.0 diff --git a/src/assets/icons/link_both.svg b/src/assets/icons/link_both.svg new file mode 100644 index 0000000..896407f --- /dev/null +++ b/src/assets/icons/link_both.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/link_next.svg b/src/assets/icons/link_next.svg new file mode 100644 index 0000000..7a0600a --- /dev/null +++ b/src/assets/icons/link_next.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/link_prev.svg b/src/assets/icons/link_prev.svg new file mode 100644 index 0000000..82d1f0a --- /dev/null +++ b/src/assets/icons/link_prev.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/fonts/MuckeIcons.ttf b/src/fonts/MuckeIcons.ttf index 8b6c5aa12bba77e6cf14c4dd39a4da16f90803e0..4c19c2e598e9d3b0bf86f541db38e7860e34e047 100644 GIT binary patch delta 1328 zcmb_cOK1~O6utM&Op-|^Nk?NlQ`%r>iin>eO{j@2sT5K111@S5ETpkUQ*F{%gSN#w zE?RIQMLP;2E`)-(mC{|e1s8%Xh;&(`i`nT$kfMqACG}HU1fh@I|2c2w-uEUSn%}lQ zGOY;!dcGT>wO8JmI$!)c;4(%0Ldkp)3tl)VCPrx2@YjoCH3vWA6uCb93erm2N=)N;ylF; z;~Vtbix{6~o@pl99=7vRv04MT4noyCK-NoyB8ZSAmWwO`b0o)KsQ@_O$lTIGs?}93 zBaR3X#cq10One`eUG`13X5VLWP^sddR5gIl9urQVmuW8&Vt_-><3e0;h{=U^0f%^8 z`1?5in(&=zb;DhBv3+cUEeofFDbWz0OLR`NUxa42B>`0Cg{Q0nDzw5;I12*+hYeS# zKH5U6tqYSFK@*K21~81#mbUiT9qT!2iRnydQBVo|5<|q2% z@xJmoU0vfSQazi?2@XwZhOQVIZK^i}*lV&{L$H=x)GR|c62}sxwRn0s9p|rO==#)# zB!Ov+;`*c_G^vAd1X5egN%+s9SX)kI(@=W0tAAXkX8vl<`d7=;F@0V9ZtQ>vbif6= zsM~xGtX~%a>$myXS$X40Z)p2>*XpK|wiVxJjN0FQ?=d?2P8mgAL52c~D4`48=s_91 k=tG769WDm$nDXF2;YRxMP$^gQ3fYlT%_|OPZ}SPlPv$)K;s5{u delta 431 zcmYLD&r1SP5T1E&e`;x>goQ?-ho~r#kOCo)F8!cWbuwZ#YY%QNB%}~_sAEL29lA!h zpdj|@AJDCchz>y!9z00YAv)UZdT1Ws%zWQ`GrYU_Q}iUZUjx7+09ef%nyLS`z5$p* z+T&KSx+%`CECKK@06C%;w2h&O)&+Ur2`QZfInA@w*9mdmC|6>`)B!deUM%G`xmWF? z{a4~4L#vqB2hTJQ6Cd2xjKW8z|NH>J>zKl3X{Q{B+E)O+I2o=_>#-RXkXk}&JkeP9 zr+>i9o20;7XxaR)?MvR|)5H~Ej!NO|sn!7AqHI0jloevZxtyy4WP!Wg7*!8hb09#1 z`~p4NYs&35WpFZa!Oe`k0*D}i42oT2-Ke@BprK-&@r6-|RtPkv10B$JI+1}rl-LM6 vWG{T4AKQ~cRJPW9pO~~3{r8x(BRy3Ns>qO|fFepLqk<}Wtee2D=-&7PKc-Mv diff --git a/src/lib/domain/repositories/music_data_repository.dart b/src/lib/domain/repositories/music_data_repository.dart index e3f90c5..72d082d 100644 --- a/src/lib/domain/repositories/music_data_repository.dart +++ b/src/lib/domain/repositories/music_data_repository.dart @@ -24,6 +24,7 @@ abstract class MusicDataInfoRepository { Stream> getSmartListSongStream(SmartList smartList); Future> getPredecessors(Song song); Future> getSuccessors(Song song); + Future> isSongFirstLast(Song song); Stream> get playlistsStream; Stream getPlaylistStream(int playlistId); diff --git a/src/lib/presentation/gradients.dart b/src/lib/presentation/gradients.dart index d28b8bf..18b3ab6 100644 --- a/src/lib/presentation/gradients.dart +++ b/src/lib/presentation/gradients.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; const CUSTOM_GRADIENTS = { -'sanguine': LinearGradient( + 'sanguine': LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ diff --git a/src/lib/presentation/home_widgets/forms/artistofday_form_page.dart b/src/lib/presentation/home_widgets/forms/artistofday_form_page.dart index ab0d1ee..cf0829f 100644 --- a/src/lib/presentation/home_widgets/forms/artistofday_form_page.dart +++ b/src/lib/presentation/home_widgets/forms/artistofday_form_page.dart @@ -39,80 +39,78 @@ class _ArtistOfDayFormPageState extends State { Widget build(BuildContext context) { final NavigationStore navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.artistOfTheDay, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () async { - // store.validateAll(); - // if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - // } - }, - ), - ], + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.artistOfTheDay, + style: TEXT_HEADER, ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - ListTile( - title: Text(L10n.of(context)!.playback, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2].map>( - (int value) { - return RadioListTile( - title: Text( - ShuffleMode.values[value].toText(context), - style: const TextStyle( - fontSize: 14.0, - ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + // store.validateAll(); + // if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + // } + }, + ), + ], + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + ListTile( + title: Text(L10n.of(context)!.playback, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2].map>( + (int value) { + return RadioListTile( + title: Text( + ShuffleMode.values[value].toText(context), + style: const TextStyle( + fontSize: 14.0, ), - value: value, - groupValue: store.shuffleMode.index, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue]; - }); - }, - ); - }, - ).toList(), - ); - }, - ), + ), + value: value, + groupValue: store.shuffleMode.index, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue]; + }); + }, + ); + }, + ).toList(), + ); + }, ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/home_widgets/forms/history_form_page.dart b/src/lib/presentation/home_widgets/forms/history_form_page.dart index 6335a64..1a71350 100644 --- a/src/lib/presentation/home_widgets/forms/history_form_page.dart +++ b/src/lib/presentation/home_widgets/forms/history_form_page.dart @@ -37,102 +37,100 @@ class _HistoryFormPageState extends State { Widget build(BuildContext context) { final NavigationStore navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.history, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () async { - // store.validateAll(); - // if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - // } - }, - ), - ], + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.history, + style: TEXT_HEADER, ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - ListTile( - title: Text(L10n.of(context)!.displaySettings, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Container( - color: Colors.transparent, - height: 48.0, - child: Text(L10n.of(context)!.maxNumberEntries), - alignment: Alignment.centerLeft, - ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + // store.validateAll(); + // if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + // } + }, + ), + ], + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + ListTile( + title: Text(L10n.of(context)!.displaySettings, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Container( + color: Colors.transparent, + height: 48.0, + child: Text(L10n.of(context)!.maxNumberEntries), + alignment: Alignment.centerLeft, ), - SizedBox( - width: 56.0, - child: TextFormField( - keyboardType: TextInputType.number, - initialValue: widget.history.maxEntries.toString(), - onChanged: (value) { - store.maxEntries = value; - }, - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - decoration: const InputDecoration( - filled: true, - fillColor: DARK35, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - errorStyle: TextStyle(height: 0, fontSize: 0), - contentPadding: EdgeInsets.only( - top: 0.0, - bottom: 0.0, - left: 4.0, - right: 2.0, - ), + ), + SizedBox( + width: 56.0, + child: TextFormField( + keyboardType: TextInputType.number, + initialValue: widget.history.maxEntries.toString(), + onChanged: (value) { + store.maxEntries = value; + }, + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + decoration: const InputDecoration( + filled: true, + fillColor: DARK35, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + errorStyle: TextStyle(height: 0, fontSize: 0), + contentPadding: EdgeInsets.only( + top: 0.0, + bottom: 0.0, + left: 4.0, + right: 2.0, ), ), ), - ], - ), - ); - }, - ), + ), + ], + ), + ); + }, ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/home_widgets/forms/playlists_form_page.dart b/src/lib/presentation/home_widgets/forms/playlists_form_page.dart index f1bfb57..accb2e4 100644 --- a/src/lib/presentation/home_widgets/forms/playlists_form_page.dart +++ b/src/lib/presentation/home_widgets/forms/playlists_form_page.dart @@ -59,192 +59,190 @@ class _PlaylistsFormPageState extends State { L10n.of(context)!.smartlistsOnly, ]; - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.playlists, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () async { - // store.validateAll(); - // if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - // } - }, - ), - ], + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.playlists, + style: TEXT_HEADER, ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Observer( - builder: (_) => TextFormField( - initialValue: store.title, - onChanged: (value) => store.title = value, - style: TEXT_HEADER, - decoration: InputDecoration( - labelText: L10n.of(context)!.name, - labelStyle: const TextStyle(color: Colors.white), - floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), - // errorText: store.error.name, - errorStyle: const TextStyle(color: RED), - filled: true, - fillColor: DARK35, - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 12.0, - ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + // store.validateAll(); + // if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + // } + }, + ), + ], + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Observer( + builder: (_) => TextFormField( + initialValue: store.title, + onChanged: (value) => store.title = value, + style: TEXT_HEADER, + decoration: InputDecoration( + labelText: L10n.of(context)!.name, + labelStyle: const TextStyle(color: Colors.white), + floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), + // errorText: store.error.name, + errorStyle: const TextStyle(color: RED), + filled: true, + fillColor: DARK35, + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 12.0, ), ), ), ), - const SizedBox(height: 8.0), - ListTile( - title: Text(L10n.of(context)!.sortingFilterSettings, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.maxNumberEntries, - switchValue: store.maxEntriesEnabled, - onSwitchChanged: (bool value) { - store.maxEntriesEnabled = value; - }, - textValue: store.maxEntries, - onTextChanged: (String value) { - store.maxEntries = value; - }, - ); - }, - ), + ), + const SizedBox(height: 8.0), + ListTile( + title: Text(L10n.of(context)!.sortingFilterSettings, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.maxNumberEntries, + switchValue: store.maxEntriesEnabled, + onSwitchChanged: (bool value) { + store.maxEntriesEnabled = value; + }, + textValue: store.maxEntries, + onTextChanged: (String value) { + store.maxEntries = value; + }, + ); + }, ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2, 3].map>((int value) { - return RadioListTile( - title: Text( - orderCriterionTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2, 3].map>((int value) { + return RadioListTile( + title: Text( + orderCriterionTexts[value], + style: const TextStyle( + fontSize: 14.0, ), - value: value, - groupValue: store.orderCriterion.index, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) - store.orderCriterion = - HomePlaylistsOrder.values[newValue]; - }); - }, - ); - }).toList(), - ); - }, - ), + ), + value: value, + groupValue: store.orderCriterion.index, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) + store.orderCriterion = + HomePlaylistsOrder.values[newValue]; + }); + }, + ); + }).toList(), + ); + }, ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1].map>((int value) { - return RadioListTile( - title: Text( - orderDirectionTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1].map>((int value) { + return RadioListTile( + title: Text( + orderDirectionTexts[value], + style: const TextStyle( + fontSize: 14.0, ), - value: value, - groupValue: store.orderDirection.index, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) - store.orderDirection = OrderDirection.values[newValue]; - }); - }, - ); - }).toList(), - ); - }, - ), + ), + value: value, + groupValue: store.orderDirection.index, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) + store.orderDirection = OrderDirection.values[newValue]; + }); + }, + ); + }).toList(), + ); + }, ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2].map>((int value) { - return RadioListTile( - title: Text( - filterTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2].map>((int value) { + return RadioListTile( + title: Text( + filterTexts[value], + style: const TextStyle( + fontSize: 14.0, ), - value: value, - groupValue: store.filter.index, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) - store.filter = HomePlaylistsFilter.values[newValue]; - }); - }, - ); - }).toList(), - ); - }, - ), + ), + value: value, + groupValue: store.filter.index, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) + store.filter = HomePlaylistsFilter.values[newValue]; + }); + }, + ); + }).toList(), + ); + }, ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/home_widgets/forms/shuffle_all_form_page.dart b/src/lib/presentation/home_widgets/forms/shuffle_all_form_page.dart index 3808134..15518a7 100644 --- a/src/lib/presentation/home_widgets/forms/shuffle_all_form_page.dart +++ b/src/lib/presentation/home_widgets/forms/shuffle_all_form_page.dart @@ -39,80 +39,78 @@ class _ShuffleAllFormPageState extends State { Widget build(BuildContext context) { final NavigationStore navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.shuffleAll, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () async { - // store.validateAll(); - // if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - // } - }, - ), - ], + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.shuffleAll, + style: TEXT_HEADER, ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - ListTile( - title: Text(L10n.of(context)!.playback, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [1, 2].map>( - (int value) { - return RadioListTile( - title: Text( - ShuffleMode.values[value].toText(context), - style: const TextStyle( - fontSize: 14.0, - ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + // store.validateAll(); + // if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + // } + }, + ), + ], + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + ListTile( + title: Text(L10n.of(context)!.playback, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [1, 2].map>( + (int value) { + return RadioListTile( + title: Text( + ShuffleMode.values[value].toText(context), + style: const TextStyle( + fontSize: 14.0, ), - value: value, - groupValue: store.shuffleMode.index, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue]; - }); - }, - ); - }, - ).toList(), - ); - }, - ), + ), + value: value, + groupValue: store.shuffleMode.index, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) store.shuffleMode = ShuffleMode.values[newValue]; + }); + }, + ); + }, + ).toList(), + ); + }, ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/mucke_icons.dart b/src/lib/presentation/mucke_icons.dart index 8daba0a..6504d98 100644 --- a/src/lib/presentation/mucke_icons.dart +++ b/src/lib/presentation/mucke_icons.dart @@ -1,5 +1,5 @@ /// Flutter icons MuckeIcons -/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com +/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com /// This font was generated by FlutterIcon.com, which is derived from Fontello. import 'package:flutter/widgets.dart'; @@ -22,7 +22,10 @@ class MuckeIcons { static const IconData favorite_2_3 = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData shuffle_heart = - IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData shuffle_none = - IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); + IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData link_both = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData link_next = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData link_prev = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/src/lib/presentation/pages/album_details_page.dart b/src/lib/presentation/pages/album_details_page.dart index abe0bc1..8017736 100644 --- a/src/lib/presentation/pages/album_details_page.dart +++ b/src/lib/presentation/pages/album_details_page.dart @@ -50,154 +50,153 @@ class _AlbumDetailsPageState extends State { final AudioStore audioStore = GetIt.I(); return Scaffold( - body: Observer( - builder: (BuildContext context) { - final album = widget.album; - final songs = store.albumSongStream.value ?? []; - final totalDuration = - songs.fold(const Duration(milliseconds: 0), (Duration d, s) => d + s.duration); - final songsByDisc = _songsByDisc(store.albumSongStream.value ?? []); - final discSongNums = [0]; - for (int i = 0; i < songsByDisc.length - 1; i++) { - discSongNums.add(songsByDisc[i].length + discSongNums[i]); - } + body: Material( + child: Observer( + builder: (BuildContext context) { + final album = widget.album; + final songs = store.albumSongStream.value ?? []; + final totalDuration = + songs.fold(const Duration(milliseconds: 0), (Duration d, s) => d + s.duration); + final songsByDisc = _songsByDisc(store.albumSongStream.value ?? []); + final discSongNums = [0]; + for (int i = 0; i < songsByDisc.length - 1; i++) { + discSongNums.add(songsByDisc[i].length + discSongNums[i]); + } - return Scrollbar( - child: CustomScrollView( - slivers: [ - CoverSliverAppBar( - title: album.title, - subtitle: album.artist, - subtitle2: - '${album.pubYear.toString()} • ${L10n.of(context)!.nSongs(songs.length)} • ${utils.msToTimeString(totalDuration)}', - actions: [ - Observer( - builder: (context) { + return Scrollbar( + child: CustomScrollView( + slivers: [ + CoverSliverAppBar( + title: album.title, + subtitle: album.artist, + subtitle2: + '${album.pubYear.toString()} • ${L10n.of(context)!.nSongs(songs.length)} • ${utils.msToTimeString(totalDuration)}', + actions: [ + Observer( + builder: (context) { + final isMultiSelectEnabled = store.selection.isMultiSelectEnabled; + + if (isMultiSelectEnabled) + return IconButton( + key: GlobalKey(), + icon: const Icon(Icons.more_vert_rounded), + onPressed: () => _openMultiselectMenu(context), + ); + + return Container(); + }, + ), + Observer( + builder: (context) { + final isMultiSelectEnabled = store.selection.isMultiSelectEnabled; + final isAllSelected = store.selection.isAllSelected; + + if (isMultiSelectEnabled) + return IconButton( + key: GlobalKey(), + icon: isAllSelected + ? const Icon(Icons.deselect_rounded) + : const Icon(Icons.select_all_rounded), + onPressed: () { + if (isAllSelected) + store.selection.deselectAll(); + else + store.selection.selectAll(); + }, + ); + + return Container(); + }, + ), + Observer(builder: (context) { final isMultiSelectEnabled = store.selection.isMultiSelectEnabled; - - if (isMultiSelectEnabled) - return IconButton( - key: GlobalKey(), - icon: const Icon(Icons.more_vert_rounded), - onPressed: () => _openMultiselectMenu(context), - ); - - return Container(); - }, + return IconButton( + key: const ValueKey('ALBUM_MULTISELECT'), + icon: isMultiSelectEnabled + ? const Icon(Icons.close_rounded) + : const Icon(Icons.checklist_rtl_rounded), + onPressed: () => store.selection.toggleMultiSelect(), + ); + }) + ], + cover: Image( + image: utils.getAlbumImage(album.albumArtPath), + fit: BoxFit.cover, ), - Observer( - builder: (context) { - final isMultiSelectEnabled = store.selection.isMultiSelectEnabled; - final isAllSelected = store.selection.isAllSelected; - - if (isMultiSelectEnabled) - return IconButton( - key: GlobalKey(), - icon: isAllSelected - ? const Icon(Icons.deselect_rounded) - : const Icon(Icons.select_all_rounded), - onPressed: () { - if (isAllSelected) - store.selection.deselectAll(); - else - store.selection.selectAll(); - }, - ); - - return Container(); - }, - ), - Observer(builder: (context) { - final isMultiSelectEnabled = store.selection.isMultiSelectEnabled; - return IconButton( - key: const ValueKey('ALBUM_MULTISELECT'), - icon: isMultiSelectEnabled - ? const Icon(Icons.close_rounded) - : const Icon(Icons.checklist_rtl_rounded), - onPressed: () => store.selection.toggleMultiSelect(), - ); - }) - ], - cover: Image( - image: utils.getAlbumImage(album.albumArtPath), - fit: BoxFit.cover, - ), - background: Image( - image: utils.getAlbumImage(album.albumArtPath), - fit: BoxFit.cover, - ), - button: SizedBox( - width: 48, - child: ElevatedButton( - onPressed: () => audioStore.playAlbum(album), - child: const Icon(Icons.play_arrow_rounded), - style: ElevatedButton.styleFrom( - shape: const StadiumBorder(), - backgroundColor: LIGHT1, - fixedSize: const Size.fromHeight(48), - padding: const EdgeInsets.symmetric(horizontal: 12.0), + backgroundColor: utils.bgColor(album.color), + button: SizedBox( + width: 48, + child: Center( + child: ElevatedButton( + onPressed: () => audioStore.playAlbum(album), + child: const Icon(Icons.play_arrow_rounded), + style: ElevatedButton.styleFrom( + shape: const StadiumBorder(), + backgroundColor: LIGHT1, + fixedSize: const Size.fromHeight(48), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + ), + ), ), ), ), - ), - for (int d = 0; d < songsByDisc.length; d++) - Observer( - builder: (context) { - final bool isMultiSelectEnabled = store.selection.isMultiSelectEnabled; - final List isSelected = store.selection.isSelected.toList(); + for (int d = 0; d < songsByDisc.length; d++) + Observer( + builder: (context) { + final bool isMultiSelectEnabled = store.selection.isMultiSelectEnabled; + final List isSelected = store.selection.isSelected.toList(); - return SliverList( - delegate: SliverChildListDelegate( - [ - if (songsByDisc.length > 1 && d > 0) Container(height: 8.0), - if (songsByDisc.length > 1) - ListTile( - title: Text('${L10n.of(context)!.disc} ${d + 1}', style: TEXT_HEADER), - leading: - const SizedBox(width: 40, child: Icon(Icons.album_rounded)), - contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING), - ), - if (songsByDisc.length > 1) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_PADDING, - ), - child: Container( - height: 1.0, - color: Colors.white10, - ), - ), - for (int s = 0; s < songsByDisc[d].length; s++) - SongListTileNumbered( - song: songsByDisc[d][s], - isSelectEnabled: isMultiSelectEnabled, - isSelected: isMultiSelectEnabled && isSelected[s + discSongNums[d]], - onTap: () => audioStore.playAlbumFromIndex( - widget.album, - s + _calcOffset(d, songsByDisc), - ), - onTapMore: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => SongBottomSheet( - song: songsByDisc[d][s], - enableGoToAlbum: false, + return SliverList( + delegate: SliverChildListDelegate( + [ + if (songsByDisc.length > 1 && d > 0) Container(height: 8.0), + if (songsByDisc.length > 1) + ListTile( + title: Text( + '${L10n.of(context)!.disc} ${d + 1}', + style: TEXT_HEADER, ), + leading: const SizedBox( + width: 40, + child: Icon(Icons.album_rounded), + ), + contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING), ), - onSelect: (bool selected) => - store.selection.setSelected(selected, s + discSongNums[d]), - ) - ], - ), - ); - }, - ), - ], - ), - ); - }, + if (songsByDisc.length > 1) + const Divider(indent: HORIZONTAL_PADDING, endIndent: HORIZONTAL_PADDING), + for (int s = 0; s < songsByDisc[d].length; s++) + SongListTileNumbered( + song: songsByDisc[d][s], + isSelectEnabled: isMultiSelectEnabled, + isSelected: + isMultiSelectEnabled && isSelected[s + discSongNums[d]], + onTap: () => audioStore.playAlbumFromIndex( + widget.album, + s + _calcOffset(d, songsByDisc), + ), + onTapMore: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => SongBottomSheet( + song: songsByDisc[d][s], + enableGoToAlbum: false, + ), + ), + onSelect: (bool selected) => + store.selection.setSelected(selected, s + discSongNums[d]), + ) + ], + ), + ); + }, + ), + ], + ), + ); + }, + ), ), ); } diff --git a/src/lib/presentation/pages/artist_details_page.dart b/src/lib/presentation/pages/artist_details_page.dart index 0ec9ecb..d4f0f77 100644 --- a/src/lib/presentation/pages/artist_details_page.dart +++ b/src/lib/presentation/pages/artist_details_page.dart @@ -48,8 +48,8 @@ class _ArtistDetailsPageState extends State { final AudioStore audioStore = GetIt.I(); return Observer( - builder: (BuildContext context) => SafeArea( - child: CustomScrollView( + builder: (BuildContext context) => Scaffold( + body: CustomScrollView( slivers: [ ArtistHeader(artist: widget.artist), SliverList( diff --git a/src/lib/presentation/pages/blocked_files_page.dart b/src/lib/presentation/pages/blocked_files_page.dart index fd87f37..5e2d72e 100644 --- a/src/lib/presentation/pages/blocked_files_page.dart +++ b/src/lib/presentation/pages/blocked_files_page.dart @@ -15,46 +15,44 @@ class BlockedFilesPage extends StatelessWidget { final SettingsStore settingsStore = GetIt.I(); final NavigationStore navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.blockedFiles, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.chevron_left_rounded), - onPressed: () => navStore.pop(context), - ), - titleSpacing: 0.0, + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.blockedFiles, + style: TEXT_HEADER, ), - body: Observer( - builder: (context) { - final paths = settingsStore.blockedFilesStream.value?.toList() ?? []; - return ListView.separated( - itemCount: paths.length, - itemBuilder: (_, int index) { - final String path = paths[index]; - final split = path.split('/'); - return ListTile( - title: Text(split.last), - subtitle: Text( - split.sublist(0, split.length - 1).join('/'), - style: TEXT_SMALL_SUBTITLE, - ), - trailing: IconButton( - icon: const Icon(Icons.delete_rounded), - onPressed: () => settingsStore.removeBlockedFiles([path]), - ), - ); - }, - separatorBuilder: (BuildContext context, int index) => const SizedBox( - height: 4.0, - ), - ); - }, + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () => navStore.pop(context), ), + titleSpacing: 0.0, + ), + body: Observer( + builder: (context) { + final paths = settingsStore.blockedFilesStream.value?.toList() ?? []; + return ListView.separated( + itemCount: paths.length, + itemBuilder: (_, int index) { + final String path = paths[index]; + final split = path.split('/'); + return ListTile( + title: Text(split.last), + subtitle: Text( + split.sublist(0, split.length - 1).join('/'), + style: TEXT_SMALL_SUBTITLE, + ), + trailing: IconButton( + icon: const Icon(Icons.delete_rounded), + onPressed: () => settingsStore.removeBlockedFiles([path]), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox( + height: 4.0, + ), + ); + }, ), ); } diff --git a/src/lib/presentation/pages/cover_customization_page.dart b/src/lib/presentation/pages/cover_customization_page.dart index 2e7a534..117746c 100644 --- a/src/lib/presentation/pages/cover_customization_page.dart +++ b/src/lib/presentation/pages/cover_customization_page.dart @@ -22,112 +22,110 @@ class _CoverCustomizationPageState extends State { Widget build(BuildContext context) { print('CoverCustomizationPage.build'); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.customizeCover, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.customizeCover, + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => Navigator.of(context).pop(), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), onPressed: () => Navigator.of(context).pop(), ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - body: Observer( - builder: (context) => Column( - children: [ - Container( - color: DARK1, - width: double.infinity, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: PlaylistCover( - size: 120, - gradient: widget.store.gradient, - icon: CUSTOM_ICONS[widget.store.iconString]!, - ), + ], + ), + body: Observer( + builder: (context) => Column( + children: [ + Container( + color: DARK1, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: PlaylistCover( + size: 120, + gradient: widget.store.gradient, + icon: CUSTOM_ICONS[widget.store.iconString]!, ), ), ), - Expanded( - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_PADDING, - vertical: HORIZONTAL_PADDING, - ), - sliver: SliverGrid.count( - crossAxisCount: 5, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - children: [ - for (final gradient in CUSTOM_GRADIENTS.entries) - GestureDetector( - onTap: () => widget.store.setGradient(gradient.key), - child: Container( - decoration: BoxDecoration( - gradient: gradient.value, - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - border: Border.all( - color: gradient.key == widget.store.gradientString - ? Colors.white - : Colors.transparent, - width: 3.0, - )), - ), - ) - ], - ), + ), + Expanded( + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: HORIZONTAL_PADDING, + vertical: HORIZONTAL_PADDING, ), - SliverPadding( - padding: const EdgeInsets.only( - left: HORIZONTAL_PADDING, - right: HORIZONTAL_PADDING, - bottom: HORIZONTAL_PADDING, - ), - sliver: SliverGrid.count( - crossAxisCount: 5, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - children: [ - for (final icon in CUSTOM_ICONS.entries) - GestureDetector( - onTap: () => widget.store.setIconString(icon.key), - child: Container( - child: Center( - child: Icon(icon.value), - ), - decoration: BoxDecoration( - color: Colors.white10, + sliver: SliverGrid.count( + crossAxisCount: 5, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + children: [ + for (final gradient in CUSTOM_GRADIENTS.entries) + GestureDetector( + onTap: () => widget.store.setGradient(gradient.key), + child: Container( + decoration: BoxDecoration( + gradient: gradient.value, borderRadius: const BorderRadius.all(Radius.circular(8.0)), border: Border.all( - color: icon.key == widget.store.iconString + color: gradient.key == widget.store.gradientString ? Colors.white : Colors.transparent, width: 3.0, - ), + )), + ), + ) + ], + ), + ), + SliverPadding( + padding: const EdgeInsets.only( + left: HORIZONTAL_PADDING, + right: HORIZONTAL_PADDING, + bottom: HORIZONTAL_PADDING, + ), + sliver: SliverGrid.count( + crossAxisCount: 5, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + children: [ + for (final icon in CUSTOM_ICONS.entries) + GestureDetector( + onTap: () => widget.store.setIconString(icon.key), + child: Container( + child: Center( + child: Icon(icon.value), + ), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + border: Border.all( + color: icon.key == widget.store.iconString + ? Colors.white + : Colors.transparent, + width: 3.0, ), ), - ) - ], - ), + ), + ) + ], ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ), ), ); diff --git a/src/lib/presentation/pages/currently_playing.dart b/src/lib/presentation/pages/currently_playing.dart index 15522e3..3f0511f 100644 --- a/src/lib/presentation/pages/currently_playing.dart +++ b/src/lib/presentation/pages/currently_playing.dart @@ -2,6 +2,7 @@ import 'package:fimber/fimber.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:get_it/get_it.dart'; +import 'package:text_scroll/text_scroll.dart'; import '../../domain/entities/song.dart'; import '../state/audio_store.dart'; @@ -69,25 +70,41 @@ class CurrentlyPlayingPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0 + 12.0), child: SizedBox( width: double.infinity, - height: 74.0, + height: 58.0, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + TextScroll( song.title, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, + mode: TextScrollMode.endless, + velocity: const Velocity(pixelsPerSecond: Offset(40, 0)), + delayBefore: const Duration(milliseconds: 500), + pauseBetween: const Duration(milliseconds: 2000), + pauseOnBounce: const Duration(milliseconds: 1000), style: TEXT_BIG, + textAlign: TextAlign.left, + fadedBorder: true, + fadedBorderWidth: 0.02, + fadeBorderVisibility: FadeBorderVisibility.auto, + intervalSpaces: 30, ), - Text( + TextScroll( '${song.artist} • ${song.album}', + mode: TextScrollMode.endless, + velocity: const Velocity(pixelsPerSecond: Offset(40, 0)), + delayBefore: const Duration(milliseconds: 500), + pauseBetween: const Duration(milliseconds: 2000), + pauseOnBounce: const Duration(milliseconds: 1000), style: TextStyle( color: Colors.grey[300], fontSize: 18.0, fontWeight: FontWeight.w300, ), - maxLines: 2, + textAlign: TextAlign.left, + fadedBorder: true, + fadedBorderWidth: 0.02, + fadeBorderVisibility: FadeBorderVisibility.auto, + intervalSpaces: 30, ), ], ), diff --git a/src/lib/presentation/pages/home_page.dart b/src/lib/presentation/pages/home_page.dart index dd62528..2ee5bc9 100644 --- a/src/lib/presentation/pages/home_page.dart +++ b/src/lib/presentation/pages/home_page.dart @@ -46,85 +46,83 @@ class _HomePageInner extends StatelessWidget { final MusicDataStore musicDataStore = GetIt.I(); print('HomePage.build'); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text(L10n.of(context)!.home), - actions: [ - IconButton( - icon: const Icon(Icons.edit_rounded), - tooltip: L10n.of(context)!.customizeHomePage, - onPressed: () => navStore.push( - context, - MaterialPageRoute(builder: (context) => const HomeSettingsPage()), - ), + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context)!.home), + actions: [ + IconButton( + icon: const Icon(Icons.edit_rounded), + tooltip: L10n.of(context)!.customizeHomePage, + onPressed: () => navStore.push( + context, + MaterialPageRoute(builder: (context) => const HomeSettingsPage()), ), - IconButton( - icon: const Icon(Icons.settings_rounded), - tooltip: L10n.of(context)!.settings, - onPressed: () => navStore.push( - context, - MaterialPageRoute(builder: (context) => const SettingsPage()), - ), + ), + IconButton( + icon: const Icon(Icons.settings_rounded), + tooltip: L10n.of(context)!.settings, + onPressed: () => navStore.push( + context, + MaterialPageRoute(builder: (context) => const SettingsPage()), ), - ], - ), - body: Observer( - builder: (context) { - final songListIsEmpty = musicDataStore.songListIsEmpty; - if (songListIsEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.music_note_rounded, size: 64.0), - const SizedBox(height: 24.0), - Text( - L10n.of(context)!.noSongsYet, - style: TEXT_BIG, - textAlign: TextAlign.justify, - ), - ], - ), + ), + ], + ), + body: Observer( + builder: (context) { + final songListIsEmpty = musicDataStore.songListIsEmpty; + if (songListIsEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.music_note_rounded, size: 64.0), + const SizedBox(height: 24.0), + Text( + L10n.of(context)!.noSongsYet, + style: TEXT_BIG, + textAlign: TextAlign.justify, + ), + ], ), - ); - } - - final widgetEntities = store.homeWidgetsStream.value; - final List widgets = [ - const SliverPadding( - padding: EdgeInsets.only(top: 8.0), ), - ]; + ); + } - for (final we in widgetEntities ?? []) { - widgets.add( - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_PADDING - 8.0, - vertical: 8.0, - ), - sliver: SliverToBoxAdapter(child: we.widget()), - ), - ); - } + final widgetEntities = store.homeWidgetsStream.value; + final List widgets = [ + const SliverPadding( + padding: EdgeInsets.only(top: 8.0), + ), + ]; + for (final we in widgetEntities ?? []) { widgets.add( - const SliverPadding( - padding: EdgeInsets.only(bottom: 8.0), + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: HORIZONTAL_PADDING - 8.0, + vertical: 8.0, + ), + sliver: SliverToBoxAdapter(child: we.widget()), ), ); + } - return Scrollbar( - child: CustomScrollView( - slivers: widgets, - ), - ); - }, - ), + widgets.add( + const SliverPadding( + padding: EdgeInsets.only(bottom: 8.0), + ), + ); + + return Scrollbar( + child: CustomScrollView( + slivers: widgets, + ), + ); + }, ), ); } diff --git a/src/lib/presentation/pages/home_settings_page.dart b/src/lib/presentation/pages/home_settings_page.dart index f6875e6..e0eccff 100644 --- a/src/lib/presentation/pages/home_settings_page.dart +++ b/src/lib/presentation/pages/home_settings_page.dart @@ -23,100 +23,98 @@ class HomeSettingsPage extends StatelessWidget { final homeStore = GetIt.I(); final navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.homeCustomization, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.chevron_left_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded), - onPressed: () => _onTapAdd(context), - ), - ], + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.homeCustomization, + style: TEXT_HEADER, ), - body: Observer( - builder: (context) { - final widgetReprs = homeStore.homeWidgetsStream.value ?? []; + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.add_rounded), + onPressed: () => _onTapAdd(context), + ), + ], + ), + body: Observer( + builder: (context) { + final widgetReprs = homeStore.homeWidgetsStream.value ?? []; - return Scrollbar( - child: CustomScrollView( - slivers: [ - ReorderableSliverList( - delegate: ReorderableSliverChildBuilderDelegate( - (context, int index) { - return Dismissible( - key: UniqueKey(), - child: ListTile( - title: Text(widgetReprs[index].title(context)), - leading: Icon(widgetReprs[index].icon), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widgetReprs[index].hasParameters) - IconButton( - onPressed: () { - navStore.push( - context, - MaterialPageRoute( - builder: (context) => widgetReprs[index].formPage()!, - ), - ); - }, - icon: const Icon(Icons.edit_rounded), - ), + return Scrollbar( + child: CustomScrollView( + slivers: [ + ReorderableSliverList( + delegate: ReorderableSliverChildBuilderDelegate( + (context, int index) { + return Dismissible( + key: UniqueKey(), + child: ListTile( + title: Text(widgetReprs[index].title(context)), + leading: Icon(widgetReprs[index].icon), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widgetReprs[index].hasParameters) + IconButton( + onPressed: () { + navStore.push( + context, + MaterialPageRoute( + builder: (context) => widgetReprs[index].formPage()!, + ), + ); + }, + icon: const Icon(Icons.edit_rounded), + ), + ], + ), + contentPadding: const EdgeInsets.fromLTRB( + HORIZONTAL_PADDING, + 8.0, + 0.0, + 8.0, + ), + ), + onDismissed: (direction) { + homeStore.removeHomeWidget(widgetReprs[index].homeWidgetEntity); + }, + background: Container( + width: double.infinity, + color: RED, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Icon( + Icons.delete_forever_rounded, + color: Colors.white, + ), + Icon( + Icons.delete_forever_rounded, + color: Colors.white, + ) ], ), - contentPadding: const EdgeInsets.fromLTRB( - HORIZONTAL_PADDING, - 8.0, - 0.0, - 8.0, - ), ), - onDismissed: (direction) { - homeStore.removeHomeWidget(widgetReprs[index].homeWidgetEntity); - }, - background: Container( - width: double.infinity, - color: RED, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - Icon( - Icons.delete_forever_rounded, - color: Colors.white, - ), - Icon( - Icons.delete_forever_rounded, - color: Colors.white, - ) - ], - ), - ), - ), - ); - }, - childCount: widgetReprs.length, - ), - onReorder: (oldIndex, newIndex) { - homeStore.moveHomeWidget(oldIndex, newIndex); + ), + ); }, + childCount: widgetReprs.length, ), - ], - ), - ); - }, - ), + onReorder: (oldIndex, newIndex) { + homeStore.moveHomeWidget(oldIndex, newIndex); + }, + ), + ], + ), + ); + }, ), ); } diff --git a/src/lib/presentation/pages/library_folders_page.dart b/src/lib/presentation/pages/library_folders_page.dart index f9bdab4..f7a26c3 100644 --- a/src/lib/presentation/pages/library_folders_page.dart +++ b/src/lib/presentation/pages/library_folders_page.dart @@ -17,47 +17,45 @@ class LibraryFoldersPage extends StatelessWidget { final SettingsStore settingsStore = GetIt.I(); final NavigationStore navStore = GetIt.I(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.libraryFolders, - style: TEXT_HEADER, + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.libraryFolders, + style: TEXT_HEADER, + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.add_rounded), + onPressed: () => _openFilePicker(settingsStore), ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.chevron_left_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - IconButton( - icon: const Icon(Icons.add_rounded), - onPressed: () => _openFilePicker(settingsStore), + ], + titleSpacing: 0.0, + ), + body: Observer( + builder: (context) { + final folders = settingsStore.libraryFoldersStream.value ?? []; + return ListView.separated( + itemCount: folders.length, + itemBuilder: (_, int index) { + final String path = folders[index]; + return ListTile( + title: Text(path), + trailing: IconButton( + icon: const Icon(Icons.delete_rounded), + onPressed: () => settingsStore.removeLibraryFolder(path), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox( + height: 4.0, ), - ], - titleSpacing: 0.0, - ), - body: Observer( - builder: (context) { - final folders = settingsStore.libraryFoldersStream.value ?? []; - return ListView.separated( - itemCount: folders.length, - itemBuilder: (_, int index) { - final String path = folders[index]; - return ListTile( - title: Text(path), - trailing: IconButton( - icon: const Icon(Icons.delete_rounded), - onPressed: () => settingsStore.removeLibraryFolder(path), - ), - ); - }, - separatorBuilder: (BuildContext context, int index) => const SizedBox( - height: 4.0, - ), - ); - }, - ), + ); + }, ), ); } diff --git a/src/lib/presentation/pages/library_tab_container.dart b/src/lib/presentation/pages/library_tab_container.dart index 84e51d7..c3125da 100644 --- a/src/lib/presentation/pages/library_tab_container.dart +++ b/src/lib/presentation/pages/library_tab_container.dart @@ -13,45 +13,45 @@ class LibraryTabContainer extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 4, - child: SafeArea( - child: Column( - children: [ - Container( - color: Theme.of(context).primaryColor, - child: Padding( - padding: const EdgeInsets.only(top: 8.0, left: 4.0), - child: Row( - children: [ - Expanded( - child: TabBar( - indicatorColor: Theme.of(context).highlightColor, - indicatorSize: TabBarIndicatorSize.label, - indicatorWeight: 3.0, - labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), - unselectedLabelColor: Colors.white30, - isScrollable: true, - tabs: [ - Tab(text: L10n.of(context)!.artists), - Tab(text: L10n.of(context)!.albums), - Tab(text: L10n.of(context)!.songs), - Tab(text: L10n.of(context)!.playlists), - ], - ), + child: Scaffold( + appBar: AppBar( + toolbarHeight: 8.0, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(48), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Align( + alignment: Alignment.centerLeft, + child: TabBar( + indicator: UnderlineTabIndicator( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: Theme.of(context).highlightColor, + width: 3.0, ), + ), + indicatorSize: TabBarIndicatorSize.label, + labelPadding: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 3.0), + unselectedLabelColor: Colors.white30, + isScrollable: true, + dividerColor: Colors.transparent, + tabs: [ + Tab(text: L10n.of(context)!.artists), + Tab(text: L10n.of(context)!.albums), + Tab(text: L10n.of(context)!.songs), + Tab(text: L10n.of(context)!.playlists), ], ), ), ), - const Expanded( - child: TabBarView( - children: [ - ArtistsPage(key: PageStorageKey('ArtistsPage')), - AlbumsPage(key: PageStorageKey('AlbumsPage')), - SongsPage(key: PageStorageKey('SongsPage')), - PlaylistsPage(key: PageStorageKey('PlaylistsPage')) - ], - ), - ) + ), + ), + body: const TabBarView( + children: [ + ArtistsPage(key: PageStorageKey('ArtistsPage')), + AlbumsPage(key: PageStorageKey('AlbumsPage')), + SongsPage(key: PageStorageKey('SongsPage')), + PlaylistsPage(key: PageStorageKey('PlaylistsPage')) ], ), ), diff --git a/src/lib/presentation/pages/playlist_form_page.dart b/src/lib/presentation/pages/playlist_form_page.dart index 6989d48..c67e8b7 100644 --- a/src/lib/presentation/pages/playlist_form_page.dart +++ b/src/lib/presentation/pages/playlist_form_page.dart @@ -8,8 +8,7 @@ import '../state/music_data_store.dart'; import '../state/navigation_store.dart'; import '../state/playlist_form_store.dart'; import '../theming.dart'; -import '../widgets/playlist_cover.dart'; -import 'cover_customization_page.dart'; +import '../widgets/cover_customization_card.dart'; class PlaylistFormPage extends StatefulWidget { const PlaylistFormPage({Key? key, this.playlist}) : super(key: key); @@ -52,159 +51,121 @@ class _PlaylistFormPageState extends State { L10n.of(context)!.favShuffleMode, ]; - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - title, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - if (widget.playlist != null) - IconButton( - icon: const Icon(Icons.delete_rounded), - onPressed: () async { - // TODO: this works, but may only pop back to the smartlist page... - // can I use pop 2x here? - await musicDataStore.removePlaylist(widget.playlist!); - navStore.pop(context); - }, - ), + return Scaffold( + appBar: AppBar( + title: Text( + title, + style: TEXT_HEADER, + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + if (widget.playlist != null) IconButton( - icon: const Icon(Icons.check_rounded), + icon: const Icon(Icons.delete_rounded), onPressed: () async { - store.validateAll(); - if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - } + // TODO: this works, but may only pop back to the smartlist page... + // can I use pop 2x here? + await musicDataStore.removePlaylist(widget.playlist!); + navStore.pop(context); }, ), - ], - titleSpacing: 0.0, - ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Observer( - builder: (_) => TextFormField( - initialValue: store.name, - onChanged: (value) => store.name = value, - style: TEXT_HEADER, - decoration: InputDecoration( - labelText: L10n.of(context)!.name, - labelStyle: const TextStyle(color: Colors.white), - floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), - errorText: store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null, - errorStyle: const TextStyle(color: RED), - filled: true, - fillColor: DARK35, - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 12.0, - ), + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + store.validateAll(); + if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + } + }, + ), + ], + titleSpacing: 0.0, + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Observer( + builder: (_) => TextFormField( + initialValue: store.name, + onChanged: (value) => store.name = value, + style: TEXT_HEADER, + decoration: InputDecoration( + labelText: L10n.of(context)!.name, + labelStyle: const TextStyle(color: Colors.white), + floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), + errorText: + store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null, + errorStyle: const TextStyle(color: RED), + filled: true, + fillColor: DARK35, + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 12.0, ), ), ), ), - const SizedBox(height: 4.0), - Observer( - builder: (context) => Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - horizontal: 10.0, - ), - child: GestureDetector( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => CoverCustomizationPage( - store: store.cover, + ), + const SizedBox(height: 4.0), + Observer( + builder: (context) => CoverCustomizationCard(store: store.cover), + ), + const SizedBox(height: 8.0), + ListTile( + title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2, 3].map>((int value) { + return RadioListTile( + title: Text( + playbackModeTexts[value], + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, ), ), + value: value, + groupValue: store.shuffleModeIndex, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) store.setShuffleModeIndex(newValue); + }); + }, ); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - PlaylistCover( - size: 64.0, - gradient: store.cover.gradient, - icon: store.cover.icon, - ), - const SizedBox(width: 16.0), - Text(L10n.of(context)!.customizeCover), - const Spacer(), - const SizedBox( - width: 56.0, - child: Icon(Icons.chevron_right_rounded), - ), - const SizedBox(width: 6.0), - ], - ), - ), - ), - ), + }).toList(), + ); + }, ), ), - const SizedBox(height: 8.0), - ListTile( - title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2, 3].map>((int value) { - return RadioListTile( - title: Text( - playbackModeTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), - ), - value: value, - groupValue: store.shuffleModeIndex, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) store.setShuffleModeIndex(newValue); - }); - }, - ); - }).toList(), - ); - }, - ), - ), - ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/pages/playlist_page.dart b/src/lib/presentation/pages/playlist_page.dart index c13c08e..9912a6c 100644 --- a/src/lib/presentation/pages/playlist_page.dart +++ b/src/lib/presentation/pages/playlist_page.dart @@ -19,6 +19,7 @@ import '../widgets/bottom_sheet/add_to_playlist.dart'; import '../widgets/bottom_sheet/remove_from_playlist.dart'; import '../widgets/cover_sliver_appbar.dart'; import '../widgets/custom_modal_bottom_sheet.dart'; +import '../widgets/play_shuffle_button.dart'; import '../widgets/playlist_cover.dart'; import '../widgets/song_bottom_sheet.dart'; import '../widgets/song_list_tile.dart'; @@ -156,31 +157,18 @@ class _PlaylistPageState extends State { ), ], title: playlist.name, - subtitle2: '${L10n.of(context)!.nSongs(songs.length).capitalize()} • ${utils.msToTimeString(totalDuration)}', - background: Container( - decoration: BoxDecoration( - gradient: playlist.gradient, - ), - ), + subtitle2: + '${L10n.of(context)!.nSongs(songs.length).capitalize()} • ${utils.msToTimeString(totalDuration)}', + backgroundColor: utils.bgColor(playlist.gradient.colors.first), cover: PlaylistCover( size: 120, icon: playlist.icon, gradient: playlist.gradient, ), - button: ElevatedButton( + button: PlayShuffleButton( onPressed: () => audioStore.playPlaylist(playlist), - child: Row( - children: [ - Expanded(child: Center(child: Text(L10n.of(context)!.play))), - Icon(playIcon), - ], - ), - style: ElevatedButton.styleFrom( - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - backgroundColor: Colors.white10, - shadowColor: Colors.transparent, - ), + shuffleMode: playlist.shuffleMode, + size: 56, ), ), Observer( @@ -197,8 +185,6 @@ class _PlaylistPageState extends State { key: ValueKey(song.path), child: SongListTile( song: song, - showAlbum: true, - subtitle: Subtitle.artistAlbum, onTap: () => audioStore.playSong(index, songs, playlist), onTapMore: () => showModalBottomSheet( context: context, diff --git a/src/lib/presentation/pages/playlists_page.dart b/src/lib/presentation/pages/playlists_page.dart index 80eaeaf..8e91eee 100644 --- a/src/lib/presentation/pages/playlists_page.dart +++ b/src/lib/presentation/pages/playlists_page.dart @@ -103,6 +103,7 @@ class _PlaylistsPageState extends State with AutomaticKeepAliveCl ), floatingActionButton: SpeedDial( child: const Icon(Icons.add_rounded), + backgroundColor: Theme.of(context).highlightColor, activeChild: Transform.rotate( angle: pi / 4, child: const Icon(Icons.add_rounded), @@ -125,6 +126,7 @@ class _PlaylistsPageState extends State with AutomaticKeepAliveCl builder: (BuildContext context) => const SmartListFormPage(), ), ), + shape: const CircleBorder(), ), SpeedDialChild( child: const Icon(Icons.playlist_add_rounded), @@ -137,6 +139,7 @@ class _PlaylistsPageState extends State with AutomaticKeepAliveCl builder: (BuildContext context) => const PlaylistFormPage(), ), ), + shape: const CircleBorder(), ), ], ), diff --git a/src/lib/presentation/pages/queue_page.dart b/src/lib/presentation/pages/queue_page.dart index 5015e1d..721b23f 100644 --- a/src/lib/presentation/pages/queue_page.dart +++ b/src/lib/presentation/pages/queue_page.dart @@ -38,187 +38,181 @@ class QueuePage extends StatelessWidget { final ScrollController _scrollController = ScrollController(initialScrollOffset: initialIndex * 72.0); - return SafeArea( - child: Scaffold( - appBar: AppBar( - leading: Padding( - padding: const EdgeInsets.only(left: HORIZONTAL_PADDING), - child: SizedBox( - width: 56.0, - child: IconButton( - icon: const Icon(Icons.expand_more_rounded), - onPressed: () => Navigator.pop(context), - ), + return Scaffold( + appBar: AppBar( + leading: Padding( + padding: const EdgeInsets.only(left: HORIZONTAL_PADDING), + child: SizedBox( + width: 56.0, + child: IconButton( + icon: const Icon(Icons.expand_more_rounded), + onPressed: () => Navigator.pop(context), ), ), - leadingWidth: 56 + HORIZONTAL_PADDING, - toolbarHeight: 80.0, - title: Observer( - builder: (context) { - final playable = audioStore.playableStream.value; - final numAvailableSongs = audioStore.numAvailableSongs; - - Widget subTitle = Container(); - - if (playable != null) { - subTitle = Text( - playable.repr(context), - maxLines: 1, - ); - } - - String text = L10n.of(context)!.nSongsInQueue(audioStore.queueLength); - if (numAvailableSongs > 0) text += ', ${L10n.of(context)!.moreAvailable(numAvailableSongs)}'; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.of(context)!.currentlyPlaying.toUpperCase(), - style: TEXT_SMALL_HEADLINE, - ), - subTitle, - const SizedBox(height: 4.0), - Text( - text, - style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70), - ), - ], - ); - }, - ), - centerTitle: false, - actions: [ - Observer( - builder: (context) { - final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; - - if (isMultiSelectEnabled) - return IconButton( - key: GlobalKey(), - icon: const Icon(Icons.more_vert_rounded), - onPressed: () => _openMultiselectMenu(context), - ); - - return Container(); - }, - ), - Observer( - builder: (context) { - final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; - final isAllSelected = queuePageStore.isAllSelected; - - if (isMultiSelectEnabled) - return IconButton( - key: GlobalKey(), - icon: isAllSelected - ? const Icon(Icons.deselect_rounded) - : const Icon(Icons.select_all_rounded), - onPressed: () { - if (isAllSelected) - queuePageStore.deselectAll(); - else - queuePageStore.selectAll(); - }, - ); - - return Container(); - }, - ), - Observer(builder: (context) { - final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; - return IconButton( - key: const ValueKey('QUEUE_MULTISELECT'), - icon: isMultiSelectEnabled - ? const Icon(Icons.close_rounded) - : const Icon(Icons.checklist_rtl_rounded), - onPressed: () => queuePageStore.toggleMultiSelect(), - ); - }) - ], ), - body: Observer( - builder: (BuildContext context) { - print('QueuePage.build -> Observer.build'); - final queue = audioStore.queue; - final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; - final isSelected = queuePageStore.isSelected.toList(); + leadingWidth: 56 + HORIZONTAL_PADDING, + toolbarHeight: 80.0, + title: Observer( + builder: (context) { + final playable = audioStore.playableStream.value; + final numAvailableSongs = audioStore.numAvailableSongs; - while (isSelected.length < queue.length) isSelected.add(false); + Widget subTitle = Container(); - final int activeIndex = queueIndexStream.value ?? -1; - return Scrollbar( - child: CustomScrollView( - controller: _scrollController, - slivers: [ - ReorderableSliverList( - delegate: ReorderableSliverChildBuilderDelegate( - (context, int index) { - final QueueItem? queueItem = queue[index]; - if (queueItem == null) return Container(); - return Dismissible( - key: ValueKey(queueItem.toString()), - child: SongListTile( - song: queueItem.song, - highlight: index == activeIndex, - source: queueItem.source, - isSelectEnabled: isMultiSelectEnabled, - isSelected: isMultiSelectEnabled && isSelected[index], - onTap: () async { - await audioStore.seekToIndex(index); - audioStore.play(); - }, - onTapMore: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => SongBottomSheet( - song: queueItem.song, - enableQueueActions: false, - numNavPop: 3, - ), - ), - onSelect: (bool selected) => - queuePageStore.setSelected(selected, index), - ), - background: Container( - width: double.infinity, - color: RED, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - Icon( - Icons.playlist_remove_rounded, - color: Colors.white, - ), - Icon( - Icons.playlist_remove_rounded, - color: Colors.white, - ) - ], - ), - ), - ), - onDismissed: (direction) { - audioStore.removeQueueIndices([index]); - }, - confirmDismiss: (direction) async => !isMultiSelectEnabled, - ); - }, - childCount: queue.length, - ), - onReorder: (oldIndex, newIndex) => audioStore.moveQueueItem(oldIndex, newIndex), - ) - ], - ), + if (playable != null) { + subTitle = Text( + playable.repr(), + maxLines: 1, + ); + } + + String text = L10n.of(context)!.nSongsInQueue(audioStore.queueLength); + if (numAvailableSongs > 0) text += ', ${L10n.of(context)!.moreAvailable(numAvailableSongs)}'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + subTitle, + const SizedBox(height: 4.0), + Text( + text, + style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white70), + ), + ], ); }, ), - bottomNavigationBar: const QueueControlBar(), + centerTitle: false, + actions: [ + Observer( + builder: (context) { + final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; + + if (isMultiSelectEnabled) + return IconButton( + key: GlobalKey(), + icon: const Icon(Icons.more_vert_rounded), + onPressed: () => _openMultiselectMenu(context), + ); + + return Container(); + }, + ), + Observer( + builder: (context) { + final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; + final isAllSelected = queuePageStore.isAllSelected; + + if (isMultiSelectEnabled) + return IconButton( + key: GlobalKey(), + icon: isAllSelected + ? const Icon(Icons.deselect_rounded) + : const Icon(Icons.select_all_rounded), + onPressed: () { + if (isAllSelected) + queuePageStore.deselectAll(); + else + queuePageStore.selectAll(); + }, + ); + + return Container(); + }, + ), + Observer(builder: (context) { + final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; + return IconButton( + key: const ValueKey('QUEUE_MULTISELECT'), + icon: isMultiSelectEnabled + ? const Icon(Icons.close_rounded) + : const Icon(Icons.checklist_rtl_rounded), + onPressed: () => queuePageStore.toggleMultiSelect(), + ); + }) + ], ), + body: Observer( + builder: (BuildContext context) { + print('QueuePage.build -> Observer.build'); + final queue = audioStore.queue; + final isMultiSelectEnabled = queuePageStore.isMultiSelectEnabled; + final isSelected = queuePageStore.isSelected.toList(); + + while (isSelected.length < queue.length) isSelected.add(false); + + final int activeIndex = queueIndexStream.value ?? -1; + return Scrollbar( + child: CustomScrollView( + controller: _scrollController, + slivers: [ + ReorderableSliverList( + delegate: ReorderableSliverChildBuilderDelegate( + (context, int index) { + final QueueItem? queueItem = queue[index]; + if (queueItem == null) return Container(); + return Dismissible( + key: ValueKey(queueItem.toString()), + child: SongListTile( + song: queueItem.song, + highlight: index == activeIndex, + source: queueItem.source, + isSelectEnabled: isMultiSelectEnabled, + isSelected: isMultiSelectEnabled && isSelected[index], + onTap: () async { + await audioStore.seekToIndex(index); + audioStore.play(); + }, + onTapMore: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => SongBottomSheet( + song: queueItem.song, + enableQueueActions: false, + numNavPop: 3, + ), + ), + onSelect: (bool selected) => + queuePageStore.setSelected(selected, index), + ), + background: Container( + width: double.infinity, + color: RED, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + Icon( + Icons.playlist_remove_rounded, + color: Colors.white, + ), + Icon( + Icons.playlist_remove_rounded, + color: Colors.white, + ) + ], + ), + ), + ), + onDismissed: (direction) { + audioStore.removeQueueIndices([index]); + }, + confirmDismiss: (direction) async => !isMultiSelectEnabled, + ); + }, + childCount: queue.length, + ), + onReorder: (oldIndex, newIndex) => audioStore.moveQueueItem(oldIndex, newIndex), + ) + ], + ), + ); + }, + ), + bottomNavigationBar: const QueueControlBar(), ); } diff --git a/src/lib/presentation/pages/search_page.dart b/src/lib/presentation/pages/search_page.dart index 3252d84..906eb6b 100644 --- a/src/lib/presentation/pages/search_page.dart +++ b/src/lib/presentation/pages/search_page.dart @@ -64,365 +64,358 @@ class _SearchPageState extends State { String searchText = ''; - return SafeArea( - child: Column( - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, - left: HORIZONTAL_PADDING - 8.0, - right: HORIZONTAL_PADDING - 8.0, - bottom: 8.0, - ), - child: StatefulBuilder(builder: (context, setState) { - return TextField( - controller: _textController, - decoration: InputDecoration( - hintText: L10n.of(context)!.search, - hintStyle: TEXT_HEADER.copyWith(color: Colors.white), - fillColor: Colors.white10, - filled: true, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - gapPadding: 0.0, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - gapPadding: 0.0, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - suffixIcon: searchText.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear_rounded), - color: Colors.white, - onPressed: () { - setState(() { - searchText = ''; - _textController.text = ''; - }); - searchStore.reset(); - }, - ) - : const SizedBox.shrink(), - ), - onChanged: (text) { - setState(() => searchText = text); - searchStore.search(text); - }, - focusNode: _searchFocus, - ); - }), + return Scaffold( + appBar: AppBar( + titleSpacing: 8.0, + title: Padding( + padding: const EdgeInsets.only( + top: 11.0, + bottom: 8.0, + ), + child: StatefulBuilder(builder: (context, setState) { + return TextField( + controller: _textController, + decoration: InputDecoration( + hintText: L10n.of(context)!.search, + hintStyle: TEXT_HEADER.copyWith(color: Colors.white), + fillColor: Colors.white10, + filled: true, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + gapPadding: 0.0, + borderRadius: BorderRadius.all(Radius.circular(8.0)), ), - Padding( - padding: const EdgeInsets.only( - left: HORIZONTAL_PADDING - 8.0, - right: HORIZONTAL_PADDING - 8.0, - bottom: 8.0, + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + gapPadding: 0.0, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + suffixIcon: searchText.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + color: Colors.white, + onPressed: () { + setState(() { + searchText = ''; + _textController.text = ''; + }); + searchStore.reset(); + }, + ) + : const SizedBox.shrink(), + ), + onChanged: (text) { + setState(() => searchText = text); + searchStore.search(text); + }, + focusNode: _searchFocus, + ); + }), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: Padding( + padding: const EdgeInsets.only( + left: HORIZONTAL_PADDING - 8.0, + right: HORIZONTAL_PADDING - 8.0, + bottom: 8.0, + ), + child: Observer( + builder: (context) { + final artists = searchStore.searchResultsArtists; + final albums = searchStore.searchResultsAlbums; + final songs = searchStore.searchResultsSongs; + final smartlists = searchStore.searchResultsSmartLists; + final playlists = searchStore.searchResultsPlaylists; + + final artistHeight = + artists.length * 56.0 + artists.isNotEmpty.toDouble() * (16.0 + 56.0); + final albumsHeight = + albums.length * 72.0 + albums.isNotEmpty.toDouble() * (16.0 + 56.0); + final songsHeight = + songs.length * 72.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0); + final smartListsHeight = + smartlists.length * 72.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: artists.isEmpty + ? null + : () => _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.person_rounded), + ), + IconButton( + onPressed: albums.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.album_rounded), + ), + IconButton( + onPressed: songs.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.audiotrack_rounded), + ), + IconButton( + onPressed: smartlists.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight + songsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.auto_awesome_rounded), + ), + IconButton( + onPressed: playlists.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight + songsHeight + smartListsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.queue_music_rounded), + ), + ], + ); + }, + ), + ), + ), + ), + body: Observer(builder: (context) { + final artists = searchStore.searchResultsArtists; + final albums = searchStore.searchResultsAlbums; + final songs = searchStore.searchResultsSongs; + final smartlists = searchStore.searchResultsSmartLists; + final playlists = searchStore.searchResultsPlaylists; + + if (_textController.text.isEmpty) { + return const Center( + child: Icon( + Icons.search_rounded, + size: 192, + color: Colors.white30, + ), + ); + } + + return Scrollbar( + controller: _scrollController, + child: CustomScrollView( + controller: _scrollController, + slivers: [ + if (artists.isNotEmpty) ...[ + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + L10n.of(context)!.artists, + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (final artist in artists) + ListTile( + title: Text(artist.name), + leading: const SizedBox( + child: Icon(Icons.person_rounded), + width: 56.0, + height: 56.0, + ), + onTap: () { + _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => ArtistDetailsPage( + artist: artist, + ), + ), + ); + }, + ), + const SizedBox(height: 16.0), + ], ), - child: Observer(builder: (context) { - final artists = searchStore.searchResultsArtists; - final albums = searchStore.searchResultsAlbums; - final songs = searchStore.searchResultsSongs; - final smartlists = searchStore.searchResultsSmartLists; - final playlists = searchStore.searchResultsPlaylists; - - final artistHeight = - artists.length * 56.0 + artists.isNotEmpty.toDouble() * (16.0 + 56.0); - final albumsHeight = - albums.length * 72.0 + albums.isNotEmpty.toDouble() * (16.0 + 56.0); - final songsHeight = - songs.length * 56.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0); - final smartListsHeight = - smartlists.length * 56.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0); - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: artists.isEmpty - ? null - : () => _scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - icon: const Icon(Icons.person_rounded), - ), - IconButton( - onPressed: albums.isEmpty - ? null - : () => _scrollController.animateTo( - artistHeight, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - icon: const Icon(Icons.album_rounded), - ), - IconButton( - onPressed: songs.isEmpty - ? null - : () => _scrollController.animateTo( - artistHeight + albumsHeight, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - icon: const Icon(Icons.audiotrack_rounded), - ), - IconButton( - onPressed: smartlists.isEmpty - ? null - : () => _scrollController.animateTo( - artistHeight + albumsHeight + songsHeight, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - icon: const Icon(Icons.auto_awesome_rounded), - ), - IconButton( - onPressed: playlists.isEmpty - ? null - : () => _scrollController.animateTo( - artistHeight + albumsHeight + songsHeight + smartListsHeight, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - icon: const Icon(Icons.queue_music_rounded), - ), - ], - ); - }), ), ], - ), - ), - Expanded( - child: Observer(builder: (context) { - final artists = searchStore.searchResultsArtists; - final albums = searchStore.searchResultsAlbums; - final songs = searchStore.searchResultsSongs; - final smartlists = searchStore.searchResultsSmartLists; - final playlists = searchStore.searchResultsPlaylists; - - if (_textController.text.isEmpty) { - return const Center( - child: Icon( - Icons.search_rounded, - size: 192, - color: Colors.white30, + if (albums.isNotEmpty) ...[ + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + L10n.of(context)!.albums, + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (final album in albums) + AlbumArtListTile( + title: album.title, + subtitle: album.artist, + albumArtPath: album.albumArtPath, + onTap: () { + _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => AlbumDetailsPage( + album: album, + ), + ), + ); + }, + ), + const SizedBox(height: 16.0), + ], ), - ); - } - - return Scrollbar( - controller: _scrollController, - child: CustomScrollView( - controller: _scrollController, - slivers: [ - if (artists.isNotEmpty) ...[ - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text( - L10n.of(context)!.artists, - style: TEXT_HEADER.underlined( - textColor: Colors.white, - underlineColor: LIGHT1, - thickness: 4, - distance: 8, - ), - ), - ), - for (final artist in artists) - ListTile( - title: Text(artist.name), - leading: const SizedBox( - child: Icon(Icons.person_rounded), - width: 56.0, - height: 56.0, - ), - onTap: () { - _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => ArtistDetailsPage( - artist: artist, - ), - ), - ); - }, - ), - const SizedBox(height: 16.0), - ], - ), - ), - ], - if (albums.isNotEmpty) ...[ - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text( - L10n.of(context)!.albums, - style: TEXT_HEADER.underlined( - textColor: Colors.white, - underlineColor: LIGHT1, - thickness: 4, - distance: 8, - ), - ), - ), - for (final album in albums) - AlbumArtListTile( - title: album.title, - subtitle: album.artist, - albumArtPath: album.albumArtPath, - onTap: () { - _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => AlbumDetailsPage( - album: album, - ), - ), - ); - }, - ), - const SizedBox(height: 16.0), - ], - ), - ), - ], - if (songs.isNotEmpty) ...[ - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text( - L10n.of(context)!.songs, - style: TEXT_HEADER.underlined( - textColor: Colors.white, - underlineColor: LIGHT1, - thickness: 4, - distance: 8, - ), - ), - ), - for (int i in songs.asMap().keys) - SongListTile( - song: songs[i], - onTap: () => audioStore.playSong( - i, - songs, - SearchQuery(searchText), - ), - onTapMore: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => SongBottomSheet( - song: songs[i], - ), - ), - onSelect: () {}, - ), - const SizedBox(height: 16.0), - ], - ), - ), - ], - if (smartlists.isNotEmpty) - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text( - L10n.of(context)!.smartlists, - style: TEXT_HEADER.underlined( - textColor: Colors.white, - underlineColor: LIGHT1, - thickness: 4, - distance: 8, - ), - ), - ), - for (int i in smartlists.asMap().keys) - ListTile( - title: Text(smartlists[i].name), - leading: PlaylistCover( - size: 56, - gradient: smartlists[i].gradient, - icon: smartlists[i].icon, - ), - onTap: () => _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => - SmartListPage(smartList: smartlists[i]), - ), - ), - trailing: PlayShuffleButton( - size: 48.0, - shuffleMode: smartlists[i].shuffleMode, - onPressed: () => audioStore.playSmartList(smartlists[i]), - ), - ), - const SizedBox(height: 16.0), - ], - ), - ), - if (playlists.isNotEmpty) - SliverList( - delegate: SliverChildListDelegate( - [ - ListTile( - title: Text( - L10n.of(context)!.playlists, - style: TEXT_HEADER.underlined( - textColor: Colors.white, - underlineColor: LIGHT1, - thickness: 4, - distance: 8, - ), - ), - ), - for (int i in playlists.asMap().keys) - ListTile( - title: Text(playlists[i].name), - leading: PlaylistCover( - size: 56, - gradient: playlists[i].gradient, - icon: playlists[i].icon, - ), - onTap: () => _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => - PlaylistPage(playlist: playlists[i]), - ), - ), - trailing: PlayShuffleButton( - size: 48.0, - onPressed: () => audioStore.playPlaylist(playlists[i]), - shuffleMode: playlists[i].shuffleMode, - ), - ), - const SizedBox(height: 16.0), - ], - ), - ), - ], ), - ); - }), + ], + if (songs.isNotEmpty) ...[ + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + L10n.of(context)!.songs, + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (int i in songs.asMap().keys) + SongListTile( + song: songs[i], + onTap: () => audioStore.playSong( + i, + songs, + SearchQuery(searchText), + ), + onTapMore: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => SongBottomSheet( + song: songs[i], + ), + ), + onSelect: () {}, + ), + const SizedBox(height: 16.0), + ], + ), + ), + ], + if (smartlists.isNotEmpty) + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + L10n.of(context)!.smartlists, + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (int i in smartlists.asMap().keys) + ListTile( + title: Text(smartlists[i].name), + leading: PlaylistCover( + size: 56, + gradient: smartlists[i].gradient, + icon: smartlists[i].icon, + ), + onTap: () => _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => + SmartListPage(smartList: smartlists[i]), + ), + ), + trailing: PlayShuffleButton( + size: 40.0, + shuffleMode: smartlists[i].shuffleMode, + onPressed: () => audioStore.playSmartList(smartlists[i]), + ), + contentPadding: const EdgeInsets.fromLTRB(HORIZONTAL_PADDING, 8.0, 4.0, 8.0), + ), + const SizedBox(height: 16.0), + ], + ), + ), + if (playlists.isNotEmpty) + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + L10n.of(context)!.playlists, + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (int i in playlists.asMap().keys) + ListTile( + title: Text(playlists[i].name), + leading: PlaylistCover( + size: 56, + gradient: playlists[i].gradient, + icon: playlists[i].icon, + ), + onTap: () => _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => + PlaylistPage(playlist: playlists[i]), + ), + ), + trailing: PlayShuffleButton( + size: 40.0, + onPressed: () => audioStore.playPlaylist(playlists[i]), + shuffleMode: playlists[i].shuffleMode, + ), + contentPadding: const EdgeInsets.fromLTRB(HORIZONTAL_PADDING, 8.0, 4.0, 8.0), + ), + const SizedBox(height: 16.0), + ], + ), + ), + ], ), - ], - ), + ); + }), ); } } diff --git a/src/lib/presentation/pages/settings_page.dart b/src/lib/presentation/pages/settings_page.dart index f8ebc7a..421e3fc 100644 --- a/src/lib/presentation/pages/settings_page.dart +++ b/src/lib/presentation/pages/settings_page.dart @@ -24,154 +24,152 @@ class SettingsPage extends StatelessWidget { final TextEditingController _textController = TextEditingController(); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.settings, - style: TEXT_HEADER, - ), - leading: IconButton( - icon: const Icon(Icons.chevron_left_rounded), - onPressed: () => navStore.pop(context), - ), - centerTitle: true, + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.settings, + style: TEXT_HEADER, ), - body: ListView( - children: [ - SettingsSection(text: L10n.of(context)!.library), - ListTile( - title: Text(L10n.of(context)!.updateLibrary), - subtitle: Observer(builder: (_) { - final int artistCount = musicDataStore.artistStream.value?.length ?? 0; - final int albumCount = musicDataStore.albumStream.value?.length ?? 0; - final int songCount = musicDataStore.songStream.value?.length ?? 0; - return Text( - L10n.of(context)!.artistsAlbumsSongs(artistCount, albumCount, songCount), - ); - }), - onTap: () => musicDataStore.updateDatabase(), - trailing: Observer(builder: (_) { - if (musicDataStore.isUpdatingDatabase) { - return const CircularProgressIndicator(); - } - return Container( - height: 0, - width: 0, - ); - }), - ), - const Divider( - height: 4.0, - ), - ListTile( - title: Text(L10n.of(context)!.manageLibraryFolders), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => navStore.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const LibraryFoldersPage(), - ), + leading: IconButton( + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () => navStore.pop(context), + ), + centerTitle: true, + ), + body: ListView( + children: [ + SettingsSection(text: L10n.of(context)!.library), + ListTile( + title: Text(L10n.of(context)!.updateLibrary), + subtitle: Observer(builder: (_) { + final int artistCount = musicDataStore.artistStream.value?.length ?? 0; + final int albumCount = musicDataStore.albumStream.value?.length ?? 0; + final int songCount = musicDataStore.songStream.value?.length ?? 0; + return Text( + L10n.of(context)!.artistsAlbumsSongs(artistCount, albumCount, songCount), + ); + }), + onTap: () => musicDataStore.updateDatabase(), + trailing: Observer(builder: (_) { + if (musicDataStore.isUpdatingDatabase) { + return const CircularProgressIndicator(); + } + return Container( + height: 0, + width: 0, + ); + }), + ), + const Divider( + height: 4.0, + ), + ListTile( + title: Text(L10n.of(context)!.manageLibraryFolders), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () => navStore.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => const LibraryFoldersPage(), ), ), - const Divider( - height: 4.0, + ), + const Divider( + height: 4.0, + ), + ListTile( + title: Text(L10n.of(context)!.allowedFileExtensions), + subtitle: Text( + L10n.of(context)!.allowedFileExtensionsDescription, + style: TEXT_SMALL_SUBTITLE, ), - ListTile( - title: Text(L10n.of(context)!.allowedFileExtensions), - subtitle: Text( - L10n.of(context)!.allowedFileExtensionsDescription, - style: TEXT_SMALL_SUBTITLE, - ), + ), + Padding( + padding: const EdgeInsets.only( + left: HORIZONTAL_PADDING, + right: HORIZONTAL_PADDING, + bottom: 16.0, ), - Padding( - padding: const EdgeInsets.only( - left: HORIZONTAL_PADDING, - right: HORIZONTAL_PADDING, - bottom: 16.0, - ), - child: Observer( - builder: (context) { - final text = settingsStore.fileExtensionsStream.value; - if (text == null) return Container(); - if (_textController.text == '') _textController.text = text; + child: Observer( + builder: (context) { + final text = settingsStore.fileExtensionsStream.value; + if (text == null) return Container(); + if (_textController.text == '') _textController.text = text; - return Row( - children: [ - Expanded( - child: TextFormField( - controller: _textController, - onChanged: (value) => settingsStore.setFileExtensions(value), - textAlign: TextAlign.start, - decoration: const InputDecoration( - filled: true, - fillColor: DARK35, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - errorStyle: TextStyle(height: 0, fontSize: 0), - contentPadding: EdgeInsets.only( - top: 0.0, - bottom: 0.0, - left: 6.0, - right: 6.0, - ), + return Row( + children: [ + Expanded( + child: TextFormField( + controller: _textController, + onChanged: (value) => settingsStore.setFileExtensions(value), + textAlign: TextAlign.start, + decoration: const InputDecoration( + filled: true, + fillColor: DARK35, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + errorStyle: TextStyle(height: 0, fontSize: 0), + contentPadding: EdgeInsets.only( + top: 0.0, + bottom: 0.0, + left: 6.0, + right: 6.0, ), ), ), - const SizedBox(width: 8.0), - IconButton( - icon: const Icon(Icons.settings_backup_restore_rounded), - onPressed: () { - _textController.text = ALLOWED_FILE_EXTENSIONS; - settingsStore.setFileExtensions(ALLOWED_FILE_EXTENSIONS); - }, - ), - ], - ); - }, - ), - ), - const Divider( - height: 4.0, - ), - Observer( - builder: (_) { - final blockedFiles = settingsStore.numBlockedFiles; - - return ListTile( - title: Text(L10n.of(context)!.manageBlockedFiles), - subtitle: Text(L10n.of(context)!.numberOfBlockedFiles(blockedFiles)), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => navStore.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => const BlockedFilesPage(), ), - ), + const SizedBox(width: 8.0), + IconButton( + icon: const Icon(Icons.settings_backup_restore_rounded), + onPressed: () { + _textController.text = ALLOWED_FILE_EXTENSIONS; + settingsStore.setFileExtensions(ALLOWED_FILE_EXTENSIONS); + }, + ), + ], ); }, ), - SettingsSection(text: L10n.of(context)!.playback), - Observer( - builder: (context) => SwitchListTile( - value: settingsStore.playAlbumsInOrderStream.value ?? false, - onChanged: settingsStore.setPlayAlbumsInOrder, - title: Text(L10n.of(context)!.playAlbumsInOrder), - subtitle: Text( - L10n.of(context)!.playAlbumsInOrderDescription, - style: TEXT_SMALL_SUBTITLE, + ), + const Divider( + height: 4.0, + ), + Observer( + builder: (_) { + final blockedFiles = settingsStore.numBlockedFiles; + + return ListTile( + title: Text(L10n.of(context)!.manageBlockedFiles), + subtitle: Text(L10n.of(context)!.numberOfBlockedFiles(blockedFiles)), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () => navStore.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => const BlockedFilesPage(), + ), ), - isThreeLine: true, + ); + }, + ), + SettingsSection(text: L10n.of(context)!.playback), + Observer( + builder: (context) => SwitchListTile( + value: settingsStore.playAlbumsInOrderStream.value ?? false, + onChanged: settingsStore.setPlayAlbumsInOrder, + title: Text(L10n.of(context)!.playAlbumsInOrder), + subtitle: Text( + L10n.of(context)!.playAlbumsInOrderDescription, + style: TEXT_SMALL_SUBTITLE, ), + isThreeLine: true, ), - const Divider( - height: 4.0, - ), - PercentageSlider(settingsStore), - ], - ), + ), + const Divider( + height: 4.0, + ), + PercentageSlider(settingsStore), + ], ), ); } diff --git a/src/lib/presentation/pages/smart_list_form_page.dart b/src/lib/presentation/pages/smart_list_form_page.dart index cda03c8..d06d0df 100644 --- a/src/lib/presentation/pages/smart_list_form_page.dart +++ b/src/lib/presentation/pages/smart_list_form_page.dart @@ -12,9 +12,8 @@ import '../state/music_data_store.dart'; import '../state/navigation_store.dart'; import '../state/smart_list_form_store.dart'; import '../theming.dart'; -import '../widgets/playlist_cover.dart'; +import '../widgets/cover_customization_card.dart'; import '../widgets/switch_text_listtile.dart'; -import 'cover_customization_page.dart'; import 'smart_lists_artists_page.dart'; class SmartListFormPage extends StatefulWidget { @@ -67,489 +66,457 @@ class _SmartListFormPageState extends State { L10n.of(context)!.favShuffleMode, ]; - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - title, - style: TEXT_HEADER, - ), - centerTitle: true, - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () => navStore.pop(context), - ), - actions: [ - if (widget.smartList != null) - IconButton( - icon: const Icon(Icons.delete_rounded), - onPressed: () async { - // TODO: this works, but may only pop back to the smartlist page... - // can I use pop 2x here? - await musicDataStore.removeSmartList(widget.smartList!); - navStore.pop(context); - }, - ), + return Scaffold( + appBar: AppBar( + title: Text( + title, + style: TEXT_HEADER, + ), + centerTitle: true, + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () => navStore.pop(context), + ), + actions: [ + if (widget.smartList != null) IconButton( - icon: const Icon(Icons.check_rounded), + icon: const Icon(Icons.delete_rounded), onPressed: () async { - store.validateAll(); - if (!store.error.hasErrors) { - await store.save(); - navStore.pop(context); - } + // TODO: this works, but may only pop back to the smartlist page... + // can I use pop 2x here? + await musicDataStore.removeSmartList(widget.smartList!); + navStore.pop(context); }, ), - ], - titleSpacing: 0.0, - ), - body: ListTileTheme( - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - child: Scrollbar( - child: CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - const SizedBox(height: 16.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Observer( - builder: (_) => TextFormField( - initialValue: store.name, - onChanged: (value) => store.name = value, - style: TEXT_HEADER, - decoration: InputDecoration( - labelText: L10n.of(context)!.name, - labelStyle: const TextStyle(color: Colors.white), - floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), - errorText: store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null, - errorStyle: const TextStyle(color: RED), - filled: true, - fillColor: DARK35, - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - contentPadding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 12.0, - ), + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { + store.validateAll(); + if (!store.error.hasErrors) { + await store.save(); + navStore.pop(context); + } + }, + ), + ], + titleSpacing: 0.0, + ), + body: ListTileTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + child: Scrollbar( + child: CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Observer( + builder: (_) => TextFormField( + initialValue: store.name, + onChanged: (value) => store.name = value, + style: TEXT_HEADER, + decoration: InputDecoration( + labelText: L10n.of(context)!.name, + labelStyle: const TextStyle(color: Colors.white), + floatingLabelStyle: TEXT_HEADER_S.copyWith(color: Colors.white), + errorText: + store.error.name ? L10n.of(context)!.nameMustNotBeEmpty : null, + errorStyle: const TextStyle(color: RED), + filled: true, + fillColor: DARK35, + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 12.0, ), ), ), ), - const SizedBox(height: 4.0), - Observer( - builder: (context) => Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - horizontal: 10.0, - ), - child: GestureDetector( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) => CoverCustomizationPage( - store: store.cover, + ), + const SizedBox(height: 4.0), + Observer( + builder: (context) => CoverCustomizationCard(store: store.cover), + ), + const SizedBox(height: 8.0), + ListTile( + title: Text(L10n.of(context)!.filterSettings, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2.0, + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + final RangeValues _currentRangeValues = RangeValues( + store.minLikeCount.toDouble(), + store.maxLikeCount.toDouble(), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 10.0, + top: 4.0, + ), + child: Text( + L10n.of(context)!.filterLikes( + store.minLikeCount, + store.maxLikeCount, ), ), + ), + RangeSlider( + values: _currentRangeValues, + min: 0, + max: MAX_LIKE_COUNT.toDouble(), + divisions: MAX_LIKE_COUNT, + labels: RangeLabels( + _currentRangeValues.start.round().toString(), + _currentRangeValues.end.round().toString(), + ), + onChanged: (RangeValues values) { + store.minLikeCount = values.start.toInt(); + store.maxLikeCount = values.end.toInt(); + }, + ), + ], + ); + }, + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Column( + children: [ + Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.minPlayCount, + switchValue: store.minPlayCountEnabled, + onSwitchChanged: (bool value) { + store.minPlayCountEnabled = value; + }, + textValue: store.minPlayCount, + onTextChanged: (String value) { + store.minPlayCount = value; + }, ); }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - PlaylistCover( - size: 64.0, - gradient: store.cover.gradient, - icon: store.cover.icon, + ), + const SizedBox(height: CARD_SPACING), + Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.maxPlayCount, + switchValue: store.maxPlayCountEnabled, + onSwitchChanged: (bool value) { + store.maxPlayCountEnabled = value; + }, + textValue: store.maxPlayCount, + onTextChanged: (String value) { + store.maxPlayCount = value; + }, + ); + }, + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2, 3].map>((int value) { + return RadioListTile( + title: Text( + blockLevelTexts[value], + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, ), - const SizedBox(width: 16.0), - Text(L10n.of(context)!.customizeCover), - const Spacer(), - const SizedBox( - width: 56.0, - child: Icon(Icons.chevron_right_rounded), + ), + value: value, + groupValue: store.blockLevel, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) store.blockLevel = newValue; + }); + }, + ); + }).toList(), + ); + }, + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Column( + children: [ + Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.minYear, + switchValue: store.minYearEnabled, + onSwitchChanged: (bool value) { + store.minYearEnabled = value; + }, + textValue: store.minYear, + onTextChanged: (String value) { + store.minYear = value; + }, + ); + }, + ), + const SizedBox(height: CARD_SPACING), + Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.maxYear, + switchValue: store.maxYearEnabled, + onSwitchChanged: (bool value) { + store.maxYearEnabled = value; + }, + textValue: store.maxYear, + onTextChanged: (String value) { + store.maxYear = value; + }, + ); + }, + ), + ], + ), + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Column( + children: [ + Observer( + builder: (_) => GestureDetector( + onTap: () { + navStore.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + SmartListArtistsPage(formStore: store), ), - const SizedBox(width: 6.0), - ], + ); + }, + child: Padding( + padding: const EdgeInsets.only( + left: 6.0, + right: 8.0, + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + store.excludeArtists + ? L10n.of(context)!.selectArtistsExclude( + store.selectedArtists.length) + : L10n.of(context)!.selectArtistsInclude( + store.selectedArtists.length), + ), + Text( + L10n.of(context)!.includeAllArtists, + style: TEXT_SMALL_SUBTITLE.copyWith( + color: Colors.white70, + ), + ), + ], + ), + ), + ), + const SizedBox( + width: 56.0, + child: Icon(Icons.chevron_right_rounded), + ), + ], + ), ), ), ), - ), - ), - ), - const SizedBox(height: 8.0), - ListTile( - title: Text(L10n.of(context)!.filterSettings, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 2.0, - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - final RangeValues _currentRangeValues = RangeValues( - store.minLikeCount.toDouble(), - store.maxLikeCount.toDouble(), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: CARD_SPACING), + Padding( + padding: const EdgeInsets.only( + left: 8.0 - 4.0, + right: 8.0, + ), + child: Row( children: [ - Padding( - padding: const EdgeInsets.only( - left: 10.0, - top: 4.0, - ), - child: Text( - L10n.of(context)! - .filterLikes(store.minLikeCount, store.maxLikeCount), + SizedBox( + width: 60.0, + child: Observer( + builder: (_) { + return Switch( + value: store.excludeArtists, + onChanged: (bool value) => store.excludeArtists = value, + ); + }, ), ), - RangeSlider( - values: _currentRangeValues, - min: 0, - max: MAX_LIKE_COUNT.toDouble(), - divisions: MAX_LIKE_COUNT, - labels: RangeLabels( - _currentRangeValues.start.round().toString(), - _currentRangeValues.end.round().toString(), - ), - onChanged: (RangeValues values) { - store.minLikeCount = values.start.toInt(); - store.maxLikeCount = values.end.toInt(); - }, - ), + const SizedBox(width: 6.0), + Text(L10n.of(context)!.excludeArtists), + const Spacer(), ], - ); - }, - ), + ), + ), + ], ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Column( - children: [ - Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.minPlayCount, - switchValue: store.minPlayCountEnabled, - onSwitchChanged: (bool value) { - store.minPlayCountEnabled = value; - }, - textValue: store.minPlayCount, - onTextChanged: (String value) { - store.minPlayCount = value; - }, - ); - }, - ), - const SizedBox(height: CARD_SPACING), - Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.maxPlayCount, - switchValue: store.maxPlayCountEnabled, - onSwitchChanged: (bool value) { - store.maxPlayCountEnabled = value; - }, - textValue: store.maxPlayCount, - onTextChanged: (String value) { - store.maxPlayCount = value; - }, - ); - }, - ), - ], - ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + ), + child: Observer( + builder: (_) { + return SwitchTextListTile( + title: L10n.of(context)!.limitSongs, + switchValue: store.limitEnabled, + onSwitchChanged: (bool value) { + store.limitEnabled = value; + }, + textValue: store.limit, + onTextChanged: (String value) { + store.limit = value; + }, + ); + }, ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2, 3].map>((int value) { - return RadioListTile( - title: Text( - blockLevelTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), - ), - value: value, - groupValue: store.blockLevel, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) store.blockLevel = newValue; - }); - }, - ); - }).toList(), - ); - }, - ), + ), + const SizedBox(height: 16.0), + ListTile( + title: Text(L10n.of(context)!.orderSettings, style: TEXT_HEADER), + subtitle: Text(L10n.of(context)!.orderSettingsDescription), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, + horizontal: 8.0, ), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Column( - children: [ - Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.minYear, - switchValue: store.minYearEnabled, - onSwitchChanged: (bool value) { - store.minYearEnabled = value; - }, - textValue: store.minYear, - onTextChanged: (String value) { - store.minYear = value; - }, - ); - }, - ), - const SizedBox(height: CARD_SPACING), - Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.maxYear, - switchValue: store.maxYearEnabled, - onSwitchChanged: (bool value) { - store.maxYearEnabled = value; - }, - textValue: store.maxYear, - onTextChanged: (String value) { - store.maxYear = value; - }, - ); - }, - ), - ], - ), - ), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Column( - children: [ - Observer( - builder: (_) => GestureDetector( - onTap: () { - navStore.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => - SmartListArtistsPage(formStore: store), - ), - ); - }, + child: Observer( + builder: (_) => ReorderableColumn( + children: List.generate( + store.orderState.length, + (i) => Padding( + key: ValueKey(i), + padding: const EdgeInsets.symmetric(vertical: 1.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + color: DARK25, + ), child: Padding( - padding: const EdgeInsets.only( - left: 6.0, - right: HORIZONTAL_PADDING, + padding: const EdgeInsets.symmetric( + horizontal: HORIZONTAL_PADDING - 8.0, + vertical: 12.0, ), child: Row( children: [ - const SizedBox(width: 60.0 + 6.0, height: 48.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - store.excludeArtists - ? L10n.of(context)!.selectArtistsExclude( - store.selectedArtists.length) - : L10n.of(context)!.selectArtistsInclude( - store.selectedArtists.length), - ), - Text( - L10n.of(context)!.includeAllArtists, - style: TEXT_SMALL_SUBTITLE.copyWith( - color: Colors.white70, - ), - ), - ], + Switch( + value: store.orderState[i].enabled, + onChanged: (bool value) => + store.setOrderEnabled(i, value), ), + const SizedBox(width: 6.0), + Text(store.orderState[i].orderCriterion.toText(context)), const Spacer(), - const SizedBox( - width: 56.0, - child: Icon(Icons.chevron_right_rounded), + IconButton( + icon: store.orderState[i].orderDirection == + OrderDirection.ascending + ? const Icon(Icons.arrow_upward_rounded) + : const Icon(Icons.arrow_downward_rounded), + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 8.0, + ), + visualDensity: VisualDensity.compact, + onPressed: () => store.toggleOrderDirection(i), ), ], ), ), ), ), - const SizedBox(height: CARD_SPACING), - Padding( - padding: const EdgeInsets.only( - left: 6.0, - right: HORIZONTAL_PADDING, - ), - child: Row( - children: [ - SizedBox( - width: 60.0, - child: Observer( - builder: (_) { - return Switch( - value: store.excludeArtists, - onChanged: (bool value) => store.excludeArtists = value, - ); - }, - ), - ), - const SizedBox(width: 6.0), - Text(L10n.of(context)!.excludeArtists), - const Spacer(), - ], - ), - ), - ], + ), + onReorder: (oldIndex, newIndex) => + store.reorderOrderState(oldIndex, newIndex), ), ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return SwitchTextListTile( - title: L10n.of(context)!.limitSongs, - switchValue: store.limitEnabled, - onSwitchChanged: (bool value) { - store.limitEnabled = value; - }, - textValue: store.limit, - onTextChanged: (String value) { - store.limit = value; - }, - ); - }, - ), + ), + const SizedBox(height: 8.0), + ListTile( + title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: CARD_PADDING, ), - ), - const SizedBox(height: 16.0), - ListTile( - title: Text(L10n.of(context)!.orderSettings, style: TEXT_HEADER), - subtitle: Text(L10n.of(context)!.orderSettingsDescription), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - horizontal: 8.0, - ), - child: Observer( - builder: (_) => ReorderableColumn( - children: List.generate( - store.orderState.length, - (i) => Padding( - key: ValueKey(i), - padding: const EdgeInsets.symmetric(vertical: 1.0), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - color: DARK25, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_PADDING - 8.0, - vertical: 12.0, - ), - child: Row( - children: [ - Switch( - value: store.orderState[i].enabled, - onChanged: (bool value) => - store.setOrderEnabled(i, value), - ), - const SizedBox(width: 6.0), - Text(store.orderState[i].orderCriterion.toText(context)), - const Spacer(), - IconButton( - icon: store.orderState[i].orderDirection == - OrderDirection.ascending - ? const Icon(Icons.arrow_upward_rounded) - : const Icon(Icons.arrow_downward_rounded), - padding: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 8.0, - ), - visualDensity: VisualDensity.compact, - onPressed: () => store.toggleOrderDirection(i), - ), - ], - ), + child: Observer( + builder: (_) { + return Column( + children: [0, 1, 2, 3].map>((int value) { + return RadioListTile( + title: Text( + playbackModeTexts[value], + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, ), ), - ), - ), - onReorder: (oldIndex, newIndex) => - store.reorderOrderState(oldIndex, newIndex), - ), - ), + value: value, + groupValue: store.shuffleModeIndex, + onChanged: (int? newValue) { + setState(() { + if (newValue != null) store.setShuffleModeIndex(newValue); + }); + }, + ); + }).toList(), + ); + }, ), ), - const SizedBox(height: 8.0), - ListTile( - title: Text(L10n.of(context)!.playbackMode, style: TEXT_HEADER), - ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: CARD_PADDING, - ), - child: Observer( - builder: (_) { - return Column( - children: [0, 1, 2, 3].map>((int value) { - return RadioListTile( - title: Text( - playbackModeTexts[value], - style: const TextStyle( - fontSize: 14.0, - ), - ), - value: value, - groupValue: store.shuffleModeIndex, - onChanged: (int? newValue) { - setState(() { - if (newValue != null) store.setShuffleModeIndex(newValue); - }); - }, - ); - }).toList(), - ); - }, - ), - ), - ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/src/lib/presentation/pages/smart_list_page.dart b/src/lib/presentation/pages/smart_list_page.dart index 20ff74b..cbd967e 100644 --- a/src/lib/presentation/pages/smart_list_page.dart +++ b/src/lib/presentation/pages/smart_list_page.dart @@ -12,11 +12,11 @@ import '../state/audio_store.dart'; import '../state/music_data_store.dart'; import '../state/navigation_store.dart'; import '../state/smart_list_page_store.dart'; -import '../theming.dart'; import '../utils.dart' as utils; import '../widgets/bottom_sheet/add_to_playlist.dart'; import '../widgets/cover_sliver_appbar.dart'; import '../widgets/custom_modal_bottom_sheet.dart'; +import '../widgets/play_shuffle_button.dart'; import '../widgets/playlist_cover.dart'; import '../widgets/song_bottom_sheet.dart'; import '../widgets/song_list_tile.dart'; @@ -153,30 +153,18 @@ class _SmartListPageState extends State { ), ], title: smartList.name, - subtitle2: '${L10n.of(context)!.nSongs(songs.length).capitalize()} • ${utils.msToTimeString(totalDuration)}', - background: Container( - decoration: BoxDecoration( - gradient: smartList.gradient, - ), - ), + subtitle2: + '${L10n.of(context)!.nSongs(songs.length).capitalize()} • ${utils.msToTimeString(totalDuration)}', + backgroundColor: utils.bgColor(smartList.gradient.colors.first), cover: PlaylistCover( size: 120, icon: smartList.icon, gradient: smartList.gradient, ), - button: ElevatedButton( + button: PlayShuffleButton( onPressed: () => audioStore.playSmartList(smartList), - child: Row( - children: [ - Expanded(child: Center(child: Text(L10n.of(context)!.play))), - Icon(playIcon), - ], - ), - style: ElevatedButton.styleFrom( - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - backgroundColor: LIGHT1, - ), + shuffleMode: smartList.shuffleMode, + size: 56, ), ), Observer( @@ -190,8 +178,6 @@ class _SmartListPageState extends State { final Song song = songs[index]; return SongListTile( song: song, - showAlbum: true, - subtitle: Subtitle.artistAlbum, isSelectEnabled: isMultiSelectEnabled, isSelected: isMultiSelectEnabled && isSelected[index], onTap: () => audioStore.playSong(index, songs, smartList), diff --git a/src/lib/presentation/pages/smart_lists_artists_page.dart b/src/lib/presentation/pages/smart_lists_artists_page.dart index b1016a9..e962cab 100644 --- a/src/lib/presentation/pages/smart_lists_artists_page.dart +++ b/src/lib/presentation/pages/smart_lists_artists_page.dart @@ -21,61 +21,59 @@ class SmartListArtistsPage extends StatelessWidget { final NavigationStore navStore = GetIt.I(); final initialSet = Set.from(store.selectedArtists); - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context)!.selectArtists, - style: TEXT_HEADER, - ), - leading: IconButton( - icon: const Icon(Icons.close_rounded), - onPressed: () { - store.selectedArtists.clear(); - store.selectedArtists.addAll(initialSet); + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context)!.selectArtists, + style: TEXT_HEADER, + ), + leading: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + store.selectedArtists.clear(); + store.selectedArtists.addAll(initialSet); + navStore.pop(context); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.check_rounded), + onPressed: () async { navStore.pop(context); }, ), - actions: [ - IconButton( - icon: const Icon(Icons.check_rounded), - onPressed: () async { - navStore.pop(context); - }, - ), - ], - titleSpacing: 0.0, - ), - body: Observer(builder: (_) { - final List artists = musicDataStore.artistStream.value ?? []; - final selectedArtists = store.selectedArtists.toSet(); - - return Scrollbar( - child: ListView.separated( - itemCount: artists.length, - itemBuilder: (_, int index) { - final Artist artist = artists[index]; - return CheckboxListTile( - title: Text(artist.name), - value: selectedArtists.contains(artist), - onChanged: (bool? value) { - if (value != null) { - if (value) { - store.addArtist(artist); - } else { - store.removeArtist(artist); - } - } - }, - ); - }, - separatorBuilder: (BuildContext context, int index) => const SizedBox( - height: 4.0, - ), - ), - ); - }), + ], + titleSpacing: 0.0, ), + body: Observer(builder: (_) { + final List artists = musicDataStore.artistStream.value ?? []; + final selectedArtists = store.selectedArtists.toSet(); + + return Scrollbar( + child: ListView.separated( + itemCount: artists.length, + itemBuilder: (_, int index) { + final Artist artist = artists[index]; + return CheckboxListTile( + title: Text(artist.name), + value: selectedArtists.contains(artist), + onChanged: (bool? value) { + if (value != null) { + if (value) { + store.addArtist(artist); + } else { + store.removeArtist(artist); + } + } + }, + ); + }, + separatorBuilder: (BuildContext context, int index) => const SizedBox( + height: 4.0, + ), + ), + ); + }), ); } } diff --git a/src/lib/presentation/pages/songs_page.dart b/src/lib/presentation/pages/songs_page.dart index 92ad8ee..b261498 100644 --- a/src/lib/presentation/pages/songs_page.dart +++ b/src/lib/presentation/pages/songs_page.dart @@ -37,15 +37,13 @@ class _SongsPageState extends State with AutomaticKeepAliveClientMixi final List songs = songStream.value ?? []; return Scrollbar( controller: _scrollController, - child: ListView.separated( + child: ListView.builder( controller: _scrollController, itemCount: songs.length, itemBuilder: (_, int index) { final Song song = songs[index]; return SongListTile( song: song, - showAlbum: true, - subtitle: Subtitle.artistAlbum, onTap: () => audioStore.playSong(index, songs, AllSongs()), onTapMore: () => showModalBottomSheet( context: context, @@ -59,9 +57,6 @@ class _SongsPageState extends State with AutomaticKeepAliveClientMixi onSelect: () {}, ); }, - separatorBuilder: (BuildContext context, int index) => const SizedBox( - height: 4.0, - ), ), ); case StreamStatus.waiting: diff --git a/src/lib/presentation/state/music_data_store.dart b/src/lib/presentation/state/music_data_store.dart index c00fcc4..e54ed30 100644 --- a/src/lib/presentation/state/music_data_store.dart +++ b/src/lib/presentation/state/music_data_store.dart @@ -138,4 +138,9 @@ abstract class _MusicDataStore with Store { Future removeSmartList(SmartList smartList) async { await _musicDataRepository.removeSmartList(smartList); } + + Future> isSongFirstLast(Song? song) async { + if (song == null) return Future.value([false, false]); + return await _musicDataRepository.isSongFirstLast(song); + } } diff --git a/src/lib/presentation/theming.dart b/src/lib/presentation/theming.dart index e22aeac..e57a08c 100644 --- a/src/lib/presentation/theming.dart +++ b/src/lib/presentation/theming.dart @@ -14,17 +14,17 @@ const Color RED = Colors.red; const double HORIZONTAL_PADDING = 16.0; ThemeData theme() => ThemeData( - // useMaterial3: true, + useMaterial3: true, colorScheme: const ColorScheme( primary: DARK2, - secondary: LIGHT2, + secondary: LIGHT1, surface: DARK3, background: DARK2, error: RED, onPrimary: Colors.white, onSecondary: Colors.white, onSurface: Colors.white, - onBackground: Colors.white, + onBackground: Colors.white10, // only seen used in Switch so far onError: Colors.white, brightness: Brightness.dark, ), @@ -36,6 +36,7 @@ ThemeData theme() => ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: LIGHT1, + foregroundColor: Colors.white, ), ), progressIndicatorTheme: const ProgressIndicatorThemeData(color: LIGHT2), @@ -79,17 +80,21 @@ ThemeData theme() => ThemeData( tabBarTheme: const TabBarTheme( labelColor: Colors.white, labelStyle: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w800, fontSize: 20.0, ), unselectedLabelStyle: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w800, fontSize: 20.0, ), ), iconTheme: const IconThemeData( color: Colors.white, ), + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + foregroundColor: Colors.white, + )), appBarTheme: const AppBarTheme( color: DARK1, elevation: 0.0, @@ -109,6 +114,7 @@ ThemeData theme() => ThemeData( indent: HORIZONTAL_PADDING, endIndent: HORIZONTAL_PADDING, space: 0.0, + color: Colors.white10, ), switchTheme: SwitchThemeData( thumbColor: MaterialStateProperty.resolveWith((states) { @@ -132,11 +138,23 @@ ThemeData theme() => ThemeData( thumbColor: MaterialStateProperty.all(Colors.white12), interactive: true, ), + listTileTheme: const ListTileThemeData( + iconColor: Colors.white, + ), + radioTheme: RadioThemeData( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.white30; + } else if (states.contains(MaterialState.selected)) { + return LIGHT1; + } + return Colors.white; + })), ); const TextStyle TEXT_HEADER = TextStyle( fontSize: 20.0, - fontWeight: FontWeight.w900, + fontWeight: FontWeight.w800, ); const TextStyle TEXT_HEADER_S = TextStyle( @@ -155,12 +173,12 @@ const TextStyle TEXT_SUBTITLE = TextStyle( ); const TextStyle TEXT_SMALL_HEADLINE = TextStyle( - fontSize: 12.0, + fontSize: 13.0, fontWeight: FontWeight.normal, ); const TextStyle TEXT_SMALL_SUBTITLE = TextStyle( - fontSize: 12.0, + fontSize: 13.0, fontWeight: FontWeight.w300, ); diff --git a/src/lib/presentation/utils.dart b/src/lib/presentation/utils.dart index 994461c..d1fb7a5 100644 --- a/src/lib/presentation/utils.dart +++ b/src/lib/presentation/utils.dart @@ -6,7 +6,6 @@ import '../domain/entities/album.dart'; import '../domain/entities/playable.dart'; import '../domain/entities/playlist.dart'; import '../domain/entities/smart_list.dart'; -import '../domain/entities/song.dart'; import 'gradients.dart'; import 'mucke_icons.dart'; import 'theming.dart'; @@ -46,6 +45,8 @@ String? validateNumber(bool enabled, String number) { return int.tryParse(number) == null ? 'Error' : null; } +Color bgColor(Color? color) => Color.lerp(DARK3, color, 0.4) ?? DARK3; + IconData blockLevelIcon(int blockLevel) { switch (blockLevel) { case 1: @@ -75,15 +76,26 @@ IconData likeCountIcon(int likeCount) { } Color likeCountColor(int likeCount) { - return likeCount == 3 ? LIGHT2 : Colors.white.withOpacity(0.24 + 0.18 * likeCount); + return likeCount == 3 ? LIGHT1 : Colors.white.withOpacity(0.24 + 0.18 * likeCount); } -Color linkColor(Song song) { - if (song.next && song.previous) { - return LIGHT2; - } else if (song.next) { +IconData linkIcon(bool prev, bool next) { + if (prev && next) { + return MuckeIcons.link_both; + } else if (prev) { + return MuckeIcons.link_prev; + } else if (next) { + return MuckeIcons.link_next; + } + return Icons.link_off_rounded; +} + +Color linkColor(bool prev, bool next) { + if (next && prev) { + return LIGHT1; + } else if (next) { return Colors.red; - } else if (song.previous) { + } else if (prev) { return Colors.blue; } return Colors.white24; diff --git a/src/lib/presentation/widgets/album_background.dart b/src/lib/presentation/widgets/album_background.dart index faa31d8..92d0ebc 100644 --- a/src/lib/presentation/widgets/album_background.dart +++ b/src/lib/presentation/widgets/album_background.dart @@ -68,7 +68,7 @@ class _AlbumBackgroundState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color.lerp(DARK3, color, 0.4) ?? DARK3, DARK1], + colors: [bgColor(color), DARK1], stops: const [0.0, 1.0], ), ), diff --git a/src/lib/presentation/widgets/artist_header.dart b/src/lib/presentation/widgets/artist_header.dart index 61d3fbb..0c15529 100644 --- a/src/lib/presentation/widgets/artist_header.dart +++ b/src/lib/presentation/widgets/artist_header.dart @@ -30,17 +30,13 @@ class ArtistHeader extends StatelessWidget { ), flexibleSpace: FlexibleSpaceBar( centerTitle: true, - titlePadding: const EdgeInsets.symmetric(horizontal: 48.0), + titlePadding: EdgeInsets.only(left: 48.0, right: 48.0, top: MediaQuery.of(context).padding.top), title: Container( alignment: Alignment.center, height: height * 0.66, child: Text( artist.name, - style: Theme.of(context).textTheme.displayLarge?.copyWith( - fontWeight: FontWeight.bold, - fontSize: 24.0, - color: Colors.white, - ), + style: Theme.of(context).textTheme.displaySmall, textAlign: TextAlign.center, maxLines: 3, ), diff --git a/src/lib/presentation/widgets/artist_highlighted_songs.dart b/src/lib/presentation/widgets/artist_highlighted_songs.dart index 3661246..3a0f27c 100644 --- a/src/lib/presentation/widgets/artist_highlighted_songs.dart +++ b/src/lib/presentation/widgets/artist_highlighted_songs.dart @@ -31,8 +31,7 @@ class ArtistHighlightedSongs extends StatelessWidget { final Song song = songsHead[index]; return SongListTile( song: song, - showAlbum: true, - subtitle: Subtitle.stats, + showPlayCount: true, onTap: () => audioStore.playSong(index, songs, artistPageStore.artist), onTapMore: () => showModalBottomSheet( context: context, diff --git a/src/lib/presentation/widgets/cover_customization_card.dart b/src/lib/presentation/widgets/cover_customization_card.dart new file mode 100644 index 0000000..3afc96a --- /dev/null +++ b/src/lib/presentation/widgets/cover_customization_card.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/localizations.dart'; + +import '../pages/cover_customization_page.dart'; +import '../state/cover_customization_store.dart'; +import 'playlist_cover.dart'; + +class CoverCustomizationCard extends StatelessWidget { + const CoverCustomizationCard({ + Key? key, + required this.store, + }) : super(key: key); + + final CoverCustomizationStore store; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => CoverCustomizationPage( + store: store, + ), + ), + ); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + PlaylistCover( + size: 64.0, + gradient: store.gradient, + icon: store.icon, + ), + const SizedBox(width: 16.0), + Text(L10n.of(context)!.customizeCover), + const Spacer(), + const SizedBox( + width: 56.0, + child: Icon(Icons.chevron_right_rounded), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/src/lib/presentation/widgets/cover_sliver_appbar.dart b/src/lib/presentation/widgets/cover_sliver_appbar.dart index 35e23c8..8adb7d3 100644 --- a/src/lib/presentation/widgets/cover_sliver_appbar.dart +++ b/src/lib/presentation/widgets/cover_sliver_appbar.dart @@ -14,7 +14,7 @@ class CoverSliverAppBar extends StatefulWidget { this.subtitle2, required this.actions, required this.cover, - required this.background, + required this.backgroundColor, this.button, }) : super(key: key); @@ -23,7 +23,7 @@ class CoverSliverAppBar extends StatefulWidget { final String? subtitle2; final List actions; final Widget cover; - final Widget background; + final Color backgroundColor; final Widget? button; @override @@ -49,7 +49,7 @@ class _CoverSliverAppBarState extends State { flexibleSpace: Header( minHeight: minHeight, maxHeight: maxHeight, - background: widget.background, + backgroundColor: widget.backgroundColor, cover: widget.cover, title: widget.title, subtitle: widget.subtitle, @@ -61,6 +61,8 @@ class _CoverSliverAppBarState extends State { onPressed: () => navStore.pop(context), ), actions: widget.actions, + snap: true, + floating: true, ); } } @@ -74,7 +76,7 @@ class Header extends StatelessWidget { this.subtitle, this.subtitle2, required this.cover, - required this.background, + required this.backgroundColor, this.button, }) : super(key: key); @@ -82,7 +84,7 @@ class Header extends StatelessWidget { final String? subtitle; final String? subtitle2; final Widget cover; - final Widget background; + final Color backgroundColor; final double maxHeight; final double minHeight; final Widget? button; @@ -99,10 +101,6 @@ class Header extends StatelessWidget { return Stack( fit: StackFit.expand, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: _buildBackground(background, animation, maxHeight, minHeight), - ), _buildGradient(animation, context), _buildImage(animation, context), if (button != null) _buildButton(animation, context), @@ -149,7 +147,7 @@ class Header extends StatelessWidget { // TODO: padding right for enabled multi select return Align( alignment: AlignmentTween( - begin: Alignment.centerLeft, + begin: Alignment.center, end: Alignment.topLeft, ).evaluate(animation), child: Container( @@ -178,6 +176,7 @@ class Header extends StatelessWidget { FontWeight.w600, Tween(begin: 0, end: 1).evaluate(animation), ), + height: 1.1, ), ), Container( @@ -215,7 +214,7 @@ class Header extends StatelessWidget { Widget _buildButton(Animation animation, BuildContext context) { return Positioned( width: 96, - height: 48, + height: 56, right: HORIZONTAL_PADDING, top: Tween( begin: kToolbarHeight + MediaQuery.of(context).padding.top + 120, @@ -252,13 +251,9 @@ class Header extends StatelessWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: [ + backgroundColor, ColorTween( - begin: Theme.of(context).primaryColor, - end: Theme.of(context).primaryColor.withOpacity(0.2), - ).evaluate(animation) ?? - Colors.transparent, - ColorTween( - begin: Theme.of(context).primaryColor, + begin: backgroundColor, end: Theme.of(context).scaffoldBackgroundColor, ).evaluate(animation) ?? Colors.transparent, @@ -270,19 +265,4 @@ class Header extends StatelessWidget { ), ); } - - Widget _buildBackground( - Widget background, - Animation animation, - double maxHeight, - double minHeight, - ) { - return ClipRect( - clipBehavior: Clip.hardEdge, - child: ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 24.0, sigmaY: 24.0), - child: background, - ), - ); - } } diff --git a/src/lib/presentation/widgets/currently_playing_bar.dart b/src/lib/presentation/widgets/currently_playing_bar.dart index 671c5f4..78b904a 100644 --- a/src/lib/presentation/widgets/currently_playing_bar.dart +++ b/src/lib/presentation/widgets/currently_playing_bar.dart @@ -29,12 +29,7 @@ class CurrentlyPlayingBar extends StatelessWidget { if (song == null) return Container(); return Padding( - padding: const EdgeInsets.only( - bottom: 0.0, - top: 8.0, - left: 4.0, - right: 4.0, - ), + padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Padding( @@ -54,7 +49,7 @@ class CurrentlyPlayingBar extends StatelessWidget { Text( song.title, overflow: TextOverflow.ellipsis, - maxLines: 2, + maxLines: 1, ), Text( song.artist, diff --git a/src/lib/presentation/widgets/playlist_cover.dart b/src/lib/presentation/widgets/playlist_cover.dart index b8df4ee..6bd7da3 100644 --- a/src/lib/presentation/widgets/playlist_cover.dart +++ b/src/lib/presentation/widgets/playlist_cover.dart @@ -43,6 +43,7 @@ class PlaylistCover extends StatelessWidget { child: Icon( icon, size: size / 2.0, + color: Colors.white, ), ), decoration: deco, diff --git a/src/lib/presentation/widgets/song_bottom_sheet.dart b/src/lib/presentation/widgets/song_bottom_sheet.dart index 18e31d9..eabb78a 100644 --- a/src/lib/presentation/widgets/song_bottom_sheet.dart +++ b/src/lib/presentation/widgets/song_bottom_sheet.dart @@ -7,6 +7,7 @@ import '../../domain/entities/album.dart'; import '../../domain/entities/artist.dart'; import '../../domain/entities/song.dart'; import '../l10n_utils.dart'; +import '../mucke_icons.dart'; import '../pages/album_details_page.dart'; import '../pages/artist_details_page.dart'; import '../state/audio_store.dart'; @@ -66,11 +67,10 @@ class _SongBottomSheetState extends State { final NavigationStore navStore = GetIt.I(); final SettingsStore settingsStore = GetIt.I(); - int optionIndex = 0; - return Observer(builder: (context) { final song = store.songStream.value; if (song == null) return Container(); + final firstLast = musicDataStore.isSongFirstLast(song); final albums = musicDataStore.albumStream.value; final artists = musicDataStore.artistStream.value; @@ -84,42 +84,18 @@ class _SongBottomSheetState extends State { artist = artists.singleWhere((a) => a.name == album!.artist); } - final options = [ - const SizedBox.shrink(), - Container( - // color: DARK3, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: SwitchListTile( - title: Text(L10n.of(context)!.previousSong.capitalize()), - value: song.previous, - onChanged: (_) => musicDataStore.togglePreviousSongLink(song), - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - ), - ), - Container(width: 1.0, height: 24.0, color: DARK2), - Expanded( - child: SwitchListTile( - title: Text(L10n.of(context)!.nextSong.capitalize()), - value: song.next, - onChanged: (_) => musicDataStore.toggleNextSongLink(song), - contentPadding: const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), - ), - ), - ], - ), - ), - ExcludeLevelOptions(songs: [song], musicDataStore: musicDataStore), - ]; - final List widgets = [ Container( color: DARK2, child: Padding( - padding: const EdgeInsets.all(HORIZONTAL_PADDING), + padding: const EdgeInsets.fromLTRB( + HORIZONTAL_PADDING, + HORIZONTAL_PADDING, + HORIZONTAL_PADDING - 14.0, + HORIZONTAL_PADDING, + ), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 64.0, @@ -149,18 +125,28 @@ class _SongBottomSheetState extends State { song.title, style: TEXT_HEADER_S, ), - const SizedBox(height: 4.0), + const SizedBox(height: 2.0), Text( '#${song.trackNumber} • ${utils.msToTimeString(song.duration)} • ${song.year}', style: TEXT_SMALL_SUBTITLE, ), Text( L10n.of(context)!.playedNTimes(song.playCount).capitalize(), - style: TEXT_SMALL_SUBTITLE, + style: TEXT_SMALL_SUBTITLE.copyWith(height: 1.2), ), ], ), - ) + ), + SizedBox( + height: 64.0, + child: Center( + child: LikeButton( + song: song, + iconSize: 28.0, + visualDensity: VisualDensity.standard, + ), + ), + ), ], ), ), @@ -169,7 +155,6 @@ class _SongBottomSheetState extends State { title: Text('${song.album}'), leading: const Icon(Icons.album_rounded), trailing: widget.enableGoToAlbum ? const Icon(Icons.open_in_new_rounded) : null, - visualDensity: VisualDensity.compact, onTap: widget.enableGoToAlbum ? () { if (album != null) { @@ -190,7 +175,6 @@ class _SongBottomSheetState extends State { title: Text(song.artist), leading: const Icon(Icons.person_rounded), trailing: widget.enableGoToArtist ? const Icon(Icons.open_in_new_rounded) : null, - visualDensity: VisualDensity.compact, onTap: widget.enableGoToArtist ? () { if (artist != null) { @@ -208,51 +192,62 @@ class _SongBottomSheetState extends State { : () {}, ), if (widget.enableSongCustomization) - StatefulBuilder( - builder: (context, setState) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: HORIZONTAL_PADDING - 12.0, vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - onPressed: () { - if (optionIndex == 1) - setState(() => optionIndex = 0); - else - setState(() => optionIndex = 1); - }, - icon: const Icon(Icons.link_rounded), - color: utils.linkColor(song), - ), - LikeButton(song: song), - IconButton( - onPressed: () { - if (optionIndex == 2) - setState(() => optionIndex = 0); - else - setState(() => optionIndex = 2); - }, - icon: Icon(utils.blockLevelIcon(song.blockLevel)), - color: utils.blockLevelColor(song.blockLevel), - ), - ], - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 100), - transitionBuilder: (child, animation) => SizeTransition( - sizeFactor: animation, - child: child, - ), - child: options[optionIndex], - ), - ], - ), - ), + ExcludeLevelOptions(songs: [song], musicDataStore: musicDataStore), + if (widget.enableSongCustomization) + FutureBuilder( + future: firstLast, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) + return Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: SwitchListTile( + secondary: Icon( + MuckeIcons.link_prev, + color: song.previous + ? utils.linkColor(true, false) + : utils.linkColor(true, false).withOpacity(0.5), + ), + value: song.previous, + onChanged: snapshot.data![0] + ? null + : (_) => musicDataStore.togglePreviousSongLink(song), + contentPadding: + const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + ), + ), + Expanded( + flex: 2, + child: Center( + child: Container(width: 1.0, height: 24.0, color: DARK2), + ), + ), + Expanded( + flex: 5, + child: SwitchListTile( + secondary: Icon( + MuckeIcons.link_next, + color: song.next + ? utils.linkColor(false, true) + : utils.linkColor(false, true).withOpacity(0.5), + ), + value: song.next, + onChanged: snapshot.data![1] + ? null + : (_) => musicDataStore.toggleNextSongLink(song), + contentPadding: + const EdgeInsets.symmetric(horizontal: HORIZONTAL_PADDING), + ), + ), + ], + ), + ); + else + return Container(); + }), if (widget.enableQueueActions) ...[ ListTile( title: Text(L10n.of(context)!.playNext), diff --git a/src/lib/presentation/widgets/song_customization_buttons.dart b/src/lib/presentation/widgets/song_customization_buttons.dart index 64d95e3..6fe133e 100644 --- a/src/lib/presentation/widgets/song_customization_buttons.dart +++ b/src/lib/presentation/widgets/song_customization_buttons.dart @@ -31,8 +31,8 @@ class SongCustomizationButtons extends StatelessWidget { children: [ IconButton( icon: Icon( - song.next || song.previous ? Icons.link_rounded : Icons.link_off_rounded, - color: linkColor(song), + linkIcon(song.previous, song.next), + color: linkColor(song.previous, song.next), ), iconSize: 24.0, onPressed: () => _editLinks(context), @@ -72,24 +72,42 @@ class SongCustomizationButtons extends StatelessWidget { Observer(builder: (context) { final song = audioStore.currentSongStream.value; if (song == null) return Container(); - return SwitchListTile( - title: Text(L10n.of(context)!.alwaysPlayPrevious), - value: song.previous, - onChanged: (bool value) { - musicDataStore.togglePreviousSongLink(song); - }, - ); + final firstLast = musicDataStore.isSongFirstLast(song); + return FutureBuilder( + future: firstLast, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) + return SwitchListTile( + title: Text(L10n.of(context)!.alwaysPlayPrevious), + value: song.previous, + onChanged: snapshot.data![0] + ? null + : (bool value) { + musicDataStore.togglePreviousSongLink(song); + }, + ); + return Container(); + }); }), Observer(builder: (context) { final song = audioStore.currentSongStream.value; if (song == null) return Container(); - return SwitchListTile( - title: Text(L10n.of(context)!.alwaysPlayNext), - value: song.next, - onChanged: (bool value) { - musicDataStore.toggleNextSongLink(song); - }, - ); + final firstLast = musicDataStore.isSongFirstLast(song); + return FutureBuilder( + future: firstLast, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) + return SwitchListTile( + title: Text(L10n.of(context)!.alwaysPlayNext), + value: song.next, + onChanged: snapshot.data![1] + ? null + : (bool value) { + musicDataStore.toggleNextSongLink(song); + }, + ); + return Container(); + }); }), ], ), diff --git a/src/lib/presentation/widgets/song_list_tile.dart b/src/lib/presentation/widgets/song_list_tile.dart index c194777..3da25d7 100644 --- a/src/lib/presentation/widgets/song_list_tile.dart +++ b/src/lib/presentation/widgets/song_list_tile.dart @@ -3,11 +3,8 @@ import 'package:flutter/material.dart'; import '../../domain/entities/queue_item.dart'; import '../../domain/entities/song.dart'; import '../theming.dart'; -import '../utils.dart' as utils; import '../utils.dart'; -enum Subtitle { artist, artistAlbum, stats } - class SongListTile extends StatelessWidget { const SongListTile({ Key? key, @@ -15,8 +12,7 @@ class SongListTile extends StatelessWidget { required this.onTap, required this.onTapMore, this.highlight = false, - this.showAlbum = true, - this.subtitle = Subtitle.artist, + this.showPlayCount = false, this.source = QueueItemSource.original, required this.onSelect, this.isSelectEnabled = false, @@ -27,8 +23,7 @@ class SongListTile extends StatelessWidget { final Function onTap; final Function onTapMore; final bool highlight; - final bool showAlbum; - final Subtitle subtitle; + final bool showPlayCount; final QueueItemSource source; final bool isSelectEnabled; final bool isSelected; @@ -36,101 +31,92 @@ class SongListTile extends StatelessWidget { @override Widget build(BuildContext context) { - final Widget leading = showAlbum - ? Image( - image: utils.getAlbumImage(song.albumArtPath), - fit: BoxFit.cover, - ) - : Center(child: Text('${song.trackNumber}')); - - Widget subtitleWidget; - switch (subtitle) { - case Subtitle.artist: - subtitleWidget = Text( - song.artist, - style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - break; - case Subtitle.artistAlbum: - subtitleWidget = Text( - '${song.artist} • ${song.album}', - style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - break; - case Subtitle.stats: - subtitleWidget = Row( - children: [ - const Icon( - Icons.favorite_rounded, - size: 12.0, - ), - const SizedBox(width: 2.0), - Text( - '${song.likeCount}', - style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white), - ), - const SizedBox(width: 8.0), - const Icon( - Icons.play_arrow_rounded, - size: 12.0, - ), - const SizedBox(width: 2.0), - Text( - '${song.playCount}', - style: TEXT_SMALL_SUBTITLE.copyWith(color: Colors.white), - ), - ], - ); - } - - if (source != QueueItemSource.original) { - final icon = source == QueueItemSource.added ? Icons.add_circle_rounded : Icons.link_rounded; - - subtitleWidget = Row( - children: [ - Icon(icon, color: LIGHT1, size: 14.0), - const SizedBox(width: 4.0), - subtitleWidget, - ], - ); - } return ListTile( contentPadding: const EdgeInsets.only(left: HORIZONTAL_PADDING), + minVerticalPadding: 8.0, leading: SizedBox( height: 56, width: 56, - child: leading, + child: Image( + image: getAlbumImage(song.albumArtPath), + fit: BoxFit.cover, + ), ), title: Text( song.title, - maxLines: 2, + maxLines: 1, overflow: TextOverflow.ellipsis, ), - subtitle: subtitleWidget, + subtitle: Row( + children: [ + if (showPlayCount) + Row( + children: [ + Text( + '${song.playCount}', + style: TEXT_SMALL_SUBTITLE, + ), + const Padding( + padding: EdgeInsets.only(right: 2.0), + child: Icon( + Icons.play_arrow_rounded, + size: 13.0, + ), + ), + + ],), + if (source != QueueItemSource.original) + Icon( + source == QueueItemSource.added ? Icons.add_circle_rounded : Icons.link_rounded, + color: LIGHT1, + size: 13.0, + ), + if (source != QueueItemSource.original) + const Text( + ' • ', + style: TEXT_SMALL_SUBTITLE, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Icon( + likeCountIcon(song.likeCount), + size: 13.0, + color: likeCountColor(song.likeCount), + ), + if (song.blockLevel > 0) + Padding( + padding: const EdgeInsets.only(left: 2.0), + child: Icon( + blockLevelIcon(song.blockLevel), + size: 13.0, + color: Colors.white38, + ), + ), + if (song.next || song.previous) + Padding( + padding: const EdgeInsets.only(left: 2.0), + child: Icon( + linkIcon(song.previous, song.next), + size: 13.0, + color: linkColor(song.previous, song.next).withOpacity(0.7), + ), + ), + Expanded( + child: Text( + ' • ${song.artist}', + style: TEXT_SMALL_SUBTITLE, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), onTap: () => onTap(), tileColor: highlight ? Colors.white10 : Colors.transparent, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (song.blockLevel > 0) - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Icon( - blockLevelIcon(song.blockLevel), - size: 16.0, - color: Colors.white38, - ), - ), - Icon( - likeCountIcon(song.likeCount), - size: 16.0, - color: utils.likeCountColor(song.likeCount), - ), if (!isSelectEnabled) IconButton( icon: const Icon(Icons.more_vert_rounded), diff --git a/src/lib/presentation/widgets/song_list_tile_numbered.dart b/src/lib/presentation/widgets/song_list_tile_numbered.dart index cc72b1d..89e3b52 100644 --- a/src/lib/presentation/widgets/song_list_tile_numbered.dart +++ b/src/lib/presentation/widgets/song_list_tile_numbered.dart @@ -36,34 +36,51 @@ class SongListTileNumbered extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, ), - subtitle: Text( - '${msToTimeString(song.duration)} • ${song.artist}', - style: const TextStyle( - fontWeight: FontWeight.w300, - ), + subtitle: Row( + children: [ + Text( + '${msToTimeString(song.duration)} • ', + style: const TextStyle( + fontWeight: FontWeight.w300, + ), + ), + Icon( + likeCountIcon(song.likeCount), + size: 13.0, + color: likeCountColor(song.likeCount), + ), + if (song.blockLevel > 0) + Padding( + padding: const EdgeInsets.only(left: 2.0), + child: Icon( + blockLevelIcon(song.blockLevel), + size: 13.0, + color: Colors.white38, + ), + ), + if (song.next || song.previous) + Padding( + padding: const EdgeInsets.only(left: 2.0), + child: Icon( + linkIcon(song.previous, song.next), + size: 13.0, + color: linkColor(song.previous, song.next).withOpacity(0.7), + ), + ), + Expanded( + child: Text( + ' • ${song.artist}', + style: const TextStyle( + fontWeight: FontWeight.w300, + ), + ), + ), + ], ), onTap: () => onTap(), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (song.blockLevel > 0) - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Icon( - blockLevelIcon(song.blockLevel), - size: 16.0, - color: Colors.white38, - ), - ), - Icon( - likeCountIcon(song.likeCount), - size: 16.0, - color: song.likeCount == 3 - ? LIGHT2 - : Colors.white.withOpacity( - 0.2 + 0.18 * song.likeCount, - ), - ), if (!isSelectEnabled) IconButton( icon: const Icon(Icons.more_vert_rounded), diff --git a/src/lib/presentation/widgets/switch_text_listtile.dart b/src/lib/presentation/widgets/switch_text_listtile.dart index 13353b2..88ee4fc 100644 --- a/src/lib/presentation/widgets/switch_text_listtile.dart +++ b/src/lib/presentation/widgets/switch_text_listtile.dart @@ -23,7 +23,7 @@ class SwitchTextListTile extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(left: 6.0, right: HORIZONTAL_PADDING), + padding: const EdgeInsets.only(left: 8.0 - 4.0, right: 8.0), child: Row( children: [ SizedBox( diff --git a/src/lib/system/datasources/drift/music_data_dao.dart b/src/lib/system/datasources/drift/music_data_dao.dart index 8a34679..792fe52 100644 --- a/src/lib/system/datasources/drift/music_data_dao.dart +++ b/src/lib/system/datasources/drift/music_data_dao.dart @@ -460,4 +460,11 @@ class MusicDataDao extends DatabaseAccessor } } } + + @override + Future> getSongsFromSameAlbum(SongModel song) async { + return (select(songs)..where((tbl) => tbl.albumId.equals(song.albumId))) + .get() + .then((driftSongs) => driftSongs.map(SongModel.fromDrift).toList()); + } } diff --git a/src/lib/system/datasources/music_data_source_contract.dart b/src/lib/system/datasources/music_data_source_contract.dart index 11d94d8..a7e425b 100644 --- a/src/lib/system/datasources/music_data_source_contract.dart +++ b/src/lib/system/datasources/music_data_source_contract.dart @@ -10,6 +10,7 @@ abstract class MusicDataSource { Future getSongByPath(String path); Future getPredecessor(SongModel song); Future getSuccessor(SongModel song); + Future> getSongsFromSameAlbum(SongModel song); Stream> get albumStream; Stream> getArtistAlbumStream(ArtistModel artist); diff --git a/src/lib/system/repositories/music_data_repository_impl.dart b/src/lib/system/repositories/music_data_repository_impl.dart index 6d4db04..6059429 100644 --- a/src/lib/system/repositories/music_data_repository_impl.dart +++ b/src/lib/system/repositories/music_data_repository_impl.dart @@ -562,4 +562,11 @@ class MusicDataRepositoryImpl implements MusicDataRepository { Future removeBlockedFiles(List paths) async { await _musicDataSource.removeBlockedFiles(paths); } + + @override + Future> isSongFirstLast(Song song) async { + final songs = await _musicDataSource.getSongsFromSameAlbum(song as SongModel).then(_sortAlbumSongs); + + return [songs.indexOf(song) == 0, songs.indexOf(song) == songs.length - 1]; + } } diff --git a/src/pubspec.lock b/src/pubspec.lock index 7775379..d995c0b 100644 --- a/src/pubspec.lock +++ b/src/pubspec.lock @@ -914,6 +914,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + text_scroll: + dependency: "direct main" + description: + name: text_scroll + sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908" + url: "https://pub.dev" + source: hosted + version: "0.2.0" timing: dependency: transitive description: diff --git a/src/pubspec.yaml b/src/pubspec.yaml index cd333bb..28b2292 100644 --- a/src/pubspec.yaml +++ b/src/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: reorderables: ^0.6.0 # MIT sqlite3_flutter_libs: ^0.5.0 # MIT string_similarity: ^2.0.0 # MIT + text_scroll: ^0.2.0 # MIT dev_dependencies: build_runner: ^2.3.3 # BSD 3