From 615d5ce901eb0512e84a120b7309c9053238ee36 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 7 Aug 2022 22:16:27 +0600 Subject: [PATCH] feat(ui): adaptive TrackTile actions & Setting ListTile --- lib/components/Settings/About.dart | 1 + lib/components/Settings/Settings.dart | 185 ++++++++---------- lib/components/Shared/AdaptiveListTile.dart | 58 ++++++ .../Shared/AdaptivePopupMenuButton.dart | 103 ++++++++++ lib/components/Shared/TrackTile.dart | 100 ++++------ lib/components/Shared/TracksTableView.dart | 4 +- lib/themes/dark-theme.dart | 1 + pubspec.lock | 7 + pubspec.yaml | 3 +- 9 files changed, 285 insertions(+), 177 deletions(-) create mode 100644 lib/components/Shared/AdaptiveListTile.dart create mode 100644 lib/components/Shared/AdaptivePopupMenuButton.dart diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart index fafb3362b..6761fe3a0 100644 --- a/lib/components/Settings/About.dart +++ b/lib/components/Settings/About.dart @@ -29,6 +29,7 @@ class About extends HookWidget { version: "2.3.0"); return ListTile( + leading: Icon(Icons.info_outline_rounded), title: const Text("About Spotube"), onTap: () { showAboutDialog( diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 8798c6d82..7355a378b 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -7,14 +5,15 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Settings/About.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; +import 'package:spotube/components/Shared/AdaptiveListTile.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/SpotifyMarkets.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:collection/collection.dart'; class Settings extends HookConsumerWidget { const Settings({Key? key}) : super(key: key); @@ -63,9 +62,9 @@ class Settings extends HookConsumerWidget { constraints: const BoxConstraints(maxWidth: 1366), child: ListView( children: [ - ListTile( + AdaptiveListTile( + leading: const Icon(Icons.dark_mode_outlined), title: const Text("Theme"), - horizontalTitleGap: 10, trailing: DropdownButton( value: preferences.themeMode, items: const [ @@ -94,8 +93,8 @@ class Settings extends HookConsumerWidget { ), ), ListTile( + leading: const Icon(Icons.palette_outlined), title: const Text("Accent Color Scheme"), - horizontalTitleGap: 10, contentPadding: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, @@ -108,8 +107,8 @@ class Settings extends HookConsumerWidget { onTap: pickColorScheme(ColorSchemeType.accent), ), ListTile( + leading: const Icon(Icons.format_color_fill_rounded), title: const Text("Background Color Scheme"), - horizontalTitleGap: 10, contentPadding: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, @@ -121,106 +120,79 @@ class Settings extends HookConsumerWidget { ), onTap: pickColorScheme(ColorSchemeType.background), ), - Padding( - padding: const EdgeInsets.all(15), - child: Wrap( - alignment: WrapAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Market Place", - style: Theme.of(context).textTheme.bodyText1, - ), - Text( - "Recommendation Country", - style: Theme.of(context).textTheme.caption, - ), - ], - ), - const SizedBox(height: 10), - DropdownButton( - value: preferences.recommendationMarket, - items: spotifyMarkets - .map( - (country) => (DropdownMenuItem( - child: Text(country.last), - value: country.first, - )), - ) - .toList(), - onChanged: (value) { - if (value == null) return; - preferences.setRecommendationMarket( - value as String, - ); - }, - ), - ], + AdaptiveListTile( + leading: const Icon(Icons.shopping_bag_rounded), + title: Text( + "Market Place", + style: Theme.of(context).textTheme.bodyText1, + ), + subtitle: Text( + "Recommendation Country", + style: Theme.of(context).textTheme.caption, + ), + trailing: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 250), + child: DropdownButton( + isExpanded: true, + value: preferences.recommendationMarket, + items: spotifyMarkets + .map( + (country) => (DropdownMenuItem( + child: Text(country.last), + value: country.first, + )), + ) + .toList(), + onChanged: (value) { + if (value == null) return; + preferences.setRecommendationMarket( + value as String, + ); + }, + ), ), ), ListTile( + leading: const Icon(Icons.file_download_outlined), title: const Text("Download Location"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - preferences.downloadLocation, - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(width: 5), - ElevatedButton( - child: const Icon(Icons.folder_rounded), - onPressed: pickDownloadLocation, - ), - ], + subtitle: Text(preferences.downloadLocation), + trailing: ElevatedButton( + child: const Icon(Icons.folder_rounded), + onPressed: pickDownloadLocation, ), onTap: pickDownloadLocation, ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15.0, - vertical: 5, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 2, - child: Text( - "Format of the YouTube Search term (Case sensitive)", - style: Theme.of(context).textTheme.bodyText2, - ), - ), - Expanded( - flex: 1, - child: TextField( - controller: ytSearchFormatController, - decoration: InputDecoration( - isDense: true, - suffix: ElevatedButton( - child: const Icon(Icons.save_rounded), - onPressed: () { - preferences.setYtSearchFormat( - ytSearchFormatController.value.text, - ); - }, - ), - ), - onSubmitted: (value) { - preferences.setYtSearchFormat(value); + AdaptiveListTile( + leading: const Icon(Icons.screen_search_desktop_rounded), + title: const Text("Format of the YouTube Search term"), + subtitle: const Text("(Case sensitive)"), + breakOn: Breakpoints.lg, + trailing: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 450), + child: TextField( + controller: ytSearchFormatController, + decoration: InputDecoration( + isDense: true, + suffix: ElevatedButton( + child: const Icon(Icons.save_rounded), + onPressed: () { + preferences.setYtSearchFormat( + ytSearchFormatController.value.text, + ); }, ), ), - ], + onSubmitted: (value) { + preferences.setYtSearchFormat(value); + }, + ), ), ), ListTile( + leading: const Icon(Icons.fast_forward_rounded), title: const Text( "Skip non-music segments (SponsorBlock)", ), - horizontalTitleGap: 10, trailing: Switch.adaptive( activeColor: Theme.of(context).primaryColor, value: preferences.skipSponsorSegments, @@ -230,8 +202,8 @@ class Settings extends HookConsumerWidget { ), ), ListTile( + leading: const Icon(Icons.lyrics_rounded), title: const Text("Download lyrics along with the Track"), - horizontalTitleGap: 10, trailing: Switch.adaptive( activeColor: Theme.of(context).primaryColor, value: preferences.saveTrackLyrics, @@ -242,6 +214,7 @@ class Settings extends HookConsumerWidget { ), if (auth.isAnonymous) ListTile( + leading: const Icon(Icons.login_rounded), title: const Text("Login with your Spotify account"), horizontalTitleGap: 10, trailing: ElevatedButton( @@ -259,8 +232,8 @@ class Settings extends HookConsumerWidget { ), ), ListTile( + leading: const Icon(Icons.update_rounded), title: const Text("Check for Update"), - horizontalTitleGap: 10, trailing: Switch.adaptive( activeColor: Theme.of(context).primaryColor, value: preferences.checkUpdate, @@ -268,9 +241,9 @@ class Settings extends HookConsumerWidget { preferences.setCheckUpdate(checked), ), ), - ListTile( + AdaptiveListTile( + leading: const Icon(Icons.low_priority_rounded), title: const Text("Track Match Algorithm"), - horizontalTitleGap: 10, trailing: DropdownButton( value: preferences.trackMatchAlgorithm, items: const [ @@ -298,9 +271,9 @@ class Settings extends HookConsumerWidget { }, ), ), - ListTile( + AdaptiveListTile( + leading: const Icon(Icons.multitrack_audio_rounded), title: const Text("Audio Quality"), - horizontalTitleGap: 10, trailing: DropdownButton( value: preferences.audioQuality, items: const [ @@ -326,8 +299,8 @@ class Settings extends HookConsumerWidget { Builder(builder: (context) { Auth auth = ref.watch(authProvider); return ListTile( + leading: const Icon(Icons.logout_rounded), title: const Text("Log out of this account"), - horizontalTitleGap: 10, trailing: ElevatedButton( child: const Text("Logout"), style: ButtonStyle( @@ -343,7 +316,11 @@ class Settings extends HookConsumerWidget { ), ); }), - ListTile( + AdaptiveListTile( + leading: const Icon( + Icons.favorite_border_rounded, + color: Colors.pink, + ), title: const Text( "We know you Love Spotube", style: TextStyle( @@ -351,7 +328,6 @@ class Settings extends HookConsumerWidget { fontWeight: FontWeight.bold, ), ), - horizontalTitleGap: 10, trailing: ElevatedButton.icon( icon: const Icon(Icons.favorite_outline_rounded), label: const Text("Please Sponsor/Donate"), @@ -367,14 +343,7 @@ class Settings extends HookConsumerWidget { ), ), const About() - ].mapIndexed((i, child) { - return Container( - color: i % 2 == 1 - ? Theme.of(context).primaryColor.withOpacity(.1) - : null, - child: child, - ); - }).toList(), + ], ), ), ), diff --git a/lib/components/Shared/AdaptiveListTile.dart b/lib/components/Shared/AdaptiveListTile.dart new file mode 100644 index 000000000..7a1cad722 --- /dev/null +++ b/lib/components/Shared/AdaptiveListTile.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; + +class AdaptiveListTile extends HookWidget { + final Widget? trailing; + final Widget? title; + final Widget? subtitle; + final Widget? leading; + final void Function()? onTap; + final Breakpoints breakOn; + + const AdaptiveListTile({ + super.key, + this.trailing, + this.onTap, + this.title, + this.subtitle, + this.leading, + this.breakOn = Breakpoints.md, + }); + + @override + Widget build(BuildContext context) { + final breakpoint = useBreakpoints(); + + return ListTile( + title: title, + subtitle: subtitle, + trailing: breakpoint.isLessThan(breakOn) ? null : trailing, + leading: leading, + onTap: breakpoint.isLessThan(breakOn) + ? () { + onTap?.call(); + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: title != null + ? Row( + children: [ + if (leading != null) ...[ + leading!, + const SizedBox(width: 5) + ], + Flexible(child: title!), + ], + ) + : null, + content: trailing, + ); + }, + ); + } + : null, + ); + } +} diff --git a/lib/components/Shared/AdaptivePopupMenuButton.dart b/lib/components/Shared/AdaptivePopupMenuButton.dart new file mode 100644 index 000000000..cb7029750 --- /dev/null +++ b/lib/components/Shared/AdaptivePopupMenuButton.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:popover/popover.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; + +class Action extends StatelessWidget { + final Widget text; + final Icon icon; + final void Function() onPressed; + final bool isExpanded; + const Action({ + Key? key, + required this.icon, + required this.text, + required this.onPressed, + this.isExpanded = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isExpanded != true) { + return Tooltip( + message: text.toStringShallow().split(",").last.replaceAll( + "\"", + "", + ), + child: IconButton( + icon: icon, + onPressed: onPressed, + ), + ); + } + return TextButton.icon( + style: TextButton.styleFrom( + primary: Theme.of(context).textTheme.bodyMedium?.color, + padding: const EdgeInsets.all(20), + ), + icon: icon, + label: Align( + alignment: Alignment.centerLeft, + child: text, + ), + onPressed: onPressed, + ); + } +} + +class AdaptiveActions extends HookWidget { + final List actions; + final Breakpoints breakOn; + const AdaptiveActions({ + required this.actions, + this.breakOn = Breakpoints.lg, + super.key, + }); + + @override + Widget build(BuildContext context) { + final breakpoint = useBreakpoints(); + + if (breakpoint.isLessThan(breakOn)) { + return IconButton( + icon: const Icon(Icons.more_horiz), + onPressed: () { + showPopover( + context: context, + direction: PopoverDirection.left, + bodyBuilder: (context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: actions + .map( + (action) => SizedBox( + width: 200, + child: Row( + children: [ + Expanded(child: action), + ], + ), + ), + ) + .toList(), + ); + }, + backgroundColor: Theme.of(context).dialogTheme.backgroundColor!, + ); + }, + ); + } + + return Row( + children: actions.map((action) { + return Action( + icon: action.icon, + onPressed: action.onPressed, + text: action.text, + isExpanded: false, + ); + }).toList(), + ); + } +} diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index 40a84f5db..114b16442 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -1,9 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Action; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart'; import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; @@ -257,73 +258,38 @@ class TrackTile extends HookConsumerWidget { Text(duration), ], const SizedBox(width: 10), - PopupMenuButton( - icon: const Icon(Icons.more_horiz_rounded), - itemBuilder: (context) { - return [ - if (auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.add_box_rounded), - SizedBox(width: 10), - Text("Add to Playlist"), - ], - ), - value: "add-playlist", - ), - if (userPlaylist && auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.remove_circle_outline_rounded), - SizedBox(width: 10), - Text("Remove from Playlist"), - ], - ), - value: "remove-playlist", - ), - if (auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: [ - Icon(isSaved - ? Icons.favorite_rounded - : Icons.favorite_border_rounded), - const SizedBox(width: 10), - const Text("Favorite") - ], - ), - value: "favorite", - ), - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.share_rounded), - SizedBox(width: 10), - Text("Share") - ], - ), - value: "share", - ) - ]; - }, - onSelected: (value) { - switch (value) { - case "favorite": - actionFavorite(isSaved); - break; - case "add-playlist": - actionAddToPlaylist(); - break; - case "remove-playlist": - actionRemoveFromPlaylist(); - break; - case "share": + AdaptiveActions( + actions: [ + if (auth.isLoggedIn) + Action( + icon: const Icon(Icons.add_box_rounded), + text: const Text("Add To playlist"), + onPressed: actionAddToPlaylist, + ), + if (userPlaylist && auth.isLoggedIn) + Action( + icon: const Icon(Icons.remove_circle_outline_rounded), + text: const Text("Remove from playlist"), + onPressed: actionRemoveFromPlaylist, + ), + if (auth.isLoggedIn) + Action( + icon: Icon(isSaved + ? Icons.favorite_rounded + : Icons.favorite_border_rounded), + text: const Text("Save as favorite"), + onPressed: () { + actionFavorite(isSaved); + }, + ), + Action( + icon: const Icon(Icons.share_rounded), + text: const Text("Share"), + onPressed: () { actionShare(track.value); - break; - } - }, + }, + ) + ], ), ], ), diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index fad86ea9d..406ee4fa9 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -75,7 +75,9 @@ class TracksTableView extends HookConsumerWidget { Text("Time", style: tableHeadStyle), const SizedBox(width: 10), ], - const SizedBox(width: 40), + SizedBox( + width: breakpoint.isLessThan(Breakpoints.lg) ? 40 : 110, + ), ], ), ...tracks.asMap().entries.map((track) { diff --git a/lib/themes/dark-theme.dart b/lib/themes/dark-theme.dart index 7533996e7..9bb7b286d 100644 --- a/lib/themes/dark-theme.dart +++ b/lib/themes/dark-theme.dart @@ -72,5 +72,6 @@ ThemeData darkTheme({ dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]), cardColor: backgroundMaterialColor[800], canvasColor: backgroundMaterialColor[900], + listTileTheme: const ListTileThemeData(horizontalTitleGap: 0), ); } diff --git a/pubspec.lock b/pubspec.lock index e4a61e1d1..c692d9869 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -989,6 +989,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + popover: + dependency: "direct main" + description: + name: popover + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+3" process: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d7cc5e77c..284b39bfe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 2.3.0+12 environment: - sdk: ">=2.15.1 <3.0.0" + sdk: ">=2.17.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -66,6 +66,7 @@ dependencies: introduction_screen: ^3.0.2 audio_session: ^0.1.9 file_picker: ^4.6.1 + popover: ^0.2.6+3 dev_dependencies: flutter_test: