From a788794c2ca7877cc2152accee6b9dcf4bfccbff Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Wed, 27 Nov 2024 00:14:58 +0100 Subject: [PATCH] feat: use settings as a dedicated page for mobile (#1057) --- lib/app/view/app.dart | 2 +- lib/app/view/master_items.dart | 10 + lib/app/view/mobile_navigation_bar.dart | 10 +- lib/common/view/icons.dart | 8 +- lib/library/library_service.dart | 1 + lib/local_audio/view/local_audio_page.dart | 4 +- lib/podcasts/view/podcasts_page.dart | 3 - lib/radio/view/radio_page.dart | 3 - lib/settings/view/about_page.dart | 292 +++++++++++---------- lib/settings/view/about_section.dart | 17 +- lib/settings/view/licenses_page.dart | 4 + lib/settings/view/settings_action.dart | 17 +- lib/settings/view/settings_page.dart | 31 ++- 13 files changed, 228 insertions(+), 174 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index f05eda01b..65539f196 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -161,7 +161,7 @@ class _MobileMusicPodAppState extends State<_MobileMusicPodApp> { navigatorKey: libraryModel.masterNavigatorKey, navigatorObservers: [libraryModel], initialRoute: - isMobile ? (libraryModel.selectedPageId ?? kSearchPageId) : null, + isMobile ? (libraryModel.selectedPageId ?? kLocalAudioPageId) : null, onGenerateRoute: (settings) { final page = (masterItems.firstWhereOrNull( (e) => e.pageId == settings.name, diff --git a/lib/app/view/master_items.dart b/lib/app/view/master_items.dart index d4d67d5c3..900cf691f 100644 --- a/lib/app/view/master_items.dart +++ b/lib/app/view/master_items.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; import '../../common/data/audio_type.dart'; import '../../common/view/icons.dart'; @@ -20,6 +21,7 @@ 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_page.dart'; import 'main_page_icon.dart'; class MasterItem { @@ -73,6 +75,14 @@ List createMasterItems({required LibraryModel libraryModel}) { ), pageId: kPodcastsPageId, ), + if (isMobile) + MasterItem( + titleBuilder: (context) => Text(context.l10n.settings), + iconBuilder: (selected) => + Icon(selected ? Iconz.settingsFilled : Iconz.settings), + pageBuilder: (context) => const SettingsPage(), + pageId: kSettingsPageId, + ), MasterItem( iconBuilder: (selected) => Icon(Iconz.plus), titleBuilder: (context) => Text(context.l10n.add), diff --git a/lib/app/view/mobile_navigation_bar.dart b/lib/app/view/mobile_navigation_bar.dart index 5d13eeb4b..9e742d008 100644 --- a/lib/app/view/mobile_navigation_bar.dart +++ b/lib/app/view/mobile_navigation_bar.dart @@ -50,11 +50,6 @@ class MobileNavigationBar extends StatelessWidget with WatchItMixin { 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), @@ -73,6 +68,11 @@ class MobileNavigationBar extends StatelessWidget with WatchItMixin { icon: const MainPageIcon(selected: false, audioType: AudioType.podcast), label: l10n.podcasts, ), + kSettingsPageId: NavigationDestination( + selectedIcon: Icon(Iconz.settingsFilled), + icon: Icon(Iconz.settings), + label: l10n.settings, + ), }; return NavigationBar( diff --git a/lib/common/view/icons.dart b/lib/common/view/icons.dart index d13c678cf..b627e21d0 100644 --- a/lib/common/view/icons.dart +++ b/lib/common/view/icons.dart @@ -186,7 +186,13 @@ class Iconz { ? YaruIcons.settings : appleStyled ? CupertinoIcons.settings - : Icons.settings_rounded; + : Icons.settings_outlined; + + static IconData get settingsFilled => yaruStyled + ? YaruIcons.settings_filled + : appleStyled + ? CupertinoIcons.settings + : Icons.settings; static IconData get addToLibrary => yaruStyled ? YaruIcons.bell diff --git a/lib/library/library_service.dart b/lib/library/library_service.dart index cdfd5f95a..a43927ed4 100644 --- a/lib/library/library_service.dart +++ b/lib/library/library_service.dart @@ -543,5 +543,6 @@ class LibraryService { kLocalAudioPageId, kPodcastsPageId, kRadioPageId, + kSettingsPageId, ]; } diff --git a/lib/local_audio/view/local_audio_page.dart b/lib/local_audio/view/local_audio_page.dart index 32718b789..700f3c15b 100644 --- a/lib/local_audio/view/local_audio_page.dart +++ b/lib/local_audio/view/local_audio_page.dart @@ -1,7 +1,6 @@ import 'package:animated_emoji/animated_emoji.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_type.dart'; @@ -18,10 +17,10 @@ import '../../search/search_model.dart'; import '../../search/search_type.dart'; import '../../settings/view/settings_action.dart'; import '../local_audio_model.dart'; +import '../local_audio_view.dart'; import 'failed_imports_content.dart'; import 'local_audio_body.dart'; import 'local_audio_control_panel.dart'; -import '../local_audio_view.dart'; class LocalAudioPage extends StatefulWidget with WatchItStatefulWidgetMixin { const LocalAudioPage({super.key}); @@ -64,7 +63,6 @@ class _LocalAudioPageState extends State { adaptive: true, titleSpacing: 0, actions: [ - if (isMobile) const SettingsButton.icon(), Padding( padding: appBarSingleActionSpacing, child: SearchButton( diff --git a/lib/podcasts/view/podcasts_page.dart b/lib/podcasts/view/podcasts_page.dart index adce6a189..e70933f18 100644 --- a/lib/podcasts/view/podcasts_page.dart +++ b/lib/podcasts/view/podcasts_page.dart @@ -1,6 +1,5 @@ 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_type.dart'; @@ -12,7 +11,6 @@ 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'; @@ -41,7 +39,6 @@ class _PodcastsPageState extends State { titleSpacing: 0, adaptive: true, actions: [ - if (isMobile) const SettingsButton.icon(), Padding( padding: appBarSingleActionSpacing, child: SearchButton( diff --git a/lib/radio/view/radio_page.dart b/lib/radio/view/radio_page.dart index 0cdaed116..fd7bd9203 100644 --- a/lib/radio/view/radio_page.dart +++ b/lib/radio/view/radio_page.dart @@ -1,6 +1,5 @@ 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'; @@ -14,7 +13,6 @@ 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 { @@ -29,7 +27,6 @@ class RadioPage extends StatelessWidget with WatchItMixin { appBar: HeaderBar( adaptive: true, actions: [ - if (isMobile) const SettingsButton.icon(), Flexible( child: Padding( padding: appBarSingleActionSpacing, diff --git a/lib/settings/view/about_page.dart b/lib/settings/view/about_page.dart index 96d618057..75d2128bd 100644 --- a/lib/settings/view/about_page.dart +++ b/lib/settings/view/about_page.dart @@ -5,6 +5,8 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; import '../../app/app_model.dart'; +import '../../app/view/music_pod_scaffold.dart'; +import '../../common/view/header_bar.dart'; import '../../common/view/progress.dart'; import '../../common/view/safe_network_image.dart'; import '../../common/view/tapable_text.dart'; @@ -40,6 +42,153 @@ class _AboutPageState extends State { ?.copyWith(color: Colors.lightBlue, overflow: TextOverflow.visible); const maxLines = 3; + final body = Padding( + padding: const EdgeInsets.all(kLargestSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TapAbleText( + text: + 'MusicPod is made by Frederik Feichtmeier. If you like MusicPod, please sponsor me!', + onTap: () => launchUrl(Uri.parse(kSponsorLink)), + style: linkStyle, + maxLines: maxLines, + ), + const SizedBox( + height: kLargestSpace, + ), + TapAbleText( + onTap: () => launchUrl(Uri.parse('https://ko-fi.com/amugofjava')), + text: + 'MusicPod uses Podcast Search to find podcasts which is made by Ben Hills, please sponsor him!', + style: linkStyle, + maxLines: maxLines, + ), + const SizedBox( + height: kLargestSpace, + ), + TapAbleText( + onTap: () => launchUrl( + Uri.parse('https://github.com/sponsors/alexmercerind'), + ), + text: + 'MusicPod uses MediaKit to play Media which is made by Hitesh Kumar Saini, please sponsor him!', + style: linkStyle, + maxLines: maxLines, + ), + const SizedBox( + height: kLargestSpace, + ), + TapAbleText( + onTap: () => launchUrl(Uri.parse('https://github.com/kenvandine')), + text: + 'MusicPod Snap packaging is made by Ken VanDine, please sponsor him!', + style: linkStyle, + maxLines: maxLines, + ), + const SizedBox( + height: kLargestSpace, + ), + TapAbleText( + onTap: () => launchUrl( + Uri.parse( + 'https://github.com/sponsors/ClementBeal', + ), + ), + text: + 'MusicPod metadata reading is enabled by Clement Beal, please sponsor him!', + style: linkStyle, + maxLines: maxLines, + ), + const SizedBox( + height: 2 * kLargestSpace, + ), + Text( + context.l10n.contributors, + style: theme.textTheme.bodyLarge, + ), + Expanded( + child: FutureBuilder>( + future: _contributors, + builder: (context, snapshot) { + if (snapshot.hasData) { + return SizedBox( + height: 300, + child: GridView.builder( + padding: const EdgeInsets.only( + bottom: kLargestSpace, + top: 10, + ), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: _kTileSize, + mainAxisSpacing: 5, + crossAxisSpacing: 5, + ), + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final e = snapshot.data!.elementAt(index); + return Tooltip( + message: e.login, + child: YaruBanner( + padding: EdgeInsets.zero, + onTap: e.htmlUrl == null + ? null + : () => launchUrl(Uri.parse(e.htmlUrl!)), + child: SizedBox.square( + dimension: _kTileSize, + child: ClipRRect( + borderRadius: const BorderRadius.all(radius), + child: SafeNetworkImage( + fit: BoxFit.cover, + url: e.avatarUrl, + fallBackIcon: const YaruPlaceholderIcon( + borderRadius: + BorderRadiusDirectional.vertical( + top: radius, + bottom: radius, + ), + size: Size.square(_kTileSize), + ), + ), + ), + ), + ), + ); + }, + ), + ); + } else { + return const Center( + child: Progress(), + ); + } + }, + ), + ), + TapAbleText( + style: linkStyle, + onTap: () => launchUrl(Uri.parse(kRepoUrl)), + text: + 'Copyright by Frederik Feichtmeier 2023 and onwards - all rights reserved.', + maxLines: maxLines, + ), + ], + ), + ); + + final title = Text('${context.l10n.about} $kAppTitle'); + + if (isMobile) { + return MusicPodScaffold( + appBar: HeaderBar( + adaptive: false, + title: title, + ), + body: body, + ); + } + return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, @@ -51,153 +200,14 @@ class _AboutPageState extends State { YaruDialogTitleBar( border: BorderSide.none, backgroundColor: context.theme.dialogBackgroundColor, - title: Text('${context.l10n.about} $kAppTitle'), + title: title, leading: YaruBackButton( style: YaruBackButtonStyle.rounded, onPressed: () => Navigator.of(context).pop(), ), ), Expanded( - child: Padding( - padding: const EdgeInsets.all(kLargestSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TapAbleText( - text: - 'MusicPod is made by Frederik Feichtmeier. If you like MusicPod, please sponsor me!', - onTap: () => launchUrl(Uri.parse(kSponsorLink)), - style: linkStyle, - maxLines: maxLines, - ), - const SizedBox( - height: kLargestSpace, - ), - TapAbleText( - onTap: () => - launchUrl(Uri.parse('https://ko-fi.com/amugofjava')), - text: - 'MusicPod uses Podcast Search to find podcasts which is made by Ben Hills, please sponsor him!', - style: linkStyle, - maxLines: maxLines, - ), - const SizedBox( - height: kLargestSpace, - ), - TapAbleText( - onTap: () => launchUrl( - Uri.parse('https://github.com/sponsors/alexmercerind'), - ), - text: - 'MusicPod uses MediaKit to play Media which is made by Hitesh Kumar Saini, please sponsor him!', - style: linkStyle, - maxLines: maxLines, - ), - const SizedBox( - height: kLargestSpace, - ), - TapAbleText( - onTap: () => - launchUrl(Uri.parse('https://github.com/kenvandine')), - text: - 'MusicPod Snap packaging is made by Ken VanDine, please sponsor him!', - style: linkStyle, - maxLines: maxLines, - ), - const SizedBox( - height: kLargestSpace, - ), - TapAbleText( - onTap: () => launchUrl( - Uri.parse( - 'https://github.com/sponsors/ClementBeal', - ), - ), - text: - 'MusicPod metadata reading is enabled by Clement Beal, please sponsor him!', - style: linkStyle, - maxLines: maxLines, - ), - const SizedBox( - height: 2 * kLargestSpace, - ), - Text( - context.l10n.contributors, - style: theme.textTheme.bodyLarge, - ), - Expanded( - child: FutureBuilder>( - future: _contributors, - builder: (context, snapshot) { - if (snapshot.hasData) { - return SizedBox( - height: 300, - child: GridView.builder( - padding: const EdgeInsets.only( - bottom: kLargestSpace, - top: 10, - ), - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: _kTileSize, - mainAxisSpacing: 5, - crossAxisSpacing: 5, - ), - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - final e = snapshot.data!.elementAt(index); - return Tooltip( - message: e.login, - child: YaruBanner( - padding: EdgeInsets.zero, - onTap: e.htmlUrl == null - ? null - : () => - launchUrl(Uri.parse(e.htmlUrl!)), - child: SizedBox.square( - dimension: _kTileSize, - child: ClipRRect( - borderRadius: - const BorderRadius.all(radius), - child: SafeNetworkImage( - fit: BoxFit.cover, - url: e.avatarUrl, - fallBackIcon: - const YaruPlaceholderIcon( - borderRadius: - BorderRadiusDirectional - .vertical( - top: radius, - bottom: radius, - ), - size: Size.square(_kTileSize), - ), - ), - ), - ), - ), - ); - }, - ), - ); - } else { - return const Center( - child: Progress(), - ); - } - }, - ), - ), - TapAbleText( - style: linkStyle, - onTap: () => launchUrl(Uri.parse(kRepoUrl)), - text: - 'Copyright by Frederik Feichtmeier 2023 and onwards - all rights reserved.', - maxLines: maxLines, - ), - ], - ), - ), + child: body, ), ], ), diff --git a/lib/settings/view/about_section.dart b/lib/settings/view/about_section.dart index 6c161453c..9be5156c1 100644 --- a/lib/settings/view/about_section.dart +++ b/lib/settings/view/about_section.dart @@ -16,6 +16,8 @@ import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../extensions/theme_data_x.dart'; import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; +import 'about_page.dart'; class AboutSection extends StatelessWidget with WatchItMixin { const AboutSection({super.key}); @@ -98,7 +100,12 @@ class _AboutTileState extends State<_AboutTile> { ), ), trailing: OutlinedButton( - onPressed: () => settingsNavigatorKey.currentState?.pushNamed('/about'), + onPressed: () => isMobile + ? di().push( + pageId: 'about', + builder: (p0) => const AboutPage(), + ) + : settingsNavigatorKey.currentState?.pushNamed('/about'), child: Text(context.l10n.contributors), ), ); @@ -115,8 +122,12 @@ class _LicenseTile extends StatelessWidget { text: '${context.l10n.license}: GPL3', ), trailing: OutlinedButton( - onPressed: () => - settingsNavigatorKey.currentState?.pushNamed('/licenses'), + onPressed: () => isMobile + ? di().push( + pageId: 'licenses', + builder: (p0) => const LicensePage(), + ) + : settingsNavigatorKey.currentState?.pushNamed('/licenses'), child: Text(context.l10n.dependencies), ), enabled: true, diff --git a/lib/settings/view/licenses_page.dart b/lib/settings/view/licenses_page.dart index 0633f33a4..2f5b2fe41 100644 --- a/lib/settings/view/licenses_page.dart +++ b/lib/settings/view/licenses_page.dart @@ -6,6 +6,10 @@ class LicensesPage extends StatelessWidget { @override Widget build(BuildContext context) { + if (isMobile) { + return const LicensePage(); + } + return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, diff --git a/lib/settings/view/settings_action.dart b/lib/settings/view/settings_action.dart index fd3be3a3b..b33ce8b33 100644 --- a/lib/settings/view/settings_action.dart +++ b/lib/settings/view/settings_action.dart @@ -1,7 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:watch_it/watch_it.dart'; +import 'package:yaru/yaru.dart'; + import '../../common/view/common_widgets.dart'; import '../../common/view/icons.dart'; -import 'package:flutter/material.dart'; +import '../../constants.dart'; import '../../l10n/l10n.dart'; +import '../../library/library_model.dart'; import 'settings_dialog.dart'; import 'settings_tile.dart'; @@ -28,10 +33,12 @@ class SettingsButton extends StatelessWidget { @override Widget build(BuildContext context) { - void onPressed() => showDialog( - context: context, - builder: (context) => const SettingsDialog(), - ); + void onPressed() => isMobile + ? di().push(pageId: kSettingsPageId) + : showDialog( + context: context, + builder: (context) => const SettingsDialog(), + ); return switch (_mode) { _SettingsButtonMode.icon => IconButton( diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index 8d0ce9227..b7941d837 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; +import '../../app/view/music_pod_scaffold.dart'; +import '../../common/view/header_bar.dart'; import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import 'about_section.dart'; @@ -14,6 +16,25 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { + final listView = ListView( + children: const [ + ThemeSection(), + PodcastSection(), + LocalAudioSection(), + ExposeOnlineSection(), + AboutSection(), + ], + ); + if (isMobile) { + return MusicPodScaffold( + appBar: HeaderBar( + adaptive: false, + title: Text(context.l10n.settings), + ), + body: listView, + ); + } + return Column( children: [ YaruDialogTitleBar( @@ -23,15 +44,7 @@ class SettingsPage extends StatelessWidget { title: Text(context.l10n.settings), ), Expanded( - child: ListView( - children: const [ - ThemeSection(), - PodcastSection(), - LocalAudioSection(), - ExposeOnlineSection(), - AboutSection(), - ], - ), + child: listView, ), ], );