feat(ui): adaptive TrackTile actions & Setting ListTile

This commit is contained in:
Kingkor Roy Tirtho 2022-08-07 22:16:27 +06:00
parent 816707c643
commit 615d5ce901
9 changed files with 285 additions and 177 deletions

View File

@ -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(

View File

@ -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<ThemeMode>(
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,26 +120,20 @@ 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(
AdaptiveListTile(
leading: const Icon(Icons.shopping_bag_rounded),
title: Text(
"Market Place",
style: Theme.of(context).textTheme.bodyText1,
),
Text(
subtitle: Text(
"Recommendation Country",
style: Theme.of(context).textTheme.caption,
),
],
),
const SizedBox(height: 10),
DropdownButton(
trailing: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: DropdownButton(
isExpanded: true,
value: preferences.recommendationMarket,
items: spotifyMarkets
.map(
@ -157,44 +150,25 @@ class Settings extends HookConsumerWidget {
);
},
),
],
),
),
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(
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,
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(
@ -213,14 +187,12 @@ class Settings extends HookConsumerWidget {
},
),
),
],
),
),
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<SpotubeTrackMatchAlgorithm>(
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<AudioQuality>(
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(),
],
),
),
),

View File

@ -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,
);
}
}

View File

@ -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<Action> 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(),
);
}
}

View File

@ -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,74 +258,39 @@ class TrackTile extends HookConsumerWidget {
Text(duration),
],
const SizedBox(width: 10),
PopupMenuButton(
icon: const Icon(Icons.more_horiz_rounded),
itemBuilder: (context) {
return [
AdaptiveActions(
actions: [
if (auth.isLoggedIn)
PopupMenuItem(
child: Row(
children: const [
Icon(Icons.add_box_rounded),
SizedBox(width: 10),
Text("Add to Playlist"),
],
),
value: "add-playlist",
Action(
icon: const Icon(Icons.add_box_rounded),
text: const Text("Add To playlist"),
onPressed: actionAddToPlaylist,
),
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",
Action(
icon: const Icon(Icons.remove_circle_outline_rounded),
text: const Text("Remove from playlist"),
onPressed: actionRemoveFromPlaylist,
),
if (auth.isLoggedIn)
PopupMenuItem(
child: Row(
children: [
Icon(isSaved
Action(
icon: 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":
text: const Text("Save as favorite"),
onPressed: () {
actionFavorite(isSaved);
break;
case "add-playlist":
actionAddToPlaylist();
break;
case "remove-playlist":
actionRemoveFromPlaylist();
break;
case "share":
actionShare(track.value);
break;
}
},
),
Action(
icon: const Icon(Icons.share_rounded),
text: const Text("Share"),
onPressed: () {
actionShare(track.value);
},
)
],
),
],
),
),

View File

@ -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) {

View File

@ -72,5 +72,6 @@ ThemeData darkTheme({
dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]),
cardColor: backgroundMaterialColor[800],
canvasColor: backgroundMaterialColor[900],
listTileTheme: const ListTileThemeData(horizontalTitleGap: 0),
);
}

View File

@ -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:

View File

@ -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: