From 9eee573ce928aa6c03dcb50bf1521350d2de32cc Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 29 Oct 2022 14:23:17 +0600 Subject: [PATCH] feat: initial platform_ui integration What's changed: - Sidebar - Settings - UserLibrary (root) - Search (search field) --- lib/components/Home/Shell.dart | 18 +- lib/components/Home/Sidebar.dart | 258 +++++++++--------- lib/components/Library/UserDownloads.dart | 4 +- lib/components/Library/UserLibrary.dart | 47 ++-- lib/components/Library/UserLocalTracks.dart | 21 +- .../LoaderShimmers/ShimmerArtistProfile.dart | 7 +- .../LoaderShimmers/ShimmerCategories.dart | 8 +- .../LoaderShimmers/ShimmerLyrics.dart | 8 +- .../LoaderShimmers/ShimmerPlaybuttonCard.dart | 8 +- .../LoaderShimmers/ShimmerTrackTile.dart | 8 +- lib/components/Lyrics/Lyrics.dart | 39 ++- .../Playlist/PlaylistCreateDialog.dart | 21 +- lib/components/Search/Search.dart | 19 +- lib/components/Settings/About.dart | 3 +- lib/components/Settings/Settings.dart | 126 ++++----- lib/components/Shared/AdaptiveListTile.dart | 3 +- lib/components/Shared/PlaybuttonCard.dart | 3 +- lib/components/Shared/SortTracksDropdown.dart | 71 +++-- lib/main.dart | 62 +++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 47 ++++ pubspec.yaml | 2 + 22 files changed, 443 insertions(+), 342 deletions(-) diff --git a/lib/components/Home/Shell.dart b/lib/components/Home/Shell.dart index 8844b6c66..dc51c464c 100644 --- a/lib/components/Home/Shell.dart +++ b/lib/components/Home/Shell.dart @@ -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( diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 5903d44d3..517c40390 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -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((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; + ], + ), + 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, - ); + 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]); + 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), + 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.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, 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, - ), - ), - ), - ], + 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, - ), ), ), - ); - } - }, + 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, + ), + ), ), - ) - ], - ), + ); + } + }, ), ); } diff --git a/lib/components/Library/UserDownloads.dart b/lib/components/Library/UserDownloads.dart index 557e94d3d..2b522b1db 100644 --- a/lib/components/Library/UserDownloads.dart +++ b/lib/components/Library/UserDownloads.dart @@ -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 ?? [], diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 26ef766a6..ef0134d0e 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -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()), + }, ), ), ); diff --git a/lib/components/Library/UserLocalTracks.dart b/lib/components/Library/UserLocalTracks.dart index 221aa213e..6aaf7d3be 100644 --- a/lib/components/Library/UserLocalTracks.dart +++ b/lib/components/Library/UserLocalTracks.dart @@ -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); diff --git a/lib/components/LoaderShimmers/ShimmerArtistProfile.dart b/lib/components/LoaderShimmers/ShimmerArtistProfile.dart index ccabeb640..6cd552f03 100644 --- a/lib/components/LoaderShimmers/ShimmerArtistProfile.dart +++ b/lib/components/LoaderShimmers/ShimmerArtistProfile.dart @@ -11,10 +11,11 @@ class ShimmerArtistProfile extends HookWidget { @override Widget build(BuildContext context) { final shimmerColor = - Theme.of(context).extension()!.shimmerColor!; + Theme.of(context).extension()?.shimmerColor ?? + Colors.white; final shimmerBackgroundColor = Theme.of(context) - .extension()! - .shimmerBackgroundColor!; + .extension() + ?.shimmerBackgroundColor; final avatarWidth = useBreakpointValue( sm: MediaQuery.of(context).size.width * 0.80, diff --git a/lib/components/LoaderShimmers/ShimmerCategories.dart b/lib/components/LoaderShimmers/ShimmerCategories.dart index 7c0d52277..4c2319221 100644 --- a/lib/components/LoaderShimmers/ShimmerCategories.dart +++ b/lib/components/LoaderShimmers/ShimmerCategories.dart @@ -9,10 +9,12 @@ class ShimmerCategories extends StatelessWidget { @override Widget build(BuildContext context) { final shimmerColor = - Theme.of(context).extension()!.shimmerColor!; + Theme.of(context).extension()?.shimmerColor ?? + Colors.white; final shimmerBackgroundColor = Theme.of(context) - .extension()! - .shimmerBackgroundColor!; + .extension() + ?.shimmerBackgroundColor ?? + Colors.grey; return Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/components/LoaderShimmers/ShimmerLyrics.dart b/lib/components/LoaderShimmers/ShimmerLyrics.dart index b1da4453c..27843c30c 100644 --- a/lib/components/LoaderShimmers/ShimmerLyrics.dart +++ b/lib/components/LoaderShimmers/ShimmerLyrics.dart @@ -12,10 +12,12 @@ class ShimmerLyrics extends HookWidget { @override Widget build(BuildContext context) { final shimmerColor = - Theme.of(context).extension()!.shimmerColor!; + Theme.of(context).extension()?.shimmerColor ?? + Colors.white; final shimmerBackgroundColor = Theme.of(context) - .extension()! - .shimmerBackgroundColor!; + .extension() + ?.shimmerBackgroundColor ?? + Colors.grey; final breakpoint = useBreakpoints(); diff --git a/lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart b/lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart index e15fcc81e..cfed9690f 100644 --- a/lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart +++ b/lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart @@ -9,10 +9,12 @@ class ShimmerPlaybuttonCard extends StatelessWidget { @override Widget build(BuildContext context) { final shimmerColor = - Theme.of(context).extension()!.shimmerColor!; + Theme.of(context).extension()?.shimmerColor ?? + Colors.white; final shimmerBackgroundColor = Theme.of(context) - .extension()! - .shimmerBackgroundColor!; + .extension() + ?.shimmerBackgroundColor ?? + Colors.grey; final card = Stack( children: [ diff --git a/lib/components/LoaderShimmers/ShimmerTrackTile.dart b/lib/components/LoaderShimmers/ShimmerTrackTile.dart index e9160bcf1..f9c8d9ef8 100644 --- a/lib/components/LoaderShimmers/ShimmerTrackTile.dart +++ b/lib/components/LoaderShimmers/ShimmerTrackTile.dart @@ -13,10 +13,12 @@ class ShimmerTrackTile extends StatelessWidget { @override Widget build(BuildContext context) { final shimmerColor = - Theme.of(context).extension()!.shimmerColor!; + Theme.of(context).extension()?.shimmerColor ?? + Colors.white; final shimmerBackgroundColor = Theme.of(context) - .extension()! - .shimmerBackgroundColor!; + .extension() + ?.shimmerBackgroundColor ?? + Colors.grey; final single = Container( margin: const EdgeInsets.symmetric(horizontal: 20), diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index 77573eea5..225e13be0 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -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( diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart index 6cae933b6..dc0857862 100644 --- a/lib/components/Playlist/PlaylistCreateDialog.dart +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -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, ), diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 3195b195d..10ba07f4e 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -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(); }, diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart index 6a3eaeb18..f243b561c 100644 --- a/lib/components/Settings/About.dart +++ b/lib/components/Settings/About.dart @@ -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: () { diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index b32307b60..cf2b6dee6 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -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( + trailing: (context, update) => + PlatformDropDownMenu( 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( + trailing: (context, update) => + PlatformDropDownMenu( 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( + PlatformDropDownMenu( 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( + PlatformDropDownMenu( 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), diff --git a/lib/components/Shared/AdaptiveListTile.dart b/lib/components/Shared/AdaptiveListTile.dart index 271c598db..ba81621b2 100644 --- a/lib/components/Shared/AdaptiveListTile.dart +++ b/lib/components/Shared/AdaptiveListTile.dart @@ -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: diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index 772319cee..c523e2468 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -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( diff --git a/lib/components/Shared/SortTracksDropdown.dart b/lib/components/Shared/SortTracksDropdown.dart index 2b503613a..0b42dd2a1 100644 --- a/lib/components/Shared/SortTracksDropdown.dart +++ b/lib/components/Shared/SortTracksDropdown.dart @@ -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( - 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( + 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), ); } } diff --git a/lib/main.dart b/lib/main.dart index ae6b3876b..c26eff738 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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(), - }, + }), ); } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f18fb8ac7..7beb29a87 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index 7db67dccd..280f5c49a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 2f7175e6b..db52cc892 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: