diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 49170ec19..f05eda01b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:phoenix_theme/phoenix_theme.dart' hide ColorX; import 'package:system_theme/system_theme.dart'; @@ -7,13 +8,15 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../common/view/theme.dart'; +import '../../constants.dart'; import '../../external_path/external_path_service.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; import '../../radio/radio_model.dart'; import '../../settings/settings_model.dart'; import '../connectivity_model.dart'; -import 'scaffold.dart'; +import 'desktop_scaffold.dart'; +import 'master_items.dart'; import 'splash_screen.dart'; class YaruMusicPodApp extends StatelessWidget { @@ -22,7 +25,7 @@ class YaruMusicPodApp extends StatelessWidget { @override Widget build(BuildContext context) { return YaruTheme( - builder: (context, yaru, child) => _MusicPodApp( + builder: (context, yaru, child) => _DesktopMusicPodApp( highContrastTheme: yaruHighContrastLight, highContrastDarkTheme: yaruHighContrastDark, lightTheme: yaruLightWithTweaks(yaru), @@ -38,15 +41,16 @@ class MaterialMusicPodApp extends StatelessWidget { @override Widget build(BuildContext context) => SystemThemeBuilder( builder: (context, accent) { - return _MusicPodApp( - accent: accent.accent, - ); + return isMobile + ? _MobileMusicPodApp(accent: accent.accent) + : _DesktopMusicPodApp(accent: accent.accent); }, ); } -class _MusicPodApp extends StatefulWidget with WatchItStatefulWidgetMixin { - const _MusicPodApp({ +class _DesktopMusicPodApp extends StatefulWidget + with WatchItStatefulWidgetMixin { + const _DesktopMusicPodApp({ this.lightTheme, this.darkTheme, this.accent, @@ -61,10 +65,10 @@ class _MusicPodApp extends StatefulWidget with WatchItStatefulWidgetMixin { final Color? accent; @override - State<_MusicPodApp> createState() => _MusicPodAppState(); + State<_DesktopMusicPodApp> createState() => _DesktopMusicPodAppState(); } -class _MusicPodAppState extends State<_MusicPodApp> { +class _DesktopMusicPodAppState extends State<_DesktopMusicPodApp> { late Future _initFuture; @override @@ -95,12 +99,12 @@ class _MusicPodAppState extends State<_MusicPodApp> { darkTheme: widget.darkTheme ?? phoenix.darkTheme, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: supportedLocales, - onGenerateTitle: (context) => 'MusicPod', + onGenerateTitle: (context) => kAppTitle, home: FutureBuilder( future: _initFuture, builder: (context, snapshot) { return snapshot.data == true - ? const MusicPodScaffold() + ? const DesktopScaffold() : const SplashScreen(); }, ), @@ -116,3 +120,83 @@ class _MusicPodAppState extends State<_MusicPodApp> { ); } } + +class _MobileMusicPodApp extends StatefulWidget + with WatchItStatefulWidgetMixin { + const _MobileMusicPodApp({this.accent}); + + final Color? accent; + + @override + State<_MobileMusicPodApp> createState() => _MobileMusicPodAppState(); +} + +class _MobileMusicPodAppState extends State<_MobileMusicPodApp> { + late Future _initFuture; + + @override + void initState() { + super.initState(); + _initFuture = _init(); + } + + Future _init() async { + await di().init(); + await di().init(); + await di().init(); + if (!mounted) return false; + di().init(); + return true; + } + + @override + Widget build(BuildContext context) { + final themeIndex = watchPropertyValue((SettingsModel m) => m.themeIndex); + final phoenix = phoenixTheme(color: widget.accent ?? Colors.greenAccent); + + final libraryModel = watchIt(); + final masterItems = createMasterItems(libraryModel: libraryModel); + + return MaterialApp( + navigatorKey: libraryModel.masterNavigatorKey, + navigatorObservers: [libraryModel], + initialRoute: + isMobile ? (libraryModel.selectedPageId ?? kSearchPageId) : null, + onGenerateRoute: (settings) { + final page = (masterItems.firstWhereOrNull( + (e) => e.pageId == settings.name, + ) ?? + masterItems.elementAt(0)) + .pageBuilder(context); + + return PageRouteBuilder( + settings: settings, + pageBuilder: (_, __, ___) => FutureBuilder( + future: _initFuture, + builder: (context, snapshot) { + return snapshot.data == true ? page : const SplashScreen(); + }, + ), + transitionsBuilder: (_, a, __, c) => + FadeTransition(opacity: a, child: c), + ); + }, + debugShowCheckedModeBanner: false, + themeMode: ThemeMode.values[themeIndex], + theme: phoenix.lightTheme, + darkTheme: phoenix.darkTheme, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: supportedLocales, + onGenerateTitle: (context) => kAppTitle, + scrollBehavior: const MaterialScrollBehavior().copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.unknown, + PointerDeviceKind.trackpad, + }, + ), + ); + } +} diff --git a/lib/app/view/scaffold.dart b/lib/app/view/desktop_scaffold.dart similarity index 93% rename from lib/app/view/scaffold.dart rename to lib/app/view/desktop_scaffold.dart index 65f0d554a..5b880326b 100644 --- a/lib/app/view/scaffold.dart +++ b/lib/app/view/desktop_scaffold.dart @@ -16,14 +16,14 @@ import '../app_model.dart'; import '../connectivity_model.dart'; import 'master_detail_page.dart'; -class MusicPodScaffold extends StatefulWidget with WatchItStatefulWidgetMixin { - const MusicPodScaffold({super.key}); +class DesktopScaffold extends StatefulWidget with WatchItStatefulWidgetMixin { + const DesktopScaffold({super.key}); @override - State createState() => _MusicPodScaffoldState(); + State createState() => _DesktopScaffoldState(); } -class _MusicPodScaffoldState extends State { +class _DesktopScaffoldState extends State { @override void initState() { super.initState(); diff --git a/lib/app/view/master_detail_page.dart b/lib/app/view/master_detail_page.dart index 2a44be3ed..6cc5c4842 100644 --- a/lib/app/view/master_detail_page.dart +++ b/lib/app/view/master_detail_page.dart @@ -5,33 +5,16 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; -import '../../app_config.dart'; -import '../../common/data/audio.dart'; import '../../common/view/back_gesture.dart'; import '../../common/view/global_keys.dart'; import '../../common/view/header_bar.dart'; import '../../common/view/icons.dart'; -import '../../common/view/side_bar_fall_back_image.dart'; -import '../../common/view/theme.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; -import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; -import '../../local_audio/view/album_page.dart'; -import '../../local_audio/view/local_audio_page.dart'; -import '../../playlists/view/liked_audio_page.dart'; import '../../playlists/view/manual_add_dialog.dart'; -import '../../playlists/view/playlist_page.dart'; -import '../../podcasts/view/podcast_page.dart'; -import '../../podcasts/view/podcast_page_side_bar_icon.dart'; -import '../../podcasts/view/podcast_page_title.dart'; -import '../../podcasts/view/podcasts_page.dart'; -import '../../radio/view/radio_page.dart'; -import '../../radio/view/station_page.dart'; -import '../../radio/view/station_page_icon.dart'; -import '../../search/view/search_page.dart'; -import '../../settings/view/settings_tile.dart'; -import 'main_page_icon.dart'; +import '../../settings/view/settings_action.dart'; +import 'master_items.dart'; import 'master_tile.dart'; class MasterDetailPage extends StatelessWidget with WatchItMixin { @@ -92,8 +75,7 @@ class MasterDetailPage extends StatelessWidget with WatchItMixin { return PageRouteBuilder( settings: settings, - pageBuilder: (_, __, ___) => - useCustomBackGestures ? page : BackGesture(child: page), + pageBuilder: (_, __, ___) => BackGesture(child: page), transitionsBuilder: (_, a, __, c) => FadeTransition(opacity: a, child: c), ); @@ -171,148 +153,9 @@ class MasterPanel extends StatelessWidget { ), ), ), - const SettingsTile(), + const SettingsButton.tile(), ], ), ); } } - -class MasterItem { - MasterItem({ - required this.titleBuilder, - this.subtitleBuilder, - required this.pageBuilder, - this.iconBuilder, - required this.pageId, - }); - - final WidgetBuilder titleBuilder; - final WidgetBuilder? subtitleBuilder; - final WidgetBuilder pageBuilder; - final Widget Function(bool selected)? iconBuilder; - final String pageId; -} - -List createMasterItems({required LibraryModel libraryModel}) { - return [ - MasterItem( - titleBuilder: (context) => Text(context.l10n.search), - pageBuilder: (_) => const SearchPage(), - iconBuilder: (_) => Icon(Iconz.search), - pageId: kSearchPageId, - ), - MasterItem( - titleBuilder: (context) => Text(context.l10n.local), - pageBuilder: (_) => const LocalAudioPage(), - iconBuilder: (selected) => MainPageIcon( - audioType: AudioType.local, - selected: selected, - ), - pageId: kLocalAudioPageId, - ), - MasterItem( - titleBuilder: (context) => Text(context.l10n.radio), - pageBuilder: (_) => const RadioPage(), - iconBuilder: (selected) => MainPageIcon( - audioType: AudioType.radio, - selected: selected, - ), - pageId: kRadioPageId, - ), - MasterItem( - titleBuilder: (context) => Text(context.l10n.podcasts), - pageBuilder: (_) => const PodcastsPage(), - iconBuilder: (selected) => MainPageIcon( - audioType: AudioType.podcast, - selected: selected, - ), - pageId: kPodcastsPageId, - ), - MasterItem( - iconBuilder: (selected) => Icon(Iconz.plus), - titleBuilder: (context) => Text(context.l10n.add), - pageBuilder: (_) => const SizedBox.shrink(), - pageId: kNewPlaylistPageId, - ), - MasterItem( - titleBuilder: (context) => Text(context.l10n.likedSongs), - pageId: kLikedAudiosPageId, - pageBuilder: (_) => const LikedAudioPage(), - subtitleBuilder: (context) => Text(context.l10n.playlist), - iconBuilder: (selected) => LikedAudioPageIcon(selected: selected), - ), - for (final playlist in libraryModel.playlists.entries) - MasterItem( - titleBuilder: (context) => Text(playlist.key), - subtitleBuilder: (context) => Text(context.l10n.playlist), - pageId: playlist.key, - pageBuilder: (_) => PlaylistPage(pageId: playlist.key), - iconBuilder: (selected) => SideBarFallBackImage( - color: getAlphabetColor(playlist.key), - child: Icon( - Iconz.playlist, - ), - ), - ), - for (final podcast in libraryModel.podcasts.entries) - MasterItem( - titleBuilder: (_) => PodcastPageTitle( - feedUrl: podcast.key, - title: podcast.value.firstOrNull?.album ?? - podcast.value.firstOrNull?.title ?? - podcast.value.firstOrNull.toString(), - ), - subtitleBuilder: (context) => Text( - podcast.value.firstOrNull?.artist ?? context.l10n.podcast, - ), - pageId: podcast.key, - pageBuilder: (_) => PodcastPage( - feedUrl: podcast.key, - title: podcast.value.firstOrNull?.album ?? - podcast.value.firstOrNull?.title ?? - podcast.value.firstOrNull.toString(), - imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? - podcast.value.firstOrNull?.imageUrl, - ), - iconBuilder: (selected) => PodcastPageSideBarIcon( - imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? - podcast.value.firstOrNull?.imageUrl, - ), - ), - for (final album in libraryModel.pinnedAlbums.entries) - MasterItem( - titleBuilder: (context) => Text( - album.value.firstOrNull?.album ?? album.key, - ), - subtitleBuilder: (context) => - Text(album.value.firstOrNull?.artist ?? context.l10n.album), - pageId: album.key, - pageBuilder: (_) => AlbumPage( - album: album.value, - id: album.key, - ), - iconBuilder: (selected) => AlbumPageSideBarIcon( - audio: album.value.firstOrNull, - ), - ), - for (final station in libraryModel.starredStations.entries - .where((e) => e.value.isNotEmpty)) - MasterItem( - titleBuilder: (context) => - Text(station.value.first.title ?? station.key), - subtitleBuilder: (context) { - return Text(context.l10n.station); - }, - pageId: station.key, - pageBuilder: (_) => StationPage( - station: station.value.first, - ), - iconBuilder: (selected) => StationPageIcon( - imageUrl: station.value.first.imageUrl, - fallBackColor: getAlphabetColor(station.value.first.title ?? 'a'), - selected: selected, - ), - ), - ]; -} diff --git a/lib/app/view/master_items.dart b/lib/app/view/master_items.dart new file mode 100644 index 000000000..4bbf4d946 --- /dev/null +++ b/lib/app/view/master_items.dart @@ -0,0 +1,162 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../../common/data/audio.dart'; +import '../../common/view/icons.dart'; +import '../../common/view/side_bar_fall_back_image.dart'; +import '../../common/view/theme.dart'; +import '../../constants.dart'; +import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; +import '../../local_audio/view/album_page.dart'; +import '../../local_audio/view/local_audio_page.dart'; +import '../../playlists/view/liked_audio_page.dart'; +import '../../playlists/view/playlist_page.dart'; +import '../../podcasts/view/podcast_page.dart'; +import '../../podcasts/view/podcast_page_side_bar_icon.dart'; +import '../../podcasts/view/podcast_page_title.dart'; +import '../../podcasts/view/podcasts_page.dart'; +import '../../radio/view/radio_page.dart'; +import '../../radio/view/station_page.dart'; +import '../../radio/view/station_page_icon.dart'; +import '../../search/view/search_page.dart'; +import 'main_page_icon.dart'; + +class MasterItem { + MasterItem({ + required this.titleBuilder, + this.subtitleBuilder, + required this.pageBuilder, + this.iconBuilder, + required this.pageId, + }); + + final WidgetBuilder titleBuilder; + final WidgetBuilder? subtitleBuilder; + final WidgetBuilder pageBuilder; + final Widget Function(bool selected)? iconBuilder; + final String pageId; +} + +List createMasterItems({required LibraryModel libraryModel}) { + return [ + MasterItem( + titleBuilder: (context) => Text(context.l10n.search), + pageBuilder: (_) => const SearchPage(), + iconBuilder: (_) => Icon(Iconz.search), + pageId: kSearchPageId, + ), + MasterItem( + titleBuilder: (context) => Text(context.l10n.local), + pageBuilder: (_) => const LocalAudioPage(), + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.local, + selected: selected, + ), + pageId: kLocalAudioPageId, + ), + MasterItem( + titleBuilder: (context) => Text(context.l10n.radio), + pageBuilder: (_) => const RadioPage(), + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.radio, + selected: selected, + ), + pageId: kRadioPageId, + ), + MasterItem( + titleBuilder: (context) => Text(context.l10n.podcasts), + pageBuilder: (_) => const PodcastsPage(), + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.podcast, + selected: selected, + ), + pageId: kPodcastsPageId, + ), + MasterItem( + iconBuilder: (selected) => Icon(Iconz.plus), + titleBuilder: (context) => Text(context.l10n.add), + pageBuilder: (_) => const SizedBox.shrink(), + pageId: kNewPlaylistPageId, + ), + MasterItem( + titleBuilder: (context) => Text(context.l10n.likedSongs), + pageId: kLikedAudiosPageId, + pageBuilder: (_) => const LikedAudioPage(), + subtitleBuilder: (context) => Text(context.l10n.playlist), + iconBuilder: (selected) => LikedAudioPageIcon(selected: selected), + ), + for (final playlist in libraryModel.playlists.entries) + MasterItem( + titleBuilder: (context) => Text(playlist.key), + subtitleBuilder: (context) => Text(context.l10n.playlist), + pageId: playlist.key, + pageBuilder: (_) => PlaylistPage(pageId: playlist.key), + iconBuilder: (selected) => SideBarFallBackImage( + color: getAlphabetColor(playlist.key), + child: Icon( + Iconz.playlist, + ), + ), + ), + for (final podcast in libraryModel.podcasts.entries) + MasterItem( + titleBuilder: (_) => PodcastPageTitle( + feedUrl: podcast.key, + title: podcast.value.firstOrNull?.album ?? + podcast.value.firstOrNull?.title ?? + podcast.value.firstOrNull.toString(), + ), + subtitleBuilder: (context) => Text( + podcast.value.firstOrNull?.artist ?? context.l10n.podcast, + ), + pageId: podcast.key, + pageBuilder: (_) => PodcastPage( + feedUrl: podcast.key, + title: podcast.value.firstOrNull?.album ?? + podcast.value.firstOrNull?.title ?? + podcast.value.firstOrNull.toString(), + imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? + podcast.value.firstOrNull?.imageUrl, + ), + iconBuilder: (selected) => PodcastPageSideBarIcon( + imageUrl: podcast.value.firstOrNull?.albumArtUrl ?? + podcast.value.firstOrNull?.imageUrl, + ), + ), + for (final album in libraryModel.pinnedAlbums.entries) + MasterItem( + titleBuilder: (context) => Text( + album.value.firstOrNull?.album ?? album.key, + ), + subtitleBuilder: (context) => + Text(album.value.firstOrNull?.artist ?? context.l10n.album), + pageId: album.key, + pageBuilder: (_) => AlbumPage( + album: album.value, + id: album.key, + ), + iconBuilder: (selected) => AlbumPageSideBarIcon( + audio: album.value.firstOrNull, + ), + ), + for (final station in libraryModel.starredStations.entries + .where((e) => e.value.isNotEmpty)) + MasterItem( + titleBuilder: (context) => + Text(station.value.first.title ?? station.key), + subtitleBuilder: (context) { + return Text(context.l10n.station); + }, + pageId: station.key, + pageBuilder: (_) => StationPage( + station: station.value.first, + ), + iconBuilder: (selected) => StationPageIcon( + imageUrl: station.value.first.imageUrl, + fallBackColor: getAlphabetColor(station.value.first.title ?? 'a'), + selected: selected, + ), + ), + ]; +} diff --git a/lib/app/view/mobile_navigation_bar.dart b/lib/app/view/mobile_navigation_bar.dart new file mode 100644 index 000000000..363303c89 --- /dev/null +++ b/lib/app/view/mobile_navigation_bar.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; + +import '../../common/data/audio.dart'; +import '../../common/view/icons.dart'; +import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; +import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; +import '../../player/view/bottom_player.dart'; +import '../../player/view/full_height_player.dart'; +import '../../player/view/player_view.dart'; +import '../app_model.dart'; +import 'main_page_icon.dart'; + +class MobilePlayerAndNavigationBar extends StatelessWidget { + const MobilePlayerAndNavigationBar({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Material( + color: context.theme.cardColor, + child: watchPropertyValue((AppModel m) => m.fullWindowMode) ?? false + ? const FullHeightPlayer( + playerPosition: PlayerPosition.fullWindow, + ) + : const Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomPlayer(), + MobileNavigationBar(), + ], + ), + ), + ); + } +} + +class MobileNavigationBar extends StatelessWidget with WatchItMixin { + const MobileNavigationBar({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + final selectedPageId = + watchPropertyValue((LibraryModel m) => m.selectedPageId); + + final destinations = { + kSearchPageId: NavigationDestination( + selectedIcon: Icon(Iconz.search), + icon: Icon(Iconz.search), + label: l10n.search, + ), + kLocalAudioPageId: NavigationDestination( + selectedIcon: + const MainPageIcon(selected: true, audioType: AudioType.local), + icon: const MainPageIcon(selected: false, audioType: AudioType.local), + label: l10n.local, + ), + kRadioPageId: NavigationDestination( + selectedIcon: + const MainPageIcon(selected: true, audioType: AudioType.radio), + icon: const MainPageIcon(selected: false, audioType: AudioType.radio), + label: l10n.radio, + ), + kPodcastsPageId: NavigationDestination( + selectedIcon: + const MainPageIcon(selected: true, audioType: AudioType.podcast), + icon: const MainPageIcon(selected: false, audioType: AudioType.podcast), + label: l10n.podcasts, + ), + }; + + return NavigationBar( + backgroundColor: context.theme.cardColor, + selectedIndex: selectedPageId == null + ? 0 + : destinations.keys.toList().indexOf(selectedPageId), + onDestinationSelected: (index) => + di().push(pageId: destinations.keys.elementAt(index)), + destinations: destinations.values.toList(), + ); + } +} diff --git a/lib/app/view/music_pod_scaffold.dart b/lib/app/view/music_pod_scaffold.dart new file mode 100644 index 000000000..a72a0910d --- /dev/null +++ b/lib/app/view/music_pod_scaffold.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + +import 'mobile_navigation_bar.dart'; + +class MusicPodScaffold extends StatelessWidget with WatchItMixin { + const MusicPodScaffold({super.key, required this.body, this.appBar}); + + final Widget body; + final PreferredSizeWidget? appBar; + + @override + Widget build(BuildContext context) => Scaffold( + resizeToAvoidBottomInset: isMobile ? false : null, + body: isMobile ? SafeArea(child: body) : body, + appBar: appBar, + bottomNavigationBar: + isMobile ? const MobilePlayerAndNavigationBar() : null, + ); +} diff --git a/lib/app_config.dart b/lib/app_config.dart index 02c3a98f7..47077232e 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -18,5 +18,5 @@ bool get allowVideoFullScreen => !Platform.isLinux; bool get isGtkApp => Platform.isLinux; -bool get useCustomBackGestures => +bool get useSystemBackGestures => Platform.isAndroid || Platform.isIOS || Platform.isFuchsia; diff --git a/lib/common/data/audio.dart b/lib/common/data/audio.dart index 5fa997e86..cb77485da 100644 --- a/lib/common/data/audio.dart +++ b/lib/common/data/audio.dart @@ -364,10 +364,15 @@ class Audio { final artistName = artist; final id = albumName == null && artistName == null ? null - : '${artistName ?? ''}:${albumName ?? ''}'; - return id; + : _isMobile + ? '${artistName ?? ''}_${albumName ?? ''}' + : '${artistName ?? ''}:${albumName ?? ''}'; + return _isMobile ? id?.replaceAll(' ', '_') : id; } + bool get _isMobile => + Platform.isAndroid || Platform.isIOS || Platform.isFuchsia; + bool get hasPathAndId => albumId?.isNotEmpty == true && path != null && diff --git a/lib/common/view/audio_tile_bottom_sheet.dart b/lib/common/view/audio_tile_bottom_sheet.dart index d779650b5..007d7af24 100644 --- a/lib/common/view/audio_tile_bottom_sheet.dart +++ b/lib/common/view/audio_tile_bottom_sheet.dart @@ -141,9 +141,8 @@ class AudioTileBottomSheet extends StatelessWidget { Navigator.of(context).pop(); showDialog( context: context, - builder: (context) { - return MetaDataDialog(audio: audio); - }, + builder: (context) => + MetaDataContent.dialog(audio: audio), ); }, icon: Icon(Iconz.info), diff --git a/lib/common/view/audio_tile_option_button.dart b/lib/common/view/audio_tile_option_button.dart index 3610adb27..0e4f8b49b 100644 --- a/lib/common/view/audio_tile_option_button.dart +++ b/lib/common/view/audio_tile_option_button.dart @@ -94,9 +94,7 @@ class AudioTileOptionButton extends StatelessWidget { PopupMenuItem( onTap: () => showDialog( context: context, - builder: (context) { - return MetaDataDialog(audio: audio); - }, + builder: (context) => MetaDataContent.dialog(audio: audio), ), child: YaruTile( leading: Icon(Iconz.info), diff --git a/lib/common/view/header_bar.dart b/lib/common/view/header_bar.dart index f8a7f7ef0..be127d660 100644 --- a/lib/common/view/header_bar.dart +++ b/lib/common/view/header_bar.dart @@ -44,16 +44,18 @@ class HeaderBar extends StatelessWidget @override Widget build(BuildContext context) { + final useSidebarButton = isMobile ? false : includeSidebarButton; + final useBackButton = isMobile ? true : includeBackButton; final canPop = watchPropertyValue((LibraryModel m) => m.canPop); Widget? leading; - if (includeSidebarButton && + if (useSidebarButton && !context.showMasterPanel && masterScaffoldKey.currentState?.isDrawerOpen == false) { leading = const SidebarButton(); } else { - if (includeBackButton && canPop) { + if (useBackButton && canPop) { leading = const NavBackButton(); } else { leading = isMobile diff --git a/lib/common/view/meta_data_dialog.dart b/lib/common/view/meta_data_dialog.dart index df16c7f90..57a6d0586 100644 --- a/lib/common/view/meta_data_dialog.dart +++ b/lib/common/view/meta_data_dialog.dart @@ -1,13 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + import '../../app_config.dart'; import '../../l10n/l10n.dart'; import '../data/audio.dart'; -import 'package:flutter/material.dart'; -import 'package:yaru/yaru.dart'; +import 'modal_mode.dart'; + +class MetaDataContent extends StatelessWidget { + const MetaDataContent.dialog({ + super.key, + required this.audio, + }) : _mode = ModalMode.dialog; -class MetaDataDialog extends StatelessWidget { - const MetaDataDialog({super.key, required this.audio}); + const MetaDataContent.bottomSheet({ + super.key, + required this.audio, + }) : _mode = ModalMode.bottomSheet; final Audio audio; + final ModalMode _mode; @override Widget build(BuildContext context) { @@ -55,28 +66,42 @@ class MetaDataDialog extends StatelessWidget { ), }; - return AlertDialog( - title: yaruStyled - ? YaruDialogTitleBar( - title: Text(l10n.metadata), - ) - : Center(child: Text(l10n.metadata)), - titlePadding: - yaruStyled ? EdgeInsets.zero : const EdgeInsets.only(top: 10), - contentPadding: const EdgeInsets.only(bottom: 12), - scrollable: true, - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: items - .map( - (e) => ListTile( - dense: true, - title: Text(e.$1), - subtitle: Text(e.$2), - ), - ) - .toList(), - ), + final title = yaruStyled + ? YaruDialogTitleBar( + title: Text(l10n.metadata), + ) + : Center(child: Text(l10n.metadata)); + + final titlePadding = + yaruStyled ? EdgeInsets.zero : const EdgeInsets.only(top: 10); + + const edgeInsets = EdgeInsets.only(bottom: 12); + + final body = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items + .map( + (e) => ListTile( + dense: true, + title: Text(e.$1), + subtitle: Text(e.$2), + ), + ) + .toList(), ); + + return switch (_mode) { + ModalMode.dialog => AlertDialog( + title: title, + titlePadding: titlePadding, + contentPadding: edgeInsets, + scrollable: true, + content: body, + ), + ModalMode.bottomSheet => BottomSheet( + onClosing: () {}, + builder: (context) => body, + ) + }; } } diff --git a/lib/common/view/modal_mode.dart b/lib/common/view/modal_mode.dart new file mode 100644 index 000000000..d6ff76f4a --- /dev/null +++ b/lib/common/view/modal_mode.dart @@ -0,0 +1,4 @@ +enum ModalMode { + dialog, + bottomSheet, +} diff --git a/lib/common/view/mpv_metadata_dialog.dart b/lib/common/view/mpv_metadata_dialog.dart index 8ef42b678..53de9364c 100644 --- a/lib/common/view/mpv_metadata_dialog.dart +++ b/lib/common/view/mpv_metadata_dialog.dart @@ -1,28 +1,36 @@ -import '../data/mpv_meta_data.dart'; -import 'icons.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:yaru/yaru.dart'; + +import '../../constants.dart'; +import '../data/mpv_meta_data.dart'; +import 'icons.dart'; +import 'modal_mode.dart'; import 'safe_network_image.dart'; import 'stream_provider_share_button.dart'; class MpvMetadataDialog extends StatelessWidget { - const MpvMetadataDialog({ + const MpvMetadataDialog.dialog({ super.key, this.image, required this.mpvMetaData, - }); + }) : _mode = ModalMode.dialog; + + const MpvMetadataDialog.bottomSheet({ + super.key, + this.image, + required this.mpvMetaData, + }) : _mode = ModalMode.bottomSheet; final String? image; final MpvMetaData mpvMetaData; + final ModalMode _mode; @override Widget build(BuildContext context) { - return SimpleDialog( - titlePadding: EdgeInsets.zero, - contentPadding: const EdgeInsets.only(bottom: 10), - children: [ - if (image != null) + final children = [ + if (image != null) + if (_mode == ModalMode.dialog) ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(kYaruContainerRadius), @@ -36,30 +44,67 @@ class MpvMetadataDialog extends StatelessWidget { url: image, ), ), + ) + else + ListTile( + title: Text(mpvMetaData.icyTitle), + subtitle: Text(mpvMetaData.icyName), + leading: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(kAudioTrackWidth / 10), + topRight: Radius.circular(kAudioTrackWidth / 10), + ), + child: SafeNetworkImage( + url: image, + height: kAudioTrackWidth, + fit: BoxFit.cover, + fallBackIcon: Icon(Iconz.radio), + errorIcon: Icon(Iconz.radio), + ), + ), ), - Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 8), - child: StreamProviderRow( - text: mpvMetaData.icyTitle, - ), + Padding( + padding: EdgeInsets.only( + left: 8, + right: 8, + top: _mode == ModalMode.dialog ? 8 : 4, ), - ...mpvMetaData - .toMap() - .entries - .map( - (e) => ListTile( - onTap: switch (e.key) { - 'icy-url' => () => launchUrl(Uri.parse(e.value)), - _ => null, - }, - dense: true, - title: Text(e.key), - subtitle: Text(e.value), - ), - ) - .toList() - .reversed, - ], - ); + child: StreamProviderRow( + text: mpvMetaData.icyTitle, + ), + ), + ...mpvMetaData + .toMap() + .entries + .map( + (e) => ListTile( + onTap: switch (e.key) { + 'icy-url' => () => launchUrl(Uri.parse(e.value)), + _ => null, + }, + dense: true, + title: Text(e.key), + subtitle: Text(e.value), + ), + ) + .toList() + .reversed, + ]; + + return switch (_mode) { + ModalMode.dialog => SimpleDialog( + titlePadding: EdgeInsets.zero, + contentPadding: const EdgeInsets.only(bottom: 10), + children: children, + ), + ModalMode.bottomSheet => BottomSheet( + enableDrag: false, + onClosing: () {}, + builder: (context) => SizedBox( + height: 800, + child: Column(children: children), + ), + ) + }; } } diff --git a/lib/common/view/sliver_audio_page.dart b/lib/common/view/sliver_audio_page.dart index 221310b64..6bc881b46 100644 --- a/lib/common/view/sliver_audio_page.dart +++ b/lib/common/view/sliver_audio_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../constants.dart'; import '../../library/library_model.dart'; import '../../search/search_model.dart'; @@ -57,8 +58,7 @@ class SliverAudioPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ diff --git a/lib/common/view/theme.dart b/lib/common/view/theme.dart index 07e443e94..43fd6cd98 100644 --- a/lib/common/view/theme.dart +++ b/lib/common/view/theme.dart @@ -246,9 +246,9 @@ EdgeInsets get audioTilePadding => kAudioTilePadding; SliverGridDelegate get audioCardGridDelegate => isMobile ? kMobileAudioCardGridDelegate : kAudioCardGridDelegate; -EdgeInsetsGeometry get appBarSingleActionSpacing => Platform.isMacOS +EdgeInsets get appBarSingleActionSpacing => Platform.isMacOS ? const EdgeInsets.only(right: 5, left: 5) - : const EdgeInsets.only(right: 10, left: 20); + : EdgeInsets.only(right: 10, left: isMobile ? 0 : 20); EdgeInsetsGeometry get radioHistoryListPadding => EdgeInsets.only(left: yaruStyled ? 0 : 5); diff --git a/lib/constants.dart b/lib/constants.dart index 1d1a6a3cc..f51166db6 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -175,6 +175,7 @@ const kPatchNotesDisposed = 'kPatchNotesDisposed'; const kCloseBtnAction = 'closeBtnAction'; const kUseMoreAnimations = 'useMoreAnimations'; const kShowPositionDuration = 'showPositionDuration'; +const kSettingsPageId = 'settings'; const shops = { 'https://us.7digital.com/': '7digital', diff --git a/lib/extensions/build_context_x.dart b/lib/extensions/build_context_x.dart index 93fb16d7e..8eaca6e11 100644 --- a/lib/extensions/build_context_x.dart +++ b/lib/extensions/build_context_x.dart @@ -8,6 +8,7 @@ extension BuildContextX on BuildContext { TextTheme get textTheme => theme.textTheme; Size get mediaQuerySize => MediaQuery.sizeOf(this); + bool get isPortrait => MediaQuery.orientationOf(this) == Orientation.portrait; bool get smallWindow => mediaQuerySize.width < kMasterDetailBreakPoint; bool get wideWindow => mediaQuerySize.width < kAdaptivContainerBreakPoint; diff --git a/lib/library/library_model.dart b/lib/library/library_model.dart index 1e52b7b2f..9104b59f0 100644 --- a/lib/library/library_model.dart +++ b/lib/library/library_model.dart @@ -204,7 +204,7 @@ class LibraryModel extends SafeChangeNotifier implements NavigatorObserver { await _masterNavigatorKey.currentState?.pushNamed(pageId); } else if (builder != null) { final materialPageRoute = MaterialPageRoute( - builder: (context) => useCustomBackGestures + builder: (context) => useSystemBackGestures ? builder(context) : BackGesture(child: builder(context)), maintainState: maintainState, diff --git a/lib/local_audio/view/artist_page.dart b/lib/local_audio/view/artist_page.dart index d1ae877a9..5ef4850a6 100644 --- a/lib/local_audio/view/artist_page.dart +++ b/lib/local_audio/view/artist_page.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_page_header.dart'; @@ -68,8 +68,7 @@ class ArtistPage extends StatelessWidget with WatchItMixin { pageId: text, ); - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ diff --git a/lib/local_audio/view/genre_page.dart b/lib/local_audio/view/genre_page.dart index c746cd22f..5ce4e4eb9 100644 --- a/lib/local_audio/view/genre_page.dart +++ b/lib/local_audio/view/genre_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:radio_browser_api/radio_browser_api.dart' hide State; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/theme.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/header_bar.dart'; @@ -45,8 +45,7 @@ class _GenrePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, titleSpacing: 0, diff --git a/lib/local_audio/view/local_audio_page.dart b/lib/local_audio/view/local_audio_page.dart index 7d39bd8c0..7fc75386e 100644 --- a/lib/local_audio/view/local_audio_page.dart +++ b/lib/local_audio/view/local_audio_page.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; -import '../../common/view/common_widgets.dart'; import '../../common/view/header_bar.dart'; import '../../common/view/search_button.dart'; import '../../common/view/sliver_filter_app_bar.dart'; @@ -15,7 +15,7 @@ import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; import '../../search/search_model.dart'; import '../../search/search_type.dart'; -import '../../settings/view/settings_dialog.dart'; +import '../../settings/view/settings_action.dart'; import '../local_audio_model.dart'; import 'failed_imports_content.dart'; import 'local_audio_body.dart'; @@ -58,12 +58,12 @@ class _LocalAudioPageState extends State { final index = watchPropertyValue((LocalAudioModel m) => m.localAudioindex); final localAudioView = LocalAudioView.values[index]; - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, titleSpacing: 0, actions: [ + if (isMobile) const SettingsButton.icon(), Padding( padding: appBarSingleActionSpacing, child: SearchButton( @@ -111,15 +111,7 @@ class _LocalAudioPageState extends State { const SizedBox( height: kYaruPagePadding, ), - ImportantButton( - child: Text(context.l10n.settings), - onPressed: () { - showDialog( - context: context, - builder: (_) => const SettingsDialog(), - ); - }, - ), + const SettingsButton.important(), ], ), ), diff --git a/lib/player/view/bottom_player.dart b/lib/player/view/bottom_player.dart index 8aae36e37..c7e8cc217 100644 --- a/lib/player/view/bottom_player.dart +++ b/lib/player/view/bottom_player.dart @@ -42,95 +42,96 @@ class BottomPlayer extends StatelessWidget with WatchItMixin { m.queue.length > 1 || audio?.audioType == AudioType.local, ); - final player = SizedBox( - height: bottomPlayerHeight, - child: Column( - children: [ - PlayerTrack( - active: active, - bottomPlayer: true, - ), - InkWell( - onTap: () => appModel.setFullWindowMode(true), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 10, right: 20), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: BottomPlayerImage( - audio: audio, - size: bottomPlayerHeight - 24, - videoController: model.controller, - isVideo: isVideo, - isOnline: isOnline, - ), - ), - ), - Expanded( - flex: 4, - child: Row( - children: [ - const Flexible( - flex: 5, - child: PlayerTitleAndArtist( - playerPosition: PlayerPosition.bottom, - ), - ), - if (!smallWindow) - Padding( - padding: const EdgeInsets.only(left: 10), - child: switch (audio?.audioType) { - AudioType.local => LikeIcon( - audio: audio, - color: theme.colorScheme.onSurface, - ), - AudioType.radio => RadioLikeIcon(audio: audio), - _ => const SizedBox.shrink(), - }, - ), - ], - ), + final children = [ + PlayerTrack( + active: active, + bottomPlayer: true, + ), + InkWell( + onTap: () => appModel.setFullWindowMode(true), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 20), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: BottomPlayerImage( + audio: audio, + size: bottomPlayerHeight - 24, + videoController: model.controller, + isVideo: isVideo, + isOnline: isOnline, ), - if (!smallWindow) - Expanded( - flex: 6, - child: PlayerMainControls(active: active), + ), + ), + Expanded( + flex: 4, + child: Row( + children: [ + const Flexible( + flex: 5, + child: PlayerTitleAndArtist( + playerPosition: PlayerPosition.bottom, + ), ), - if (!smallWindow) - Flexible( - flex: 4, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (audio?.audioType == AudioType.podcast) - PlaybackRateButton(active: active), - const VolumeSliderPopup(), - if (showQueueButton) const QueueButton(), - IconButton( - tooltip: context.l10n.fullWindow, - icon: Icon( - Iconz.fullWindow, + if (!smallWindow) + Padding( + padding: const EdgeInsets.only(left: 10), + child: switch (audio?.audioType) { + AudioType.local => LikeIcon( + audio: audio, color: theme.colorScheme.onSurface, ), - onPressed: () => appModel.setFullWindowMode(true), - ), - ], + AudioType.radio => RadioLikeIcon(audio: audio), + _ => const SizedBox.shrink(), + }, + ), + ], + ), + ), + if (!smallWindow) + Expanded( + flex: 6, + child: PlayerMainControls(active: active), + ), + if (!smallWindow) + Flexible( + flex: 4, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (audio?.audioType == AudioType.podcast) + PlaybackRateButton(active: active), + const VolumeSliderPopup(), + if (showQueueButton) const QueueButton(), + IconButton( + tooltip: context.l10n.fullWindow, + icon: Icon( + Iconz.fullWindow, + color: theme.colorScheme.onSurface, + ), + onPressed: () => appModel.setFullWindowMode(true), ), - ) - else - PlayButton(active: active), - const SizedBox( - width: 10, + ], ), - ], + ) + else + PlayButton(active: active), + const SizedBox( + width: 10, ), - ), + ], ), - ], + ), + ), + ]; + final player = SizedBox( + height: bottomPlayerHeight, + child: Column( + children: (isMobile ? children.reversed : children).toList(), ), ); diff --git a/lib/player/view/full_height_player.dart b/lib/player/view/full_height_player.dart index 4ef611477..010871e95 100644 --- a/lib/player/view/full_height_player.dart +++ b/lib/player/view/full_height_player.dart @@ -52,7 +52,7 @@ class FullHeightPlayer extends StatelessWidget with WatchItMixin { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - const FullHeightPlayerImage(), + if (!isMobile || context.isPortrait) const FullHeightPlayerImage(), const SizedBox( height: kYaruPagePadding, ), diff --git a/lib/player/view/full_height_player_image.dart b/lib/player/view/full_height_player_image.dart index b12b3c8e8..c80fa8366 100644 --- a/lib/player/view/full_height_player_image.dart +++ b/lib/player/view/full_height_player_image.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; import '../../constants.dart'; +import '../../extensions/build_context_x.dart'; import '../../local_audio/view/local_cover.dart'; import '../player_model.dart'; import 'player_fall_back_image.dart'; @@ -26,11 +28,19 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin { Widget build(BuildContext context) { final audio = watchPropertyValue((PlayerModel m) => m.audio); + final size = context.isPortrait + ? fullHeightPlayerImageSize + : isMobile + ? fullHeightPlayerImageSize / 3 + : fullHeightPlayerImageSize; + final theHeight = height ?? size; + final theWidth = width ?? size; + final fallBackImage = PlayerFallBackImage( noIcon: emptyFallBack, audio: audio, - height: height ?? fullHeightPlayerImageSize, - width: width ?? fullHeightPlayerImageSize, + height: theHeight, + width: theWidth, ); Widget image; @@ -39,15 +49,15 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin { key: ValueKey(audio!.path), albumId: audio.albumId!, path: audio.path!, - width: width, - height: height, + width: theWidth, + height: theHeight, fit: fit ?? BoxFit.fitHeight, fallback: fallBackImage, ); } else { image = PlayerRemoteSourceImage( - height: height ?? fullHeightPlayerImageSize, - width: width ?? fullHeightPlayerImageSize, + height: theHeight, + width: theWidth, fit: fit, fallBackIcon: fallBackImage, errorIcon: fallBackImage, @@ -55,8 +65,8 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin { } return SizedBox( - height: height ?? fullHeightPlayerImageSize, - width: width ?? fullHeightPlayerImageSize, + height: theHeight, + width: theWidth, child: ClipRRect( borderRadius: borderRadius ?? BorderRadius.circular(10), child: AnimatedSwitcher( diff --git a/lib/playlists/view/playlist_page.dart b/lib/playlists/view/playlist_page.dart index 835369758..b863acce2 100644 --- a/lib/playlists/view/playlist_page.dart +++ b/lib/playlists/view/playlist_page.dart @@ -5,8 +5,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:super_drag_and_drop/super_drag_and_drop.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/theme.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_page_header.dart'; @@ -88,8 +88,7 @@ class PlaylistPage extends StatelessWidget with WatchItMixin { return DropOperation.none; } }, - child: Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + child: MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ diff --git a/lib/podcasts/view/podcast_page.dart b/lib/podcasts/view/podcast_page.dart index 1d3c7e212..b09b3b273 100644 --- a/lib/podcasts/view/podcast_page.dart +++ b/lib/podcasts/view/podcast_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_filter.dart'; @@ -98,8 +99,7 @@ class _PodcastPageState extends State { .map((e) => e.copyWith(path: libraryModel.getDownload(e.url))) .toList(); - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ diff --git a/lib/podcasts/view/podcasts_page.dart b/lib/podcasts/view/podcasts_page.dart index 66bbaf139..d1a934327 100644 --- a/lib/podcasts/view/podcasts_page.dart +++ b/lib/podcasts/view/podcasts_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/theme.dart'; +import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/header_bar.dart'; import '../../common/view/search_button.dart'; @@ -11,6 +12,7 @@ import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; import '../../search/search_model.dart'; import '../../search/search_type.dart'; +import '../../settings/view/settings_action.dart'; import '../podcast_model.dart'; import 'podcasts_collection_body.dart'; @@ -34,12 +36,12 @@ class _PodcastsPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( titleSpacing: 0, adaptive: true, actions: [ + if (isMobile) const SettingsButton.icon(), Padding( padding: appBarSingleActionSpacing, child: SearchButton( diff --git a/lib/radio/view/radio_history_tile.dart b/lib/radio/view/radio_history_tile.dart index 729bfb9e7..69621868d 100644 --- a/lib/radio/view/radio_history_tile.dart +++ b/lib/radio/view/radio_history_tile.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/constants.dart'; import 'package:yaru/yaru.dart'; import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/copy_clipboard_content.dart'; import '../../common/view/icons.dart'; +import '../../common/view/modals.dart'; import '../../common/view/mpv_metadata_dialog.dart'; import '../../common/view/snackbars.dart'; import '../../common/view/tapable_text.dart'; @@ -66,15 +66,21 @@ class RadioHistoryTile extends StatelessWidget { trailing: IconButton( tooltip: context.l10n.metadata, onPressed: () { - final image = di().getCover(icyTitle); + final imageUrl = di().getCover(icyTitle); final metadata = di().getMetadata(icyTitle); if (metadata == null) return; - showDialog( + + showModal( context: context, - builder: (context) => MpvMetadataDialog( - mpvMetaData: metadata, - image: image, - ), + content: isMobile + ? MpvMetadataDialog.bottomSheet( + image: imageUrl, + mpvMetaData: metadata, + ) + : MpvMetadataDialog.dialog( + image: imageUrl, + mpvMetaData: metadata, + ), ); }, icon: Icon(Iconz.info), diff --git a/lib/radio/view/radio_history_tile_image.dart b/lib/radio/view/radio_history_tile_image.dart index 6844b2670..a0abd9111 100644 --- a/lib/radio/view/radio_history_tile_image.dart +++ b/lib/radio/view/radio_history_tile_image.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; import '../../common/view/icons.dart'; +import '../../common/view/modals.dart'; import '../../common/view/mpv_metadata_dialog.dart'; import '../../common/view/safe_network_image.dart'; import '../../constants.dart'; @@ -39,12 +41,17 @@ class RadioHistoryTileImage extends StatelessWidget with WatchItMixin { onTap: () { final metadata = di().getMetadata(icyTitle); if (metadata == null) return; - showDialog( + showModal( context: context, - builder: (context) => MpvMetadataDialog( - image: imageUrl, - mpvMetaData: metadata, - ), + content: isMobile + ? MpvMetadataDialog.bottomSheet( + image: imageUrl, + mpvMetaData: metadata, + ) + : MpvMetadataDialog.dialog( + image: imageUrl, + mpvMetaData: metadata, + ), ); }, child: SizedBox( diff --git a/lib/radio/view/radio_page.dart b/lib/radio/view/radio_page.dart index febef50e6..df44d1d39 100644 --- a/lib/radio/view/radio_page.dart +++ b/lib/radio/view/radio_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/theme.dart'; +import 'package:yaru/yaru.dart'; import '../../app/connectivity_model.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/header_bar.dart'; import '../../common/view/offline_page.dart'; @@ -13,6 +14,7 @@ import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; import '../../search/search_model.dart'; import '../../search/search_type.dart'; +import '../../settings/view/settings_action.dart'; import 'radio_lib_page.dart'; class RadioPage extends StatelessWidget with WatchItMixin { @@ -23,11 +25,11 @@ class RadioPage extends StatelessWidget with WatchItMixin { final isOnline = watchPropertyValue((ConnectivityModel m) => m.isOnline); if (!isOnline) return const OfflinePage(); - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ + if (isMobile) const SettingsButton.icon(), Flexible( child: Padding( padding: appBarSingleActionSpacing, diff --git a/lib/radio/view/station_page.dart b/lib/radio/view/station_page.dart index f14b0f25e..18c2cc4c0 100644 --- a/lib/radio/view/station_page.dart +++ b/lib/radio/view/station_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import 'package:yaru/yaru.dart'; import '../../app/connectivity_model.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; import '../../common/view/audio_page_header.dart'; @@ -37,8 +37,7 @@ class StationPage extends StatelessWidget with WatchItMixin { final isOnline = watchPropertyValue((ConnectivityModel m) => m.isOnline); if (!isOnline) return const OfflinePage(); - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, actions: [ diff --git a/lib/search/view/search_page.dart b/lib/search/view/search_page.dart index 1701f1675..303d605af 100644 --- a/lib/search/view/search_page.dart +++ b/lib/search/view/search_page.dart @@ -1,8 +1,11 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/theme.dart'; +import '../../app/view/music_pod_scaffold.dart'; import '../../app_config.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; @@ -28,8 +31,7 @@ class SearchPage extends StatelessWidget with WatchItMixin { final audioType = watchPropertyValue((SearchModel m) => m.audioType); final loading = watchPropertyValue((SearchModel m) => m.loading); - return Scaffold( - resizeToAvoidBottomInset: isMobile ? false : null, + return MusicPodScaffold( appBar: HeaderBar( adaptive: true, title: Padding( @@ -38,7 +40,9 @@ class SearchPage extends StatelessWidget with WatchItMixin { ), actions: [ Padding( - padding: appBarSingleActionSpacing, + padding: appBarSingleActionSpacing.copyWith( + left: Platform.isMacOS ? 5 : 20, + ), child: SearchButton( active: true, onPressed: () => di().pop(), diff --git a/lib/settings/view/settings_action.dart b/lib/settings/view/settings_action.dart new file mode 100644 index 000000000..fd3be3a3b --- /dev/null +++ b/lib/settings/view/settings_action.dart @@ -0,0 +1,48 @@ +import '../../common/view/common_widgets.dart'; +import '../../common/view/icons.dart'; +import 'package:flutter/material.dart'; +import '../../l10n/l10n.dart'; +import 'settings_dialog.dart'; +import 'settings_tile.dart'; + +enum _SettingsButtonMode { + icon, + important, + tile; +} + +class SettingsButton extends StatelessWidget { + const SettingsButton.icon({ + super.key, + }) : _mode = _SettingsButtonMode.icon; + + const SettingsButton.important({ + super.key, + }) : _mode = _SettingsButtonMode.important; + + const SettingsButton.tile({ + super.key, + }) : _mode = _SettingsButtonMode.tile; + + final _SettingsButtonMode _mode; + + @override + Widget build(BuildContext context) { + void onPressed() => showDialog( + context: context, + builder: (context) => const SettingsDialog(), + ); + + return switch (_mode) { + _SettingsButtonMode.icon => IconButton( + onPressed: onPressed, + icon: Icon(Iconz.settings), + ), + _SettingsButtonMode.important => ImportantButton( + onPressed: onPressed, + child: Text(context.l10n.settings), + ), + _SettingsButtonMode.tile => const SettingsTile(), + }; + } +} diff --git a/needs_translation.json b/needs_translation.json index ee2d46d3c..8190e2f19 100644 --- a/needs_translation.json +++ b/needs_translation.json @@ -898,8 +898,6 @@ ], "it": [ - "showPositionDurationTitle", - "showPositionDurationDescription", "exposeToLastfmTitle", "exposeToLastfmSubTitle", "lastfmApiKey", @@ -3031,24 +3029,6 @@ "regionZimbabwe" ], - "sk": [ - "saveAndAuthorize", - "downloadsDirectory", - "downloadsDirectoryDescription", - "downloadsChangeWarning", - "disconnectedFrom", - "useMoreAnimationsTitle", - "useMoreAnimationsDescription", - "showPositionDurationTitle", - "showPositionDurationDescription", - "exposeToLastfmTitle", - "exposeToLastfmSubTitle", - "lastfmApiKey", - "lastfmSecret", - "lastfmApiKeyEmpty", - "lastfmSecretEmpty" - ], - "sv": [ "shuffle", "repeat", @@ -3066,7 +3046,6 @@ "removeFromFavorites", "share", "local", - "saveAndAuthorize", "genre", "year", "albumArtist", @@ -3080,15 +3059,7 @@ "noStarredStations", "stations", "copyToClipBoard", - "insertedIntoQueue", - "showPositionDurationTitle", - "showPositionDurationDescription", - "exposeToLastfmTitle", - "exposeToLastfmSubTitle", - "lastfmApiKey", - "lastfmSecret", - "lastfmApiKeyEmpty", - "lastfmSecretEmpty" + "insertedIntoQueue" ], "tr": [