feat: initial platform_ui integration

What's changed:
- Sidebar
- Settings
- UserLibrary (root)
- Search (search field)
This commit is contained in:
Kingkor Roy Tirtho 2022-10-29 14:23:17 +06:00
parent 4b21cc8299
commit 9eee573ce9
22 changed files with 452 additions and 351 deletions

View File

@ -84,17 +84,13 @@ class Shell extends HookConsumerWidget {
)
: null,
extendBodyBehindAppBar: true,
body: Row(
children: [
Sidebar(
selectedIndex: index.value,
onSelectedIndexChanged: (selectedIndex) {
index.value = selectedIndex;
GoRouter.of(context).go(_path[selectedIndex]!);
},
),
Expanded(child: child),
],
body: Sidebar(
selectedIndex: index.value,
onSelectedIndexChanged: (i) {
index.value = i;
GoRouter.of(context).go(_path[index.value]!);
},
child: child,
),
extendBody: true,
bottomNavigationBar: Column(

View File

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart';
@ -15,16 +16,19 @@ import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:fluent_ui/fluent_ui.dart' as FluentUI;
final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null);
class Sidebar extends HookConsumerWidget {
final int selectedIndex;
final void Function(int) onSelectedIndexChanged;
final Widget child;
const Sidebar({
required this.selectedIndex,
required this.onSelectedIndexChanged,
required this.child,
Key? key,
}) : super(key: key);
@ -45,7 +49,6 @@ class Sidebar extends HookConsumerWidget {
final breakpoints = useBreakpoints();
final extended = useState(false);
final auth = ref.watch(authProvider);
final downloadCount = ref.watch(
downloaderProvider.select((s) => s.currentlyRunning),
);
@ -81,10 +84,31 @@ class Sidebar extends HookConsumerWidget {
return SafeArea(
top: false,
child: Material(
color: Theme.of(context).navigationRailTheme.backgroundColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
child: PlatformSidebar(
currentIndex: selectedIndex,
onIndexChanged: onSelectedIndexChanged,
body: Map.fromEntries(
sidebarTileList.map(
(e) {
final icon = Icon(e.icon);
return MapEntry(
PlatformSidebarItem(
icon: icon,
title: Text(
e.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
child,
);
},
),
),
expanded: extended.value,
header: Column(
children: [
if (kIsDesktop)
SizedBox(
@ -126,138 +150,120 @@ class Sidebar extends HookConsumerWidget {
],
)
: _buildSmallLogo(),
Expanded(
child: NavigationRail(
destinations: sidebarTileList.map(
(e) {
final icon = Icon(e.icon);
return NavigationRailDestination(
icon: e.title == "Library" && downloadCount > 0
? Badge(
badgeColor: Colors.red[100]!,
badgeContent: Text(
downloadCount.toString(),
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
animationType: BadgeAnimationType.fade,
child: icon,
)
: icon,
label: Text(
e.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
);
},
).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onSelectedIndexChanged,
extended: extended.value,
),
),
SizedBox(
width: extended.value ? 256 : 80,
child: HookBuilder(
builder: (context) {
final me = useQuery(
job: currentUserQueryJob,
externalData: ref.watch(spotifyProvider),
);
final data = me.data;
final avatarImg = TypeConversionUtils.image_X_UrlString(
data?.images,
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
useEffect(() {
if (auth.isLoggedIn && !me.hasData) {
me.setExternalData(ref.read(spotifyProvider));
me.refetch();
}
return;
}, [auth.isLoggedIn, me.hasData]);
if (extended.value) {
return Padding(
padding: const EdgeInsets.all(16).copyWith(left: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth.isLoggedIn && data == null)
const Center(
child: CircularProgressIndicator(),
)
else if (data != null)
Flexible(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(
avatarImg),
onBackgroundImageError:
(exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
),
),
const SizedBox(
width: 10,
),
Flexible(
child: Text(
data.displayName ?? "Guest",
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => goToSettings(context)),
],
));
} else {
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () => goToSettings(context),
child: CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
),
),
),
);
}
},
),
)
],
),
windowsFooterItems: [
FluentUI.PaneItemAction(
icon: const FluentUI.Icon(FluentUI.FluentIcons.settings),
onTap: () => goToSettings(context),
),
],
footer: SidebarFooter(extended: extended.value),
),
);
}
}
class SidebarFooter extends HookConsumerWidget {
final bool extended;
const SidebarFooter({
Key? key,
required this.extended,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
return SizedBox(
width: extended ? 256 : 80,
child: HookBuilder(
builder: (context) {
final me = useQuery(
job: currentUserQueryJob,
externalData: ref.watch(spotifyProvider),
);
final data = me.data;
final avatarImg = TypeConversionUtils.image_X_UrlString(
data?.images,
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
useEffect(() {
if (auth.isLoggedIn && !me.hasData) {
me.setExternalData(ref.read(spotifyProvider));
me.refetch();
}
return;
}, [auth.isLoggedIn, me.hasData]);
if (extended) {
return Padding(
padding: const EdgeInsets.all(16).copyWith(left: 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth.isLoggedIn && data == null)
const Center(
child: CircularProgressIndicator(),
)
else if (data != null)
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
),
),
const SizedBox(
width: 10,
),
Flexible(
child: Text(
data.displayName ?? "Guest",
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => Sidebar.goToSettings(context)),
],
));
} else {
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () => Sidebar.goToSettings(context),
child: CircleAvatar(
backgroundImage: UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
width: 16,
),
),
),
);
}
},
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/provider/Downloader.dart';
@ -47,7 +48,7 @@ class UserDownloads extends HookConsumerWidget {
itemCount: downloader.inQueue.length,
itemBuilder: (context, index) {
final track = downloader.inQueue.elementAt(index);
return ListTile(
return PlatformListTile(
title: Text(track.name!),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
@ -68,7 +69,6 @@ class UserDownloads extends HookConsumerWidget {
height: 30,
child: CircularProgressIndicator.adaptive(),
),
horizontalTitleGap: 5,
subtitle: Text(
TypeConversionUtils.artists_X_String(
track.artists ?? <Artist>[],

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart' hide Image;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Library/UserAlbums.dart';
import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserDownloads.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Library/UserPlaylists.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/components/Shared/ColoredTabBar.dart';
class UserLibrary extends ConsumerWidget {
const UserLibrary({Key? key}) : super(key: key);
@ -15,27 +15,30 @@ class UserLibrary extends ConsumerWidget {
return DefaultTabController(
length: 5,
child: SafeArea(
child: Scaffold(
appBar: ColoredTabBar(
color: Theme.of(context).backgroundColor,
child: const TabBar(
isScrollable: true,
tabs: [
Tab(text: "Playlist"),
Tab(text: "Downloads"),
Tab(text: "Local"),
Tab(text: "Artists"),
Tab(text: "Album"),
],
),
),
body: const TabBarView(children: [
AnonymousFallback(child: UserPlaylists()),
UserDownloads(),
UserLocalTracks(),
AnonymousFallback(child: UserArtists()),
AnonymousFallback(child: UserAlbums()),
]),
child: PlatformTabView(
placement: PlatformProperty.all(PlatformTabbarPlacement.top),
body: {
PlatformTab(
label: "Playlist",
icon: Container(),
): const AnonymousFallback(child: UserPlaylists()),
PlatformTab(
label: "Downloads",
icon: Container(),
): const UserDownloads(),
PlatformTab(
label: "Local",
icon: Container(),
): const UserLocalTracks(),
PlatformTab(
label: "Artists",
icon: Container(),
): const AnonymousFallback(child: UserArtists()),
PlatformTab(
label: "Album",
icon: Container(),
): const AnonymousFallback(child: UserAlbums()),
},
),
),
);

View File

@ -9,6 +9,7 @@ import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
import 'package:spotube/components/Shared/SortTracksDropdown.dart';
@ -169,13 +170,7 @@ class UserLocalTracks extends HookConsumerWidget {
child: Row(
children: [
const SizedBox(width: 10),
ElevatedButton.icon(
label: const Text("Play"),
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
PlatformFilledButton(
onPressed: trackSnapshot.value != null
? () {
if (trackSnapshot.value?.isNotEmpty == true) {
@ -187,6 +182,16 @@ class UserLocalTracks extends HookConsumerWidget {
}
}
: null,
child: Row(
children: [
const Text("Play"),
Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
)
],
),
),
const Spacer(),
SortTracksDropdown(
@ -196,7 +201,7 @@ class UserLocalTracks extends HookConsumerWidget {
},
),
const SizedBox(width: 10),
ElevatedButton(
PlatformFilledButton(
child: const Icon(Icons.refresh_rounded),
onPressed: () {
ref.refresh(localTracksProvider);

View File

@ -11,10 +11,11 @@ class ShimmerArtistProfile extends HookWidget {
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
Colors.white;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
.extension<ShimmerColorTheme>()
?.shimmerBackgroundColor;
final avatarWidth = useBreakpointValue(
sm: MediaQuery.of(context).size.width * 0.80,

View File

@ -9,10 +9,12 @@ class ShimmerCategories extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
Colors.white;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
.extension<ShimmerColorTheme>()
?.shimmerBackgroundColor ??
Colors.grey;
return Padding(
padding: const EdgeInsets.all(8.0),

View File

@ -12,10 +12,12 @@ class ShimmerLyrics extends HookWidget {
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
Colors.white;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
.extension<ShimmerColorTheme>()
?.shimmerBackgroundColor ??
Colors.grey;
final breakpoint = useBreakpoints();

View File

@ -9,10 +9,12 @@ class ShimmerPlaybuttonCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
Colors.white;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
.extension<ShimmerColorTheme>()
?.shimmerBackgroundColor ??
Colors.grey;
final card = Stack(
children: [

View File

@ -13,10 +13,12 @@ class ShimmerTrackTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shimmerColor =
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
Colors.white;
final shimmerBackgroundColor = Theme.of(context)
.extension<ShimmerColorTheme>()!
.shimmerBackgroundColor!;
.extension<ShimmerColorTheme>()
?.shimmerBackgroundColor ??
Colors.grey;
final single = Container(
margin: const EdgeInsets.symmetric(horizontal: 20),

View File

@ -3,6 +3,8 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Lyrics/GeniusLyrics.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
@ -14,6 +16,25 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class Lyrics extends HookConsumerWidget {
const Lyrics({Key? key}) : super(key: key);
Widget buildContainer(Widget child, String albumArt, PaletteColor palette) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(albumArt),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: child,
),
),
);
}
@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
@ -33,8 +54,22 @@ class Lyrics extends HookConsumerWidget {
noSetBGColor: true,
);
return DefaultTabController(
length: 2,
return SafeArea(
child: PlatformTabView(
body: {
PlatformTab(
label: "Synced Lyrics",
icon: Container(),
): buildContainer(SyncedLyrics(palette: palette), albumArt, palette),
PlatformTab(
label: "Lyrics (genius.com)",
icon: Container(),
): buildContainer(GeniusLyrics(palette: palette), albumArt, palette),
},
),
);
return SafeArea(
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: const TabBar(

View File

@ -2,6 +2,7 @@ import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
@ -12,7 +13,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider);
return TextButton(
return PlatformTextButton(
onPressed: () {
showDialog(
context: context,
@ -26,11 +27,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
return AlertDialog(
title: const Text("Create a Playlist"),
actions: [
TextButton(
PlatformTextButton(
child: const Text("Cancel"),
onPressed: () => Navigator.of(context).pop(),
),
ElevatedButton(
PlatformFilledButton(
child: const Text("Create"),
onPressed: () async {
if (playlistName.text.isEmpty) return;
@ -58,19 +59,15 @@ class PlaylistCreateDialog extends HookConsumerWidget {
child: ListView(
shrinkWrap: true,
children: [
TextField(
PlatformTextField(
controller: playlistName,
decoration: const InputDecoration(
hintText: "Name of the playlist",
label: Text("Playlist Name"),
),
placeholder: "Name of the playlist",
label: "Playlist Name",
),
const SizedBox(height: 10),
TextField(
PlatformTextField(
controller: description,
decoration: const InputDecoration(
hintText: "Description...",
),
placeholder: "Description...",
keyboardType: TextInputType.multiline,
maxLines: 5,
),

View File

@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistCard.dart';
@ -85,23 +86,15 @@ class Search extends HookConsumerWidget {
vertical: 10,
),
color: Theme.of(context).backgroundColor,
child: TextField(
child: PlatformTextField(
onChanged: (value) {
ref.read(searchTermStateProvider.notifier).state = value;
},
decoration: InputDecoration(
isDense: true,
suffix: ElevatedButton(
onPressed: onSearch,
child: const Icon(Icons.search_rounded),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 7,
),
hintStyle: const TextStyle(height: 2),
hintText: "Search...",
suffix: PlatformFilledButton(
onPressed: onSearch,
child: const Icon(Icons.search_rounded),
),
placeholder: "Search...",
onSubmitted: (value) {
onSearch();
},

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/hooks/usePackageInfo.dart';
@ -29,7 +30,7 @@ class About extends HookWidget {
version: "2.5.0",
);
return ListTile(
return PlatformListTile(
leading: const Icon(Icons.info_outline_rounded),
title: const Text("About Spotube"),
onTap: () {

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Settings/About.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/components/Shared/AdaptiveListTile.dart';
@ -88,7 +89,7 @@ class Settings extends HookConsumerWidget {
),
),
),
trailing: (context, update) => ElevatedButton(
trailing: (context, update) => PlatformFilledButton(
onPressed: () {
GoRouter.of(context).push("/login");
},
@ -105,7 +106,7 @@ class Settings extends HookConsumerWidget {
if (auth.isLoggedIn)
Builder(builder: (context) {
Auth auth = ref.watch(authProvider);
return ListTile(
return PlatformListTile(
leading: const Icon(Icons.logout_rounded),
title: const SizedBox(
height: 50,
@ -118,7 +119,7 @@ class Settings extends HookConsumerWidget {
),
),
),
trailing: ElevatedButton(
trailing: PlatformFilledButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.red),
@ -144,24 +145,25 @@ class Settings extends HookConsumerWidget {
subtitle: const Text(
"Override responsive layout mode settings",
),
trailing: (context, update) => DropdownButton<LayoutMode>(
trailing: (context, update) =>
PlatformDropDownMenu<LayoutMode>(
value: preferences.layoutMode,
items: const [
DropdownMenuItem(
items: [
PlatformDropDownMenuItem(
value: LayoutMode.adaptive,
child: Text(
child: const Text(
"Adaptive",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: LayoutMode.compact,
child: Text(
child: const Text(
"Compact",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: LayoutMode.extended,
child: Text("Extended"),
child: const Text("Extended"),
),
],
onChanged: (value) {
@ -175,24 +177,25 @@ class Settings extends HookConsumerWidget {
AdaptiveListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Theme"),
trailing: (context, update) => DropdownButton<ThemeMode>(
trailing: (context, update) =>
PlatformDropDownMenu<ThemeMode>(
value: preferences.themeMode,
items: const [
DropdownMenuItem(
items: [
PlatformDropDownMenuItem(
value: ThemeMode.dark,
child: Text(
child: const Text(
"Dark",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: ThemeMode.light,
child: Text(
child: const Text(
"Light",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: ThemeMode.system,
child: Text("System"),
child: const Text("System"),
),
],
onChanged: (value) {
@ -203,7 +206,7 @@ class Settings extends HookConsumerWidget {
},
),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.palette_outlined),
title: const Text("Accent Color Scheme"),
contentPadding: const EdgeInsets.symmetric(
@ -217,7 +220,7 @@ class Settings extends HookConsumerWidget {
),
onTap: pickColorScheme(ColorSchemeType.accent),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.format_color_fill_rounded),
title: const Text("Background Color Scheme"),
contentPadding: const EdgeInsets.symmetric(
@ -231,11 +234,10 @@ class Settings extends HookConsumerWidget {
),
onTap: pickColorScheme(ColorSchemeType.background),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.album_rounded),
title: const Text("Rotating Album Art"),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
trailing: PlatformSwitch(
value: preferences.rotatingAlbumArt,
onChanged: (state) {
preferences.setRotatingAlbumArt(state);
@ -251,18 +253,18 @@ class Settings extends HookConsumerWidget {
leading: const Icon(Icons.multitrack_audio_rounded),
title: const Text("Audio Quality"),
trailing: (context, update) =>
DropdownButton<AudioQuality>(
PlatformDropDownMenu<AudioQuality>(
value: preferences.audioQuality,
items: const [
DropdownMenuItem(
items: [
PlatformDropDownMenuItem(
value: AudioQuality.high,
child: Text(
child: const Text(
"High",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: AudioQuality.low,
child: Text("Low"),
child: const Text("Low"),
),
],
onChanged: (value) {
@ -274,7 +276,7 @@ class Settings extends HookConsumerWidget {
),
),
if (kIsMobile)
ListTile(
PlatformListTile(
leading: const Icon(Icons.download_for_offline_rounded),
title: const Text(
"Pre download and play",
@ -282,21 +284,19 @@ class Settings extends HookConsumerWidget {
subtitle: const Text(
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
trailing: PlatformSwitch(
value: preferences.androidBytesPlay,
onChanged: (state) {
preferences.setAndroidBytesPlay(state);
},
),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.fast_forward_rounded),
title: const Text(
"Skip non-music segments (SponsorBlock)",
),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
trailing: PlatformSwitch(
value: preferences.skipSponsorSegments,
onChanged: (state) {
preferences.setSkipSponsorSegments(state);
@ -320,12 +320,11 @@ class Settings extends HookConsumerWidget {
),
trailing: (context, update) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: DropdownButton(
isExpanded: true,
child: PlatformDropDownMenu(
value: preferences.recommendationMarket,
items: spotifyMarkets
.map(
(country) => (DropdownMenuItem(
(country) => (PlatformDropDownMenuItem(
value: country.first,
child: Text(country.last),
)),
@ -358,18 +357,15 @@ class Settings extends HookConsumerWidget {
breakOn: Breakpoints.lg,
trailing: (context, update) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450),
child: TextField(
child: PlatformTextField(
controller: ytSearchFormatController,
decoration: InputDecoration(
isDense: true,
suffix: ElevatedButton(
child: const Icon(Icons.save_rounded),
onPressed: () {
preferences.setYtSearchFormat(
ytSearchFormatController.value.text,
);
},
),
suffix: PlatformFilledButton(
child: const Icon(Icons.save_rounded),
onPressed: () {
preferences.setYtSearchFormat(
ytSearchFormatController.value.text,
);
},
),
onSubmitted: (value) {
preferences.setYtSearchFormat(value);
@ -392,24 +388,24 @@ class Settings extends HookConsumerWidget {
),
),
trailing: (context, update) =>
DropdownButton<SpotubeTrackMatchAlgorithm>(
PlatformDropDownMenu<SpotubeTrackMatchAlgorithm>(
value: preferences.trackMatchAlgorithm,
items: const [
DropdownMenuItem(
items: [
PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.authenticPopular,
child: Text(
child: const Text(
"Popular from Author",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.popular,
child: Text(
child: const Text(
"Accurately Popular",
),
),
DropdownMenuItem(
PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.youtube,
child: Text("YouTube's Top choice"),
child: const Text("YouTube's Top choice"),
),
],
onChanged: (value) {
@ -425,21 +421,20 @@ class Settings extends HookConsumerWidget {
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.file_download_outlined),
title: const Text("Download Location"),
subtitle: Text(preferences.downloadLocation),
trailing: ElevatedButton(
trailing: PlatformFilledButton(
onPressed: pickDownloadLocation,
child: const Icon(Icons.folder_rounded),
),
onTap: pickDownloadLocation,
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.lyrics_rounded),
title: const Text("Download lyrics along with the Track"),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
trailing: PlatformSwitch(
value: preferences.saveTrackLyrics,
onChanged: (state) {
preferences.setSaveTrackLyrics(state);
@ -487,11 +482,10 @@ class Settings extends HookConsumerWidget {
},
),
),
ListTile(
PlatformListTile(
leading: const Icon(Icons.update_rounded),
title: const Text("Check for Update"),
trailing: Switch.adaptive(
activeColor: Theme.of(context).primaryColor,
trailing: PlatformSwitch(
value: preferences.checkUpdate,
onChanged: (checked) =>
preferences.setCheckUpdate(checked),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
class AdaptiveListTile extends HookWidget {
@ -24,7 +25,7 @@ class AdaptiveListTile extends HookWidget {
Widget build(BuildContext context) {
final breakpoint = useBreakpoints();
return ListTile(
return PlatformListTile(
title: title,
subtitle: subtitle,
trailing:

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
@ -67,7 +68,7 @@ class PlaybuttonCard extends StatelessWidget {
bottom: 10,
end: 5,
child: Builder(builder: (context) {
return ElevatedButton(
return PlatformFilledButton(
onPressed: onPlaybuttonPressed,
style: ButtonStyle(
shape: MaterialStateProperty.all(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
class SortTracksDropdown extends StatelessWidget {
@ -12,43 +13,41 @@ class SortTracksDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<SortBy>(
itemBuilder: (context) {
return [
PopupMenuItem(
value: SortBy.none,
enabled: value != SortBy.none,
child: const Text("None"),
),
PopupMenuItem(
value: SortBy.ascending,
enabled: value != SortBy.ascending,
child: const Text("Sort by A-Z"),
),
PopupMenuItem(
value: SortBy.descending,
enabled: value != SortBy.descending,
child: const Text("Sort by Z-A"),
),
PopupMenuItem(
value: SortBy.dateAdded,
enabled: value != SortBy.dateAdded,
child: const Text("Sort by Date"),
),
PopupMenuItem(
value: SortBy.artist,
enabled: value != SortBy.artist,
child: const Text("Sort by Artist"),
),
PopupMenuItem(
value: SortBy.album,
enabled: value != SortBy.album,
child: const Text("Sort by Album"),
),
];
},
return PlatformPopupMenuButton<SortBy>(
items: [
PlatformPopupMenuItem(
value: SortBy.none,
enabled: value != SortBy.none,
child: const Text("None"),
),
PlatformPopupMenuItem(
value: SortBy.ascending,
enabled: value != SortBy.ascending,
child: const Text("Sort by A-Z"),
),
PlatformPopupMenuItem(
value: SortBy.descending,
enabled: value != SortBy.descending,
child: const Text("Sort by Z-A"),
),
PlatformPopupMenuItem(
value: SortBy.dateAdded,
enabled: value != SortBy.dateAdded,
child: const Text("Sort by Date"),
),
PlatformPopupMenuItem(
value: SortBy.artist,
enabled: value != SortBy.artist,
child: const Text("Sort by Artist"),
),
PlatformPopupMenuItem(
value: SortBy.album,
enabled: value != SortBy.album,
child: const Text("Sort by Album"),
),
],
onSelected: onChanged,
icon: const Icon(Icons.sort_rounded),
child: const Icon(Icons.sort_rounded),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
import 'package:spotube/entities/CacheTrack.dart';
@ -198,57 +199,66 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
};
}, []);
return MaterialApp.router(
routerConfig: router,
platform = TargetPlatform.macOS;
return PlatformApp.router(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider,
debugShowCheckedModeBanner: false,
title: 'Spotube',
theme: lightTheme(
androidTheme: lightTheme(
accentMaterialColor: accentMaterialColor,
backgroundMaterialColor: backgroundMaterialColor,
),
darkTheme: darkTheme(
androidDarkTheme: darkTheme(
accentMaterialColor: accentMaterialColor,
backgroundMaterialColor: backgroundMaterialColor,
),
themeMode: themeMode,
shortcuts: {
...WidgetsApp.defaultShortcuts,
const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref),
const SingleActivator(LogicalKeyboardKey.comma, control: true):
shortcuts: PlatformProperty.all({
...WidgetsApp.defaultShortcuts.map((key, value) {
return MapEntry(
LogicalKeySet.fromSet(key.triggers?.toSet() ?? {}),
value,
);
}),
LogicalKeySet(LogicalKeyboardKey.space): PlayPauseIntent(ref),
LogicalKeySet(LogicalKeyboardKey.comma, LogicalKeyboardKey.control):
NavigationIntent(router, "/settings"),
const SingleActivator(
LogicalKeySet(
LogicalKeyboardKey.keyB,
control: true,
shift: true,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.browse),
const SingleActivator(
LogicalKeySet(
LogicalKeyboardKey.keyS,
control: true,
shift: true,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.search),
const SingleActivator(
LogicalKeySet(
LogicalKeyboardKey.keyL,
control: true,
shift: true,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.library),
const SingleActivator(
LogicalKeySet(
LogicalKeyboardKey.keyY,
control: true,
shift: true,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.lyrics),
const SingleActivator(
LogicalKeySet(
LogicalKeyboardKey.keyW,
control: true,
shift: true,
LogicalKeyboardKey.control,
LogicalKeyboardKey.shift,
): CloseAppIntent(),
},
actions: {
}),
actions: PlatformProperty.all({
...WidgetsApp.defaultActions,
PlayPauseIntent: PlayPauseAction(),
NavigationIntent: NavigationAction(),
HomeTabIntent: HomeTabAction(),
CloseAppIntent: CloseAppAction(),
},
}),
);
}
}

View File

@ -10,6 +10,7 @@ import audio_session
import audioplayers_darwin
import bitsdojo_window_macos
import connectivity_plus_macos
import macos_ui
import metadata_god
import package_info_plus_macos
import path_provider_macos
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@ -493,6 +493,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1"
fluent_ui:
dependency: transitive
description:
name: fluent_ui
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.3"
flutter:
dependency: "direct main"
description: flutter
@ -561,6 +568,11 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -690,6 +702,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
introduction_screen:
dependency: "direct main"
description:
@ -739,6 +758,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
macos_ui:
dependency: transitive
description:
name: macos_ui
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.5"
marquee:
dependency: "direct main"
description:
@ -972,6 +998,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
platform_ui:
dependency: "direct main"
description:
path: "../platform_ui"
relative: true
source: path
version: "0.0.1"
plugin_platform_interface:
dependency: transitive
description:
@ -1021,6 +1054,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0+1"
recase:
dependency: transitive
description:
name: recase
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
riverpod:
dependency: transitive
description:
@ -1035,6 +1075,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.3"
scroll_pos:
dependency: transitive
description:
name: scroll_pos
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
scroll_to_index:
dependency: "direct main"
description:

View File

@ -62,6 +62,8 @@ dependencies:
flutter_inappwebview: ^5.4.3+7
tuple: ^2.0.1
uuid: ^3.0.6
platform_ui:
path: ../platform_ui
dev_dependencies:
flutter_test: