From eeca18673672b8c017d80d9a60d980068b3110e6 Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Thu, 12 Dec 2024 21:54:13 +0100 Subject: [PATCH] feat: improve mobile home (#1091) --- lib/app/view/home_page.dart | 156 ------------------ lib/app/view/master_items.dart | 2 +- lib/app/view/mobile_navigation_bar.dart | 32 +--- lib/common/view/loading_grid.dart | 22 +++ lib/common/view/modals.dart | 11 +- lib/home/home_page.dart | 140 ++++++++++++++++ lib/local_audio/local_audio_model.dart | 3 +- lib/player/view/full_height_player.dart | 19 ++- lib/player/view/player_track.dart | 7 +- lib/player/view/queue_button.dart | 77 ++++++--- lib/search/search_model.dart | 4 +- .../view/sliver_radio_country_grid.dart | 12 +- 12 files changed, 263 insertions(+), 222 deletions(-) delete mode 100644 lib/app/view/home_page.dart create mode 100644 lib/home/home_page.dart diff --git a/lib/app/view/home_page.dart b/lib/app/view/home_page.dart deleted file mode 100644 index e8332e17e..000000000 --- a/lib/app/view/home_page.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:watch_it/watch_it.dart'; - -import '../../common/data/audio_type.dart'; -import '../../common/view/adaptive_container.dart'; -import '../../common/view/header_bar.dart'; -import '../../common/view/icons.dart'; -import '../../common/view/search_button.dart'; -import '../../common/view/theme.dart'; -import '../../common/view/ui_constants.dart'; -import '../../constants.dart'; -import '../../extensions/build_context_x.dart'; -import '../../extensions/country_x.dart'; -import '../../l10n/l10n.dart'; -import '../../library/library_model.dart'; -import '../../local_audio/view/playlists_view.dart'; -import '../../search/search_model.dart'; -import '../../search/search_type.dart'; -import '../../search/view/sliver_podcast_search_results.dart'; -import '../../search/view/sliver_radio_country_grid.dart'; - -class HomePage extends StatelessWidget with WatchItMixin { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - final textTheme = context.textTheme; - final l10n = context.l10n; - final playlists = watchPropertyValue( - (LibraryModel m) => m.playlists.keys.toList(), - ); - final style = textTheme.headlineSmall; - const textPadding = EdgeInsets.only( - left: kSmallestSpace, - bottom: kSmallestSpace, - ); - - final country = - watchPropertyValue((SearchModel m) => m.country?.localize(l10n)); - - return Scaffold( - appBar: HeaderBar( - title: Text(l10n.home), - adaptive: false, - actions: [ - const SearchButton(), - IconButton( - selectedIcon: Icon(Iconz.settingsFilled), - icon: Icon(Iconz.settings), - tooltip: l10n.settings, - onPressed: () => di().push(pageId: kSettingsPageId), - ), - const SizedBox( - width: kSmallestSpace, - ), - ], - ), - body: LayoutBuilder( - builder: (context, constraints) { - final padding = getAdaptiveHorizontalPadding( - constraints: constraints, - ); - return CustomScrollView( - slivers: [ - SliverPadding( - padding: padding, - sliver: SliverToBoxAdapter( - child: Padding( - padding: textPadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${l10n.podcast} ${l10n.charts} ${country ?? ''}', - style: style, - ), - IconButton( - onPressed: () { - di().push(pageId: kSearchPageId); - di() - ..setAudioType(AudioType.podcast) - ..setSearchType(SearchType.podcastTitle) - ..search(); - }, - icon: Icon(Iconz.goNext), - ), - ], - ), - ), - ), - ), - SliverPadding( - padding: padding, - sliver: const SliverPodcastSearchResults( - take: 3, - ), - ), - SliverPadding( - padding: padding, - sliver: SliverToBoxAdapter( - child: Padding( - padding: textPadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${l10n.radio} ${l10n.charts} ${country ?? ''}', - style: style, - ), - IconButton( - onPressed: () { - di().push(pageId: kSearchPageId); - di() - ..setAudioType(AudioType.radio) - ..setSearchType(SearchType.radioCountry) - ..search(); - }, - icon: Icon(Iconz.goNext), - ), - ], - ), - ), - ), - ), - SliverPadding( - padding: padding, - sliver: const SliverRadioCountryGrid(), - ), - SliverPadding( - padding: padding, - sliver: SliverToBoxAdapter( - child: Padding( - padding: textPadding, - child: Text( - '${l10n.playlists} ', - style: style, - ), - ), - ), - ), - SliverPadding( - padding: padding.copyWith( - bottom: bottomPlayerPageGap, - ), - sliver: PlaylistsView( - playlists: playlists, - take: 2, - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/lib/app/view/master_items.dart b/lib/app/view/master_items.dart index 16443deea..86822b8f8 100644 --- a/lib/app/view/master_items.dart +++ b/lib/app/view/master_items.dart @@ -7,6 +7,7 @@ import '../../common/view/icons.dart'; import '../../common/view/side_bar_fall_back_image.dart'; import '../../common/view/theme.dart'; import '../../constants.dart'; +import '../../home/home_page.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; import '../../local_audio/view/album_page.dart'; @@ -22,7 +23,6 @@ import '../../radio/view/station_page.dart'; import '../../radio/view/station_page_icon.dart'; import '../../search/view/search_page.dart'; import '../../settings/view/settings_page.dart'; -import 'home_page.dart'; import 'main_page_icon.dart'; class MasterItem { diff --git a/lib/app/view/mobile_navigation_bar.dart b/lib/app/view/mobile_navigation_bar.dart index c1a30e407..1d6c28ee6 100644 --- a/lib/app/view/mobile_navigation_bar.dart +++ b/lib/app/view/mobile_navigation_bar.dart @@ -1,11 +1,9 @@ -import '../../common/data/audio_type.dart'; import '../../common/view/icons.dart'; import '../../common/view/theme.dart'; import '../../common/view/ui_constants.dart'; import '../../constants.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; -import 'main_page_icon.dart'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; @@ -35,41 +33,23 @@ class MobileNavigationBar extends StatelessWidget with WatchItMixin { ), IconButton( isSelected: selectedPageId == kLocalAudioPageId, - selectedIcon: const MainPageIcon( - selected: true, - audioType: AudioType.local, - ), - icon: const MainPageIcon( - selected: false, - audioType: AudioType.local, - ), + selectedIcon: Icon(Iconz.localAudioFilled), + icon: Icon(Iconz.localAudio), tooltip: l10n.local, onPressed: () => di().push(pageId: kLocalAudioPageId), ), IconButton( isSelected: selectedPageId == kRadioPageId, - selectedIcon: const MainPageIcon( - selected: true, - audioType: AudioType.radio, - ), - icon: const MainPageIcon( - selected: false, - audioType: AudioType.radio, - ), + selectedIcon: Icon(Iconz.radioFilled), + icon: Icon(Iconz.radio), tooltip: l10n.radio, onPressed: () => di().push(pageId: kRadioPageId), ), IconButton( isSelected: selectedPageId == kPodcastsPageId, - selectedIcon: const MainPageIcon( - selected: true, - audioType: AudioType.podcast, - ), - icon: const MainPageIcon( - selected: false, - audioType: AudioType.podcast, - ), + selectedIcon: Icon(Iconz.podcastFilled), + icon: Icon(Iconz.podcast), tooltip: l10n.podcasts, onPressed: () => di().push(pageId: kPodcastsPageId), ), diff --git a/lib/common/view/loading_grid.dart b/lib/common/view/loading_grid.dart index 906356a2e..515f815a7 100644 --- a/lib/common/view/loading_grid.dart +++ b/lib/common/view/loading_grid.dart @@ -35,3 +35,25 @@ class LoadingGrid extends StatelessWidget { ); } } + +class SliverLoadingGrid extends StatelessWidget { + const SliverLoadingGrid({ + super.key, + required this.limit, + }); + + final int limit; + + @override + Widget build(BuildContext context) { + return SliverGrid.builder( + itemCount: limit, + gridDelegate: audioCardGridDelegate, + itemBuilder: (context, index) => const AudioCard( + color: Colors.transparent, + showBorder: false, + bottom: AudioCardBottom(), + ), + ); + } +} diff --git a/lib/common/view/modals.dart b/lib/common/view/modals.dart index 10e106768..1a58aa159 100644 --- a/lib/common/view/modals.dart +++ b/lib/common/view/modals.dart @@ -6,12 +6,21 @@ Future showModal({ required BuildContext context, required Widget content, required ModalMode mode, + bool isScrollControlled = false, + bool enableDrag = true, + bool? showDragHandle, }) async { Widget builder(context) => content; switch (mode) { case ModalMode.bottomSheet: - showModalBottomSheet(context: context, builder: builder); + showModalBottomSheet( + isScrollControlled: isScrollControlled, + context: context, + builder: builder, + enableDrag: enableDrag, + showDragHandle: showDragHandle, + ); case ModalMode.dialog: showDialog( diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart new file mode 100644 index 000000000..5dece9d0b --- /dev/null +++ b/lib/home/home_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; + +import '../common/data/audio_type.dart'; +import '../common/view/adaptive_container.dart'; +import '../common/view/header_bar.dart'; +import '../common/view/icons.dart'; +import '../common/view/search_button.dart'; +import '../common/view/theme.dart'; +import '../common/view/ui_constants.dart'; +import '../constants.dart'; +import '../extensions/country_x.dart'; +import '../l10n/l10n.dart'; +import '../library/library_model.dart'; +import '../local_audio/local_audio_model.dart'; +import '../local_audio/local_audio_view.dart'; +import '../local_audio/view/playlists_view.dart'; +import '../search/search_model.dart'; +import '../search/search_type.dart'; +import '../search/view/sliver_podcast_search_results.dart'; +import '../search/view/sliver_radio_country_grid.dart'; + +class HomePage extends StatelessWidget with WatchItMixin { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final playlists = watchPropertyValue( + (LibraryModel m) => m.playlists.keys.toList(), + ); + const textPadding = EdgeInsets.only( + right: kMediumSpace, + left: kSmallestSpace, + ); + + final country = + watchPropertyValue((SearchModel m) => m.country?.localize(l10n)); + + return Scaffold( + appBar: HeaderBar( + title: Text(l10n.home), + adaptive: false, + actions: [ + const SearchButton(), + IconButton( + selectedIcon: Icon(Iconz.settingsFilled), + icon: Icon(Iconz.settings), + tooltip: l10n.settings, + onPressed: () => di().push(pageId: kSettingsPageId), + ), + const SizedBox( + width: kSmallestSpace, + ), + ], + ), + body: LayoutBuilder( + builder: (context, constraints) { + final padding = getAdaptiveHorizontalPadding( + constraints: constraints, + ); + return CustomScrollView( + slivers: [ + SliverPadding( + padding: padding, + sliver: SliverToBoxAdapter( + child: ListTile( + contentPadding: textPadding, + title: Text( + '${l10n.podcast} ${l10n.charts} ${country ?? ''}', + // style: style, + ), + trailing: Icon(Iconz.goNext), + onTap: () { + di().push(pageId: kSearchPageId); + di() + ..setAudioType(AudioType.podcast) + ..setSearchType(SearchType.podcastTitle) + ..search(); + }, + ), + ), + ), + SliverPadding( + padding: padding, + sliver: const SliverPodcastSearchResults( + take: 3, + ), + ), + SliverPadding( + padding: padding, + sliver: SliverToBoxAdapter( + child: ListTile( + contentPadding: textPadding, + title: Text( + '${l10n.radio} ${l10n.charts} ${country ?? ''}', + ), + onTap: () { + di().push(pageId: kSearchPageId); + di() + ..setAudioType(AudioType.radio) + ..setSearchType(SearchType.radioCountry) + ..search(); + }, + trailing: Icon(Iconz.goNext), + ), + ), + ), + SliverPadding( + padding: padding, + sliver: const SliverRadioCountryGrid(), + ), + SliverPadding( + padding: padding, + sliver: SliverToBoxAdapter( + child: ListTile( + contentPadding: textPadding, + title: Text(l10n.playlists), + trailing: Icon(Iconz.goNext), + onTap: () { + di().localAudioindex = + LocalAudioView.playlists.index; + di().push(pageId: kLocalAudioPageId); + }, + ), + ), + ), + SliverPadding( + padding: padding.copyWith( + bottom: bottomPlayerPageGap, + ), + sliver: PlaylistsView(playlists: playlists), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/local_audio/local_audio_model.dart b/lib/local_audio/local_audio_model.dart index 62778c6fa..1c940bf4b 100644 --- a/lib/local_audio/local_audio_model.dart +++ b/lib/local_audio/local_audio_model.dart @@ -21,8 +21,7 @@ class LocalAudioModel extends SafeChangeNotifier { StreamSubscription? _audiosChangedSub; int? _localAudioIndex; - int get localAudioindex => - _localAudioIndex ?? LocalAudioView.values.indexOf(LocalAudioView.albums); + int get localAudioindex => _localAudioIndex ?? LocalAudioView.albums.index; set localAudioindex(int value) { if (value == _localAudioIndex) return; _localAudioIndex = value; diff --git a/lib/player/view/full_height_player.dart b/lib/player/view/full_height_player.dart index b7a3bb9ac..7a3a35bd8 100644 --- a/lib/player/view/full_height_player.dart +++ b/lib/player/view/full_height_player.dart @@ -99,7 +99,7 @@ class FullHeightPlayer extends StatelessWidget with WatchItMixin { ); body = Stack( - alignment: Alignment.topRight, + alignment: Alignment.center, children: [ Center( child: playerWithSidePanel @@ -113,11 +113,20 @@ class FullHeightPlayer extends StatelessWidget with WatchItMixin { ) : column, ), - FullHeightPlayerTopControls( - iconColor: iconColor, - playerPosition: playerPosition, - showQueueButton: !playerWithSidePanel, + Positioned( + top: 0, + right: 0, + child: FullHeightPlayerTopControls( + iconColor: iconColor, + playerPosition: playerPosition, + showQueueButton: !isMobilePlatform && !playerWithSidePanel, + ), ), + if (isMobilePlatform) + const Positioned( + bottom: 2 * kLargestSpace, + child: QueueButton.text(), + ), ], ); } diff --git a/lib/player/view/player_track.dart b/lib/player/view/player_track.dart index 7d1d20956..578fc8efc 100644 --- a/lib/player/view/player_track.dart +++ b/lib/player/view/player_track.dart @@ -70,7 +70,9 @@ class PlayerTrack extends StatelessWidget with WatchItMixin { : const EdgeInsets.only(left: 7, right: 7, top: 3), child: LinearProgress( value: null, - trackHeight: yaruStyled && !bottomPlayer ? 5.0 : 4.0, + trackHeight: yaruStyled && !bottomPlayer + ? 5.0 + : (isMobilePlatform ? 2.0 : 4.0), color: mainColor.withOpacity(0.8), backgroundColor: mainColor.withOpacity(0.4), ), @@ -88,7 +90,8 @@ class PlayerTrack extends StatelessWidget with WatchItMixin { overlayShape: thumbShape, minThumbSeparation: 0, trackShape: trackShape as SliderTrackShape, - trackHeight: bottomPlayer ? 4.0 : 4.0, + trackHeight: + bottomPlayer ? (isMobilePlatform ? 2.0 : 4.0) : 4.0, inactiveTrackColor: mainColor.withOpacity(0.2), activeTrackColor: mainColor.withOpacity(0.85), overlayColor: mainColor, diff --git a/lib/player/view/queue_button.dart b/lib/player/view/queue_button.dart index 0ee2e537b..eac2d0596 100644 --- a/lib/player/view/queue_button.dart +++ b/lib/player/view/queue_button.dart @@ -3,6 +3,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:watch_it/watch_it.dart'; import '../../app/app_model.dart'; +import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/data/audio_type.dart'; import '../../common/view/icons.dart'; @@ -14,12 +15,23 @@ import '../../library/library_model.dart'; import '../player_model.dart'; import 'player_main_controls.dart'; +enum _Mode { + icon, + text; +} + class QueueButton extends StatelessWidget with WatchItMixin { - const QueueButton({super.key, this.color, this.isSelected}); + const QueueButton({super.key, this.color, this.isSelected}) + : _mode = _Mode.icon; + + const QueueButton.text({super.key, this.color, this.isSelected}) + : _mode = _Mode.text; final Color? color; final bool? isSelected; + final _Mode _mode; + @override Widget build(BuildContext context) { final theme = context.theme; @@ -29,26 +41,49 @@ class QueueButton extends StatelessWidget with WatchItMixin { (PlayerModel m) => m.audio?.audioType == AudioType.radio, ); - return IconButton( - isSelected: - isSelected ?? watchPropertyValue((AppModel m) => m.showQueueOverlay), - color: color ?? theme.colorScheme.onSurface, - padding: EdgeInsets.zero, - tooltip: radio ? context.l10n.hearingHistory : context.l10n.queue, - icon: Icon( - Iconz.playlist, - color: color ?? theme.colorScheme.onSurface, - ), - onPressed: playerToTheRight || isFullScreen == true - ? di().setOrToggleQueueOverlay - : () => showModal( - context: context, - content: ModalMode.platformModalMode == ModalMode.bottomSheet - ? const QueueBody() - : const QueueDialog(), - mode: ModalMode.platformModalMode, - ), - ); + return switch (_mode) { + _Mode.icon => IconButton( + isSelected: isSelected ?? + watchPropertyValue((AppModel m) => m.showQueueOverlay), + color: color ?? theme.colorScheme.onSurface, + padding: EdgeInsets.zero, + tooltip: radio ? context.l10n.hearingHistory : context.l10n.queue, + icon: Icon( + Iconz.playlist, + color: color ?? theme.colorScheme.onSurface, + ), + onPressed: () => onPressed(playerToTheRight, isFullScreen, context), + ), + _Mode.text => TextButton( + onPressed: () => onPressed(playerToTheRight, isFullScreen, context), + child: Text( + context.l10n.queue, + style: context.textTheme.bodyLarge?.copyWith( + color: context.colorScheme.onSurface, + ), + ), + ) + }; + } + + void onPressed( + bool playerToTheRight, + bool? isFullScreen, + BuildContext context, + ) { + if ((playerToTheRight || isFullScreen == true) && !isMobilePlatform) { + di().setOrToggleQueueOverlay(); + } else { + showModal( + context: context, + isScrollControlled: true, + showDragHandle: true, + content: ModalMode.platformModalMode == ModalMode.bottomSheet + ? const QueueBody() + : const QueueDialog(), + mode: ModalMode.platformModalMode, + ); + } } } diff --git a/lib/search/search_model.dart b/lib/search/search_model.dart index 2075a899e..cb33b847a 100644 --- a/lib/search/search_model.dart +++ b/lib/search/search_model.dart @@ -334,6 +334,6 @@ class SearchModel extends SafeChangeNotifier { List? _countryCharts; List? get countryCharts => _countryCharts; - Future radioCountrySearch() async => _countryCharts = - await _radioService.search(country: _country?.name, limit: 3); + Future radioCountrySearch({int limit = 3}) async => _countryCharts = + await _radioService.search(country: _country?.name, limit: limit); } diff --git a/lib/search/view/sliver_radio_country_grid.dart b/lib/search/view/sliver_radio_country_grid.dart index 32c93b534..24f78fda9 100644 --- a/lib/search/view/sliver_radio_country_grid.dart +++ b/lib/search/view/sliver_radio_country_grid.dart @@ -3,8 +3,8 @@ import 'package:watch_it/watch_it.dart'; import '../../app/connectivity_model.dart'; import '../../common/data/audio.dart'; +import '../../common/view/loading_grid.dart'; import '../../common/view/offline_page.dart'; -import '../../common/view/progress.dart'; import '../../common/view/theme.dart'; import '../../player/player_model.dart'; import '../../radio/radio_model.dart'; @@ -14,7 +14,9 @@ import '../search_model.dart'; class SliverRadioCountryGrid extends StatefulWidget with WatchItStatefulWidgetMixin { - const SliverRadioCountryGrid({super.key}); + const SliverRadioCountryGrid({super.key, this.limit = 3}); + + final int limit; @override State createState() => _SliverRadioCountryGridState(); @@ -24,7 +26,7 @@ class _SliverRadioCountryGridState extends State { @override void initState() { super.initState(); - di().radioCountrySearch(); + di().radioCountrySearch(limit: widget.limit); } @override @@ -47,9 +49,7 @@ class _SliverRadioCountryGridState extends State { ); if (radioSearchResult == null) { - return const SliverToBoxAdapter( - child: Progress(), - ); + return SliverLoadingGrid(limit: widget.limit); } if (radioSearchResult.isEmpty) {