diff --git a/.vscode/launch.json b/.vscode/launch.json index b3769b2b..6c0bb0d8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "name": "Flutter", "type": "dart", "request": "launch", - "program": "lib/main.dart" + "program": "${workspaceFolder}/lib/main.dart" }, ], "compounds": [] diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 8619fee1..bf8784b3 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -1,12 +1,16 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide Colors; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; +import 'package:spotube/hooks/usePlatformProperty.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -class ArtistCard extends StatelessWidget { +class ArtistCard extends HookWidget { final Artist artist; const ArtistCard(this.artist, {Key? key}) : super(key: key); @@ -18,28 +22,70 @@ class ArtistCard extends StatelessWidget { placeholder: ImagePlaceholder.artist, ), ); + final boxShadow = usePlatformProperty( + (context) => PlatformProperty( + android: BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor, + ), + ios: null, + macos: null, + linux: BoxShadow( + blurRadius: 6, + color: Theme.of(context).shadowColor.withOpacity(0.3), + ), + windows: null, + ), + ); + + final splash = usePlatformProperty( + (context) => PlatformProperty.only( + android: InkRipple.splashFactory, + other: NoSplash.splashFactory, + ), + ); + return SizedBox( height: 240, width: 200, child: InkWell( + splashFactory: splash, onTap: () { ServiceUtils.navigate(context, "/artist/${artist.id}"); }, - borderRadius: BorderRadius.circular(10), + customBorder: platform == TargetPlatform.windows + ? Border.all( + color: FluentTheme.maybeOf(context) + ?.micaBackgroundColor + .withOpacity(.7) ?? + Colors.transparent, + width: 1, + ) + : null, + borderRadius: BorderRadius.circular( + platform == TargetPlatform.windows ? 5 : 8, + ), child: HoverBuilder(builder: (context, isHovering) { return Ink( width: 200, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), + color: PlatformTheme.of(context).secondaryBackgroundColor, + borderRadius: BorderRadius.circular( + platform == TargetPlatform.windows ? 5 : 8, + ), boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor, - ) + if (boxShadow != null) boxShadow, ], + border: [TargetPlatform.windows, TargetPlatform.macOS] + .contains(platform) + ? Border.all( + color: PlatformTheme.of(context).borderColor ?? + Colors.transparent, + width: 1, + ) + : null, ), child: Padding( padding: const EdgeInsets.all(15), @@ -79,7 +125,7 @@ class ArtistCard extends StatelessWidget { artist.name!, maxLines: 2, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge!.copyWith( + style: PlatformTextTheme.of(context).body?.copyWith( fontWeight: FontWeight.bold, ), ), diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 266137fe..e5924e02 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; 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/Artist/ArtistAlbumList.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; @@ -30,13 +31,13 @@ class ArtistProfile extends HookConsumerWidget { Widget build(BuildContext context, ref) { SpotifyApi spotify = ref.watch(spotifyProvider); final parentScrollController = useScrollController(); - final textTheme = Theme.of(context).textTheme; + final textTheme = PlatformTheme.of(context).textTheme; final chipTextVariant = useBreakpointValue( - sm: textTheme.bodySmall, - md: textTheme.bodyMedium, - lg: textTheme.headline6, - xl: textTheme.headline6, - xxl: textTheme.headline6, + sm: textTheme!.caption, + md: textTheme.body, + lg: textTheme.subheading, + xl: textTheme.headline, + xxl: textTheme.headline, ); final avatarWidth = useBreakpointValue( @@ -52,9 +53,9 @@ class ArtistProfile extends HookConsumerWidget { final Playback playback = ref.watch(playbackProvider); return SafeArea( - child: Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), + child: PlatformScaffold( + appBar: PageWindowTitleBar( + leading: const PlatformBackButton(), ), body: HookBuilder( builder: (context) { @@ -67,7 +68,7 @@ class ArtistProfile extends HookConsumerWidget { return const ShimmerArtistProfile(); } else if (artistsQuery.hasError) { return Center( - child: Text(artistsQuery.error.toString()), + child: PlatformText(artistsQuery.error.toString()), ); } @@ -105,21 +106,22 @@ class ArtistProfile extends HookConsumerWidget { decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(50)), - child: Text(data.type!.toUpperCase(), + child: PlatformText(data.type!.toUpperCase(), style: chipTextVariant?.copyWith( color: Colors.white)), ), - Text( + PlatformText( data.name!, style: breakpoint.isSm - ? textTheme.headline4 - : textTheme.headline2, + ? textTheme.subheading + : textTheme.headline, ), - Text( + PlatformText( "${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers", style: breakpoint.isSm - ? textTheme.bodyText1 - : textTheme.headline5, + ? textTheme.body + : textTheme.body + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 20), Row( @@ -138,11 +140,12 @@ class ArtistProfile extends HookConsumerWidget { return const SizedBox( height: 20, width: 20, - child: CircularProgressIndicator(), + child: + PlatformCircularProgressIndicator(), ); } - return OutlinedButton( + return PlatformFilledButton( onPressed: () async { try { isFollowingQuery.data! @@ -168,7 +171,7 @@ class ArtistProfile extends HookConsumerWidget { ]); } }, - child: Text( + child: PlatformText( isFollowingQuery.data! ? "Following" : "Follow", @@ -176,7 +179,7 @@ class ArtistProfile extends HookConsumerWidget { ); }, ), - IconButton( + PlatformIconButton( icon: const Icon(Icons.share_rounded), onPressed: () { Clipboard.setData( @@ -188,7 +191,7 @@ class ArtistProfile extends HookConsumerWidget { const SnackBar( width: 300, behavior: SnackBarBehavior.floating, - content: Text( + content: PlatformText( "Artist URL copied to clipboard", textAlign: TextAlign.center, ), @@ -213,10 +216,10 @@ class ArtistProfile extends HookConsumerWidget { ); if (topTracksQuery.isLoading || !topTracksQuery.hasData) { - return const CircularProgressIndicator.adaptive(); + return const PlatformCircularProgressIndicator(); } else if (topTracksQuery.hasError) { return Center( - child: Text(topTracksQuery.error.toString()), + child: PlatformText(topTracksQuery.error.toString()), ); } @@ -250,9 +253,10 @@ class ArtistProfile extends HookConsumerWidget { return Column(children: [ Row( children: [ - Text( + PlatformText( "Top Tracks", - style: Theme.of(context).textTheme.headline4, + style: + PlatformTheme.of(context).textTheme?.headline, ), Container( margin: const EdgeInsets.symmetric(horizontal: 5), @@ -260,11 +264,13 @@ class ArtistProfile extends HookConsumerWidget { color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(50), ), - child: IconButton( - icon: Icon(isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded), - color: Colors.white, + child: PlatformIconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, + color: Colors.white, + ), onPressed: () => playPlaylist(topTracks.toList()), ), @@ -290,16 +296,16 @@ class ArtistProfile extends HookConsumerWidget { }, ), const SizedBox(height: 50), - Text( + PlatformText( "Albums", - style: Theme.of(context).textTheme.headline4, + style: PlatformTheme.of(context).textTheme?.headline, ), const SizedBox(height: 10), ArtistAlbumList(artistId), const SizedBox(height: 20), - Text( + PlatformText( "Fans also likes", - style: Theme.of(context).textTheme.headline4, + style: PlatformTheme.of(context).textTheme?.headline, ), const SizedBox(height: 10), HookBuilder( @@ -310,10 +316,10 @@ class ArtistProfile extends HookConsumerWidget { ); if (relatedArtists.isLoading || !relatedArtists.hasData) { - return const CircularProgressIndicator.adaptive(); + return const PlatformCircularProgressIndicator(); } else if (relatedArtists.hasError) { return Center( - child: Text(relatedArtists.error.toString()), + child: PlatformText(relatedArtists.error.toString()), ); } diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 55025526..c94b65f8 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.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/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; @@ -46,15 +47,13 @@ class CategoryCard extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Row( children: [ - Text( - category.name ?? "Unknown", - style: Theme.of(context).textTheme.headline5, - ), + PlatformText.headline(category.name ?? "Unknown"), ], ), ), playlistQuery.hasError - ? Text("Something Went Wrong\n${playlistQuery.errors.first}") + ? PlatformText( + "Something Went Wrong\n${playlistQuery.errors.first}") : SizedBox( height: 245, child: ScrollConfiguration( diff --git a/lib/components/Home/Genres.dart b/lib/components/Home/Genres.dart index 4b85d4d9..e555aa76 100644 --- a/lib/components/Home/Genres.dart +++ b/lib/components/Home/Genres.dart @@ -2,13 +2,16 @@ import 'package:fl_query_hooks/fl_query_hooks.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:spotify/spotify.dart'; import 'package:spotube/components/Category/CategoryCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/utils/platform.dart'; class Genres extends HookConsumerWidget { const Genres({Key? key}) : super(key: key); @@ -40,7 +43,8 @@ class Genres extends HookConsumerWidget { .toList() ]; - return Scaffold( + return PlatformScaffold( + appBar: kIsDesktop ? PageWindowTitleBar() : null, body: ListView.builder( itemCount: categories.length, itemBuilder: (context, index) { diff --git a/lib/components/Home/Shell.dart b/lib/components/Home/Shell.dart index 8844b6c6..b9e85605 100644 --- a/lib/components/Home/Shell.dart +++ b/lib/components/Home/Shell.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.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/Home/Sidebar.dart'; import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; import 'package:spotube/components/Player/Player.dart'; @@ -12,7 +13,7 @@ import 'package:spotube/hooks/useUpdateChecker.dart'; import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/utils/platform.dart'; -const _path = { +const rootPaths = { 0: "/", 1: "/search", 2: "/library", @@ -35,8 +36,8 @@ class Shell extends HookConsumerWidget { useEffect(() { downloader.onFileExists = (track) async { if (!isMounted()) return false; - return await showDialog( - context: context, + return await showPlatformAlertDialog( + context, builder: (context) => ReplaceDownloadedFileDialog( track: track, ), @@ -63,38 +64,14 @@ class Shell extends HookConsumerWidget { return null; }, [backgroundColor]); - final allowedPath = _path.values.contains(GoRouter.of(context).location); - final preferredSize = - allowedPath ? PageWindowTitleBar.staticPreferredSize : Size.zero; - return Scaffold( - appBar: kIsDesktop - ? PreferredSize( - preferredSize: preferredSize, - child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - height: allowedPath - ? PageWindowTitleBar.staticPreferredSize.height - : 0, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 250), - opacity: allowedPath ? 1 : 0, - child: PageWindowTitleBar(preferredSize: preferredSize), - ), - ), - ) - : null, - extendBodyBehindAppBar: true, - body: Row( - children: [ - Sidebar( - selectedIndex: index.value, - onSelectedIndexChanged: (selectedIndex) { - index.value = selectedIndex; - GoRouter.of(context).go(_path[selectedIndex]!); - }, - ), - Expanded(child: child), - ], + return PlatformScaffold( + body: Sidebar( + selectedIndex: index.value, + onSelectedIndexChanged: (i) { + index.value = i; + GoRouter.of(context).go(rootPaths[index.value]!); + }, + child: child, ), extendBody: true, bottomNavigationBar: Column( @@ -105,7 +82,7 @@ class Shell extends HookConsumerWidget { selectedIndex: index.value, onSelectedIndexChanged: (selectedIndex) { index.value = selectedIndex; - GoRouter.of(context).go(_path[selectedIndex]!); + GoRouter.of(context).go(rootPaths[selectedIndex]!); }, ), ], diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 5903d44d..c7557392 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,20 +16,23 @@ 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); - Widget _buildSmallLogo() { + static Widget brandLogo() { return Image.asset( "assets/spotube-logo.png", height: 50, @@ -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), ); @@ -72,7 +75,7 @@ class Sidebar extends HookConsumerWidget { if (layoutMode == LayoutMode.compact || (breakpoints.isSm && layoutMode == LayoutMode.adaptive)) { - return Container(); + return PlatformScaffold(body: child); } void toggleExtended() => @@ -81,19 +84,40 @@ 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) + if (kIsMacOS) SizedBox( height: appWindow.titleBarHeight, width: extended.value ? 256 : 80, child: MoveWindow( child: !extended.value ? Center( - child: IconButton( + child: PlatformIconButton( icon: const Icon(Icons.menu_rounded), onPressed: toggleExtended, ), @@ -103,161 +127,144 @@ class Sidebar extends HookConsumerWidget { ), if (!kIsDesktop && !extended.value) Center( - child: IconButton( + child: PlatformIconButton( icon: const Icon(Icons.menu_rounded), onPressed: toggleExtended, ), ), (extended.value) - ? Row( - children: [ - _buildSmallLogo(), - const SizedBox( - width: 10, - ), - Text( - "Spotube", - style: Theme.of(context).textTheme.headline4, - ), - IconButton( - icon: const Icon(Icons.menu_rounded), - onPressed: toggleExtended, - ), - ], + ? Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + brandLogo(), + const SizedBox( + width: 10, + ), + PlatformText.headline("Spotube"), + PlatformIconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), + ], + ), ) - : _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, - ), - ), - ), - ); - } - }, - ), - ) + : brandLogo(), ], ), + 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: PlatformCircularProgressIndicator(), + ) + 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: PlatformTheme.of(context) + .textTheme + ?.body + ?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + PlatformIconButton( + 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/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart index fbac8a4a..e7bc268a 100644 --- a/lib/components/Home/SpotubeNavigationBar.dart +++ b/lib/components/Home/SpotubeNavigationBar.dart @@ -1,7 +1,7 @@ -import 'package:badges/badges.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/components/Home/Sidebar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; @@ -37,37 +37,23 @@ class SpotubeNavigationBar extends HookConsumerWidget { if (layoutMode == LayoutMode.extended || (breakpoint.isMoreThan(Breakpoints.sm) && layoutMode == LayoutMode.adaptive)) return const SizedBox(); - return NavigationBar( - destinations: [ + return PlatformBottomNavigationBar( + items: [ ...sidebarTileList.map( (e) { - final icon = Icon(e.icon); - return NavigationDestination( - 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, + return PlatformBottomNavigationBarItem( + icon: e.icon, label: e.title, ); }, ), - const NavigationDestination( - icon: Icon(Icons.settings_rounded), + const PlatformBottomNavigationBarItem( + icon: Icons.settings_rounded, label: "Settings", ) ], selectedIndex: insideSelectedIndex.value, - onDestinationSelected: (i) { + onSelectedIndexChanged: (i) { if (i == 4) { insideSelectedIndex.value = 4; Sidebar.goToSettings(context); diff --git a/lib/components/Library/UserAlbums.dart b/lib/components/Library/UserAlbums.dart index 520afa0b..30c7f4ca 100644 --- a/lib/components/Library/UserAlbums.dart +++ b/lib/components/Library/UserAlbums.dart @@ -1,8 +1,11 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Image; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; +import 'package:spotube/components/Shared/AnonymousFallback.dart'; +import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -12,6 +15,10 @@ class UserAlbums extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final auth = ref.watch(authProvider); + if (auth.isAnonymous) { + return const AnonymousFallback(); + } final albumsQuery = useQuery( job: currentUserAlbumsQueryJob, externalData: ref.watch(spotifyProvider), @@ -22,16 +29,22 @@ class UserAlbums extends HookConsumerWidget { } return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Wrap( - spacing: 20, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: WrapAlignment.center, - children: albumsQuery.data! - .map((album) => - AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album))) - .toList(), + child: Material( + type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, + color: PlatformTheme.of(context).scaffoldBackgroundColor, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 20, // gap between adjacent chips + runSpacing: 20, // gap between lines + alignment: WrapAlignment.center, + children: albumsQuery.data! + .map((album) => + AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album))) + .toList(), + ), ), ), ); diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 1641bcb7..757193a1 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -2,9 +2,12 @@ import 'package:fl_query_hooks/fl_query_hooks.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:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; +import 'package:spotube/components/Shared/AnonymousFallback.dart'; import 'package:spotube/components/Shared/Waypoint.dart'; +import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -13,6 +16,10 @@ class UserArtists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final auth = ref.watch(authProvider); + if (auth.isAnonymous) { + return const AnonymousFallback(); + } final artistQuery = useInfiniteQuery( job: currentUserFollowingArtistsQueryJob, externalData: ref.watch(spotifyProvider), @@ -28,26 +35,31 @@ class UserArtists extends HookConsumerWidget { ? false : (artistQuery.pages.last?.items?.length ?? 0) == 15; - return GridView.builder( - itemCount: artists.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 250, - crossAxisSpacing: 20, - mainAxisSpacing: 20, + return Material( + type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, + color: PlatformTheme.of(context).scaffoldBackgroundColor, + child: GridView.builder( + itemCount: artists.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 250, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + itemBuilder: (context, index) { + if (index == artists.length - 1 && hasNextPage) { + return Waypoint( + onEnter: () { + artistQuery.fetchNextPage(); + }, + child: ArtistCard(artists[index]), + ); + } + return ArtistCard(artists[index]); + }, ), - padding: const EdgeInsets.all(10), - itemBuilder: (context, index) { - if (index == artists.length - 1 && hasNextPage) { - return Waypoint( - onEnter: () { - artistQuery.fetchNextPage(); - }, - child: ArtistCard(artists[index]), - ); - } - return ArtistCard(artists[index]); - }, ); } } diff --git a/lib/components/Library/UserDownloads.dart b/lib/components/Library/UserDownloads.dart index 557e94d3..33263d6d 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'; @@ -25,19 +26,20 @@ class UserDownloads extends HookConsumerWidget { child: AutoSizeText( "Currently downloading (${downloader.currentlyRunning})", maxLines: 1, - style: Theme.of(context).textTheme.headline5, + style: PlatformTextTheme.of(context).headline, ), ), const SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[50], - foregroundColor: Colors.red[400], + PlatformFilledButton( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red[50]), + foregroundColor: MaterialStatePropertyAll(Colors.red[400]), ), onPressed: downloader.currentlyRunning > 0 ? downloader.cancelAll : null, - child: const Text("Cancel All"), + isSecondary: true, + child: const PlatformText("Cancel All"), ), ], ), @@ -47,8 +49,8 @@ class UserDownloads extends HookConsumerWidget { itemCount: downloader.inQueue.length, itemBuilder: (context, index) { final track = downloader.inQueue.elementAt(index); - return ListTile( - title: Text(track.name!), + return PlatformListTile( + title: Text(track.name ?? ''), leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: ClipRRect( @@ -66,9 +68,8 @@ class UserDownloads extends HookConsumerWidget { trailing: const SizedBox( width: 30, height: 30, - child: CircularProgressIndicator.adaptive(), + child: PlatformCircularProgressIndicator(), ), - 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 26ef766a..e426b00e 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -1,42 +1,49 @@ import 'package:flutter/material.dart' hide Image; -import 'package:flutter_riverpod/flutter_riverpod.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/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'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -class UserLibrary extends ConsumerWidget { +class UserLibrary extends HookConsumerWidget { const UserLibrary({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - 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"), - ], - ), + final index = useState(0); + + final body = [ + const UserPlaylists(), + const UserLocalTracks(), + const UserDownloads(), + const UserArtists(), + const UserAlbums(), + ][index.value]; + + return SafeArea( + child: PlatformScaffold( + appBar: PageWindowTitleBar( + titleWidth: 347, + centerTitle: true, + center: PlatformTabBar( + androidIsScrollable: true, + selectedIndex: index.value, + onSelectedIndexChanged: (value) => index.value = value, + isNavigational: + PlatformProperty.byPlatformGroup(mobile: false, desktop: true), + tabs: [ + PlatformTab(label: 'Playlists', icon: const SizedBox.shrink()), + PlatformTab(label: 'Tracks', icon: const SizedBox.shrink()), + PlatformTab(label: 'Downloads', icon: const SizedBox.shrink()), + PlatformTab(label: 'Artists', icon: const SizedBox.shrink()), + PlatformTab(label: 'Albums', icon: const SizedBox.shrink()), + ], ), - body: const TabBarView(children: [ - AnonymousFallback(child: UserPlaylists()), - UserDownloads(), - UserLocalTracks(), - AnonymousFallback(child: UserArtists()), - AnonymousFallback(child: UserAlbums()), - ]), ), + body: body, ), ); } diff --git a/lib/components/Library/UserLocalTracks.dart b/lib/components/Library/UserLocalTracks.dart index 221aa213..6aaf7d3b 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/Library/UserPlaylists.dart b/lib/components/Library/UserPlaylists.dart index 26d2bf91..8fdfcefa 100644 --- a/lib/components/Library/UserPlaylists.dart +++ b/lib/components/Library/UserPlaylists.dart @@ -1,10 +1,13 @@ import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Image; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart'; +import 'package:spotube/components/Shared/AnonymousFallback.dart'; +import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -13,6 +16,11 @@ class UserPlaylists extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final auth = ref.watch(authProvider); + if (auth.isAnonymous) { + return const AnonymousFallback(); + } + final playlistsQuery = useQuery( job: currentUserPlaylistsQueryJob, externalData: ref.watch(spotifyProvider), @@ -34,19 +42,24 @@ class UserPlaylists extends HookConsumerWidget { } return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Wrap( - spacing: 20, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: WrapAlignment.center, - children: [ - const PlaylistCreateDialog(), - PlaylistCard(likedTracksPlaylist), - ...playlistsQuery.data! - .map((playlist) => PlaylistCard(playlist)) - .toList(), - ], + child: Material( + type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 20, // gap between adjacent chips + runSpacing: 20, // gap between lines + alignment: WrapAlignment.center, + children: [ + const PlaylistCreateDialog(), + PlaylistCard(likedTracksPlaylist), + ...playlistsQuery.data! + .map((playlist) => PlaylistCard(playlist)) + .toList(), + ], + ), ), ), ); diff --git a/lib/components/LoaderShimmers/ShimmerArtistProfile.dart b/lib/components/LoaderShimmers/ShimmerArtistProfile.dart index ccabeb64..6cd552f0 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 7c0d5227..4c231922 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 b1da4453..27843c30 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 e15fcc81..cfed9690 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 e9160bcf..f9c8d9ef 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/Login/LoginTutorial.dart b/lib/components/Login/LoginTutorial.dart index 21a51bbb..4a0ec3a3 100644 --- a/lib/components/Login/LoginTutorial.dart +++ b/lib/components/Login/LoginTutorial.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:introduction_screen/introduction_screen.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -13,46 +14,56 @@ class LoginTutorial extends ConsumerWidget { @override Widget build(BuildContext context, ref) { final auth = ref.watch(authProvider); + final key = GlobalKey>(); - return Scaffold( + return PlatformScaffold( appBar: PageWindowTitleBar( - leading: TextButton( - child: const Text("Exit"), + leading: PlatformTextButton( + child: const PlatformText("Exit"), onPressed: () { Navigator.of(context).pop(); }, ), ), body: IntroductionScreen( - next: const Text("Next"), - back: const Text("Previous"), + key: key, + overrideBack: PlatformFilledButton( + isSecondary: true, + child: const Center(child: PlatformText("Previous")), + onPressed: () { + (key.currentState as IntroductionScreenState).previous(); + }, + ), + overrideNext: PlatformFilledButton( + child: const Center(child: PlatformText("Next")), + onPressed: () { + (key.currentState as IntroductionScreenState).next(); + }, + ), showBackButton: true, - overrideDone: TextButton( + overrideDone: PlatformFilledButton( onPressed: auth.isLoggedIn ? () { ServiceUtils.navigate(context, "/"); } : null, - child: const Text("Done"), + child: const Center(child: PlatformText("Done")), ), pages: [ PageViewModel( title: "Step 1", image: Image.asset("assets/tutorial/step-1.png"), bodyWidget: Wrap( - children: [ - Text( + children: const [ + PlatformText( "First, Go to ", - style: Theme.of(context).textTheme.bodyText1, ), Hyperlink( "accounts.spotify.com ", "https://accounts.spotify.com", - style: Theme.of(context).textTheme.bodyText1!, ), - Text( + PlatformText( "and Login/Sign up if you're not logged in", - style: Theme.of(context).textTheme.bodyText1, ), ], ), @@ -60,10 +71,9 @@ class LoginTutorial extends ConsumerWidget { PageViewModel( title: "Step 2", image: Image.asset("assets/tutorial/step-2.png"), - bodyWidget: Text( + bodyWidget: const PlatformText( "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection", textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyText1, ), ), PageViewModel( @@ -71,10 +81,9 @@ class LoginTutorial extends ConsumerWidget { image: Image.asset( "assets/tutorial/step-3.png", ), - bodyWidget: Text( + bodyWidget: const PlatformText( "Copy the values of \"sp_dc\" and \"sp_key\" Cookies", textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyText1, ), ), if (auth.isLoggedIn) @@ -91,13 +100,12 @@ class LoginTutorial extends ConsumerWidget { PageViewModel( title: "Step 5", bodyWidget: Column( - children: [ - Text( + children: const [ + PlatformText( "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields", - style: Theme.of(context).textTheme.bodyText1, ), - const SizedBox(height: 10), - const TokenLoginForm(), + SizedBox(height: 10), + TokenLoginForm(), ], ), ), diff --git a/lib/components/Login/TokenLogin.dart b/lib/components/Login/TokenLogin.dart index 57b2be9a..681818f5 100644 --- a/lib/components/Login/TokenLogin.dart +++ b/lib/components/Login/TokenLogin.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.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/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; @@ -14,12 +15,17 @@ class TokenLogin extends HookConsumerWidget { final textTheme = Theme.of(context).textTheme; return SafeArea( - child: Scaffold( - appBar: const PageWindowTitleBar(leading: BackButton()), + child: PlatformScaffold( + appBar: PageWindowTitleBar(leading: const PlatformBackButton()), body: SingleChildScrollView( child: Center( child: Container( - margin: const EdgeInsets.symmetric(horizontal: 10), + margin: const EdgeInsets.all(10), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: PlatformTheme.of(context).secondaryBackgroundColor, + borderRadius: BorderRadius.circular(10), + ), child: Column( children: [ Image.asset( @@ -27,11 +33,11 @@ class TokenLogin extends HookConsumerWidget { width: MediaQuery.of(context).size.width * (breakpoint <= Breakpoints.md ? .5 : .3), ), - Text("Add your spotify credentials to get started", + PlatformText("Add your spotify credentials to get started", style: breakpoint <= Breakpoints.md ? textTheme.headline5 : textTheme.headline4), - Text( + PlatformText( "Don't worry, any of your credentials won't be collected or shared with anyone", style: Theme.of(context).textTheme.caption, ), @@ -44,9 +50,9 @@ class TokenLogin extends HookConsumerWidget { alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const Text("Don't know how to do this?"), - TextButton( - child: const Text( + const PlatformText("Don't know how to do this?"), + PlatformTextButton( + child: const PlatformText( "Follow along the Step by Step guide", ), onPressed: () => GoRouter.of(context).push( diff --git a/lib/components/Login/TokenLoginForms.dart b/lib/components/Login/TokenLoginForms.dart index f50e25db..85f7c77d 100644 --- a/lib/components/Login/TokenLoginForms.dart +++ b/lib/components/Login/TokenLoginForms.dart @@ -1,6 +1,7 @@ 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/Auth.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -24,31 +25,27 @@ class TokenLoginForm extends HookConsumerWidget { ), child: Column( children: [ - TextField( + PlatformTextField( controller: directCodeController, - decoration: const InputDecoration( - hintText: "Spotify \"sp_dc\" Cookie", - label: Text("sp_dc Cookie"), - ), + placeholder: "Spotify \"sp_dc\" Cookie", + label: "sp_dc Cookie", keyboardType: TextInputType.visiblePassword, ), const SizedBox(height: 10), - TextField( + PlatformTextField( controller: keyCodeController, - decoration: const InputDecoration( - hintText: "Spotify \"sp_key\" Cookie", - label: Text("sp_key Cookie"), - ), + placeholder: "Spotify \"sp_key\" Cookie", + label: "sp_key Cookie", keyboardType: TextInputType.visiblePassword, ), const SizedBox(height: 20), - ElevatedButton( + PlatformFilledButton( onPressed: () async { if (keyCodeController.text.isEmpty || directCodeController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text("Please fill in all fields"), + content: PlatformText("Please fill in all fields"), behavior: SnackBarBehavior.floating, ), ); @@ -67,7 +64,7 @@ class TokenLoginForm extends HookConsumerWidget { onDone?.call(); } }, - child: const Text("Submit"), + child: const PlatformText("Submit"), ) ], ), diff --git a/lib/components/Login/WebViewLogin.dart b/lib/components/Login/WebViewLogin.dart index 4ab84016..32f29a35 100644 --- a/lib/components/Login/WebViewLogin.dart +++ b/lib/components/Login/WebViewLogin.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.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/provider/Auth.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -23,7 +24,7 @@ class WebViewLogin extends HookConsumerWidget { ); } - return Scaffold( + return PlatformScaffold( body: SafeArea( child: InAppWebView( initialOptions: InAppWebViewGroupOptions( diff --git a/lib/components/Lyrics/LyricDelayAdjustDialog.dart b/lib/components/Lyrics/LyricDelayAdjustDialog.dart index 222efa01..19dacec7 100644 --- a/lib/components/Lyrics/LyricDelayAdjustDialog.dart +++ b/lib/components/Lyrics/LyricDelayAdjustDialog.dart @@ -1,6 +1,8 @@ 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/components/Home/Sidebar.dart'; import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; class LyricDelayAdjustDialog extends HookConsumerWidget { @@ -15,16 +17,20 @@ class LyricDelayAdjustDialog extends HookConsumerWidget { double getValue() => double.tryParse(controller.text.replaceAll("ms", "")) ?? 0; - return AlertDialog( + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), title: const Center(child: Text("Adjust Lyrics Delay")), - actions: [ - ElevatedButton( - child: const Text("Cancel"), + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, onPressed: () { Navigator.of(context).pop(); }, + child: const Text("Cancel"), ), - ElevatedButton( + ], + primaryActions: [ + PlatformFilledButton( child: const Text("Done"), onPressed: () { Navigator.of(context).pop( @@ -35,39 +41,39 @@ class LyricDelayAdjustDialog extends HookConsumerWidget { }, ) ], - content: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove_rounded), - onPressed: () { - controller.text = "${getValue() - 25}ms"; - }, - ), - Flexible( - child: TextField( - keyboardType: TextInputType.number, - controller: controller, - decoration: const InputDecoration( - isDense: true, - hintText: "Delay in milliseconds", - ), - onSubmitted: (_) { - Navigator.of(context).pop( - Duration( - milliseconds: getValue().toInt(), - ), - ); + content: SizedBox( + height: 100, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformIconButton( + icon: const Icon(Icons.remove_rounded), + onPressed: () { + controller.text = "${getValue() - 25}ms"; }, ), - ), - IconButton( - icon: const Icon(Icons.add_rounded), - onPressed: () { - controller.text = "${getValue() + 25}ms"; - }, - ), - ], + Flexible( + child: PlatformTextField( + keyboardType: TextInputType.number, + controller: controller, + placeholder: "Delay in milliseconds", + onSubmitted: (_) { + Navigator.of(context).pop( + Duration( + milliseconds: getValue().toInt(), + ), + ); + }, + ), + ), + PlatformIconButton( + icon: const Icon(Icons.add_rounded), + onPressed: () { + controller.text = "${getValue() + 25}ms"; + }, + ), + ], + ), ), ); } diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index 77573eea..0a344a98 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -3,12 +3,15 @@ import 'dart:ui'; 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/components/Lyrics/GeniusLyrics.dart'; import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class Lyrics extends HookConsumerWidget { @@ -26,6 +29,7 @@ class Lyrics extends HookConsumerWidget { [playback.track?.album?.images], ); final palette = usePaletteColor(albumArt, ref); + final index = useState(0); useCustomStatusBarColor( palette.color, @@ -33,38 +37,52 @@ class Lyrics extends HookConsumerWidget { noSetBGColor: true, ); - return DefaultTabController( - length: 2, - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: const TabBar( - isScrollable: true, - tabs: [ - Tab(text: "Synced Lyrics"), - Tab(text: "Lyrics (genius.com)"), - ], - ), - body: 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: SafeArea( - child: TabBarView( - children: [ - SyncedLyrics(palette: palette), - GeniusLyrics(palette: palette), - ], - ), + final body = [ + SyncedLyrics(palette: palette), + GeniusLyrics(palette: palette), + ][index.value]; + + return PlatformScaffold( + extendBodyBehindAppBar: true, + appBar: !kIsMacOS + ? PageWindowTitleBar( + toolbarOpacity: 0, + backgroundColor: Colors.transparent, + center: PlatformTabBar( + isNavigational: + PlatformProperty.only(linux: true, other: false), + selectedIndex: index.value, + onSelectedIndexChanged: (value) => index.value = value, + backgroundColor: + PlatformTheme.of(context).scaffoldBackgroundColor, + tabs: [ + PlatformTab( + label: "Synced", + icon: const SizedBox.shrink(), + color: PlatformTextTheme.of(context).caption?.color, + ), + PlatformTab( + label: "Genius", + icon: const SizedBox.shrink(), + color: PlatformTextTheme.of(context).caption?.color, + ), + ], ), - ), + ) + : null, + body: 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: SafeArea(child: body), ), ), ), diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index 042102a8..ea130908 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -3,6 +3,7 @@ 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:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; @@ -73,6 +74,7 @@ class SyncedLyrics extends HookConsumerWidget { height: breakpoint >= Breakpoints.md ? 50 : 30, child: Material( type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, child: Stack( children: [ Center( @@ -85,18 +87,24 @@ class SyncedLyrics extends HookConsumerWidget { Positioned.fill( child: Align( alignment: Alignment.centerRight, - child: IconButton( - tooltip: "Lyrics Delay", - icon: const Icon(Icons.av_timer_rounded), - onPressed: () async { - final delay = await showDialog( - context: context, - builder: (context) => const LyricDelayAdjustDialog(), - ); - if (delay != null) { - ref.read(lyricDelayState.notifier).state = delay; - } - }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: PlatformFilledButton( + child: const Icon( + Icons.av_timer_rounded, + size: 16, + ), + onPressed: () async { + final delay = await showPlatformAlertDialog( + context, + builder: (context) => + const LyricDelayAdjustDialog(), + ); + if (delay != null) { + ref.read(lyricDelayState.notifier).state = delay; + } + }, + ), ), ), ), diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index f012d9de..616f787f 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,11 +1,16 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' as FluentUI; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/hooks/usePlatformProperty.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; @@ -46,10 +51,51 @@ class Player extends HookConsumerWidget { ); } - return Container( - color: Theme.of(context).backgroundColor, + final backgroundColor = usePlatformProperty( + (context) => PlatformProperty( + android: Theme.of(context).backgroundColor, + ios: CupertinoTheme.of(context).scaffoldBackgroundColor, + macos: MacosTheme.of(context).brightness == Brightness.dark + ? Colors.grey[800] + : Colors.blueGrey[50], + linux: Theme.of(context).backgroundColor, + windows: FluentUI.FluentTheme.maybeOf(context)?.micaBackgroundColor, + ), + ); + + final border = usePlatformProperty( + (context) => PlatformProperty( + android: null, + ios: Border( + top: BorderSide( + color: PlatformTheme.of(context).borderColor ?? Colors.transparent, + width: 1, + ), + ), + macos: Border( + top: BorderSide( + color: PlatformTheme.of(context).borderColor ?? Colors.transparent, + width: 1, + ), + ), + linux: Border( + top: BorderSide( + color: PlatformTheme.of(context).borderColor ?? Colors.transparent, + width: 1, + ), + ), + windows: null, + ), + ); + + return DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + border: border, + ), child: Material( type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -90,7 +136,7 @@ class Player extends HookConsumerWidget { } } }, - child: Slider.adaptive( + child: PlatformSlider( min: 0, max: 1, value: volume.value, diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart index 53f93595..9b543d55 100644 --- a/lib/components/Player/PlayerActions.dart +++ b/lib/components/Player/PlayerActions.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.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:spotify/spotify.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Player/PlayerQueue.dart'; @@ -48,7 +49,7 @@ class PlayerActions extends HookConsumerWidget { return Row( mainAxisAlignment: mainAxisAlignment, children: [ - IconButton( + PlatformIconButton( icon: const Icon(Icons.queue_music_rounded), tooltip: 'Queue', onPressed: playback.playlist != null @@ -73,7 +74,7 @@ class PlayerActions extends HookConsumerWidget { } : null, ), - IconButton( + PlatformIconButton( icon: const Icon(Icons.alt_route_rounded), tooltip: "Alternative Track Sources", onPressed: playback.track != null @@ -108,7 +109,7 @@ class PlayerActions extends HookConsumerWidget { ), ) else - IconButton( + PlatformIconButton( tooltip: 'Download track', icon: Icon( isDownloaded diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index d0a2c413..71779116 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.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/hooks/playback.dart'; import 'package:spotube/models/Intents.dart'; import 'package:spotube/models/Logger.dart'; @@ -94,10 +95,9 @@ class PlayerControls extends HookConsumerWidget { return Column( children: [ - Tooltip( + PlatformTooltip( message: "Slide to seek forward or backward", - child: Slider.adaptive( - focusNode: FocusNode(), + child: PlatformSlider( // cannot divide by zero // there's an edge case for value being bigger // than total duration. Keeping it resolved @@ -122,10 +122,10 @@ class PlayerControls extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( + PlatformText( "$currentMinutes:$currentSeconds", ), - Text("$totalMinutes:$totalSeconds"), + PlatformText("$totalMinutes:$totalSeconds"), ], ), ), @@ -138,7 +138,7 @@ class PlayerControls extends HookConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( + PlatformIconButton( tooltip: playback.isLoop ? "Repeat playlist" : playback.isShuffled @@ -156,14 +156,16 @@ class PlayerControls extends HookConsumerWidget { ? null : playback.cyclePlaybackMode, ), - IconButton( + PlatformIconButton( tooltip: "Previous track", - icon: const Icon(Icons.skip_previous_rounded), - color: iconColor, + icon: Icon( + Icons.skip_previous_rounded, + color: iconColor, + ), onPressed: () { onPrevious(); }), - IconButton( + PlatformIconButton( tooltip: playback.isPlaying ? "Pause playback" : "Resume playback", @@ -171,29 +173,33 @@ class PlayerControls extends HookConsumerWidget { ? const SizedBox( height: 20, width: 20, - child: CircularProgressIndicator(), + child: PlatformCircularProgressIndicator(), ) : Icon( playback.isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + color: iconColor, ), - color: iconColor, onPressed: Actions.handler( context, PlayPauseIntent(ref), ), ), - IconButton( + PlatformIconButton( tooltip: "Next track", - icon: const Icon(Icons.skip_next_rounded), + icon: Icon( + Icons.skip_next_rounded, + color: iconColor, + ), onPressed: () => onNext(), - color: iconColor, ), - IconButton( + PlatformIconButton( tooltip: "Stop playback", - icon: const Icon(Icons.stop_rounded), - color: iconColor, + icon: Icon( + Icons.stop_rounded, + color: iconColor, + ), onPressed: playback.track != null ? () async { try { diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index f5104722..a7726f68 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.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/Player/PlayerTrackDetails.dart'; import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; @@ -61,6 +62,7 @@ class PlayerOverlay extends HookConsumerWidget { duration: const Duration(milliseconds: 250), opacity: canShow ? 1 : 0, child: Material( + textStyle: PlatformTheme.of(context).textTheme!.body!, type: MaterialType.transparency, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -80,8 +82,10 @@ class PlayerOverlay extends HookConsumerWidget { Row( children: [ IconButton( - icon: const Icon(Icons.skip_previous_rounded), - color: paletteColor.bodyTextColor, + icon: Icon( + Icons.skip_previous_rounded, + color: paletteColor.bodyTextColor, + ), onPressed: () { onPrevious(); }), @@ -92,8 +96,8 @@ class PlayerOverlay extends HookConsumerWidget { ref.read(playbackProvider).isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + color: paletteColor.bodyTextColor, ), - color: paletteColor.bodyTextColor, onPressed: Actions.handler( context, PlayPauseIntent(ref), @@ -102,9 +106,11 @@ class PlayerOverlay extends HookConsumerWidget { }, ), IconButton( - icon: const Icon(Icons.skip_next_rounded), + icon: Icon( + Icons.skip_next_rounded, + color: paletteColor.bodyTextColor, + ), onPressed: () => onNext(), - color: paletteColor.bodyTextColor, ), ], ), diff --git a/lib/components/Player/PlayerQueue.dart b/lib/components/Player/PlayerQueue.dart index 307b918e..d7a04849 100644 --- a/lib/components/Player/PlayerQueue.dart +++ b/lib/components/Player/PlayerQueue.dart @@ -3,6 +3,7 @@ import 'dart:ui'; 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:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/components/Shared/NotFound.dart'; import 'package:spotube/components/Shared/TrackTile.dart'; @@ -47,9 +48,6 @@ class PlayerQueue extends HookConsumerWidget { return null; }, []); - var titleStyle = Theme.of(context).textTheme.headline4?.copyWith( - fontWeight: FontWeight.bold, - ); return BackdropFilter( filter: ImageFilter.blur( sigmaX: 12.0, @@ -61,9 +59,8 @@ class PlayerQueue extends HookConsumerWidget { top: 5.0, ), decoration: BoxDecoration( - color: Theme.of(context) - .navigationRailTheme - .backgroundColor + color: PlatformTheme.of(context) + .scaffoldBackgroundColor ?.withOpacity(0.5), borderRadius: borderRadius, ), @@ -78,16 +75,13 @@ class PlayerQueue extends HookConsumerWidget { borderRadius: BorderRadius.circular(20), ), ), - Text("Queue", style: titleStyle), + PlatformText.subheading("Queue"), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( + child: PlatformText( playback.playlist?.name ?? "", overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1 - ?.copyWith(fontWeight: FontWeight.bold), + style: PlatformTextTheme.of(context).body, ), ), const SizedBox(height: 10), diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart index 4a8a0342..9dfc079b 100644 --- a/lib/components/Player/PlayerTrackDetails.dart +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; @@ -36,13 +37,10 @@ class PlayerTrackDetails extends HookConsumerWidget { ), if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) Flexible( - child: Text( + child: PlatformText( playback.track?.name ?? "Not playing", overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1 - ?.copyWith(fontWeight: FontWeight.bold, color: color), + style: TextStyle(fontWeight: FontWeight.bold, color: color), ), ), @@ -52,13 +50,10 @@ class PlayerTrackDetails extends HookConsumerWidget { flex: 1, child: Column( children: [ - Text( + PlatformText( playback.track?.name ?? "Not playing", overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1 - ?.copyWith(fontWeight: FontWeight.bold, color: color), + style: TextStyle(fontWeight: FontWeight.bold, color: color), ), TypeConversionUtils.artists_X_ClickableArtists( playback.track?.artists ?? [], diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index 2622de4b..6075f8f7 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.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:palette_generator/palette_generator.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -57,7 +58,19 @@ class PlayerView extends HookConsumerWidget { noSetBGColor: true, ); - return Scaffold( + return PlatformScaffold( + appBar: PageWindowTitleBar( + backgroundColor: Colors.transparent, + foregroundColor: paletteColor.titleTextColor, + toolbarOpacity: 0, + leading: PlatformBackButton( + color: PlatformProperty.only( + macos: Colors.transparent, + other: paletteColor.titleTextColor, + ).resolve(platform!), + ), + ), + extendBodyBehindAppBar: true, body: Container( decoration: BoxDecoration( image: DecorationImage( @@ -68,15 +81,11 @@ class PlayerView extends HookConsumerWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Material( + textStyle: PlatformTheme.of(context).textTheme!.body!, color: paletteColor.color.withOpacity(.5), child: SafeArea( child: Column( children: [ - PageWindowTitleBar( - leading: const BackButton(), - backgroundColor: Colors.transparent, - foregroundColor: paletteColor.titleTextColor, - ), Padding( padding: const EdgeInsets.all(10), child: Column( @@ -162,7 +171,7 @@ class PlayerView extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, floatingQueue: false, extraActions: [ - IconButton( + PlatformIconButton( tooltip: "Open Lyrics", icon: const Icon(Icons.lyrics_rounded), onPressed: () { diff --git a/lib/components/Player/SiblingTracksSheet.dart b/lib/components/Player/SiblingTracksSheet.dart index 605bf1bd..172aedfc 100644 --- a/lib/components/Player/SiblingTracksSheet.dart +++ b/lib/components/Player/SiblingTracksSheet.dart @@ -3,6 +3,7 @@ import 'dart:ui'; 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/components/Shared/UniversalImage.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/utils/primitive_utils.dart'; @@ -40,42 +41,46 @@ class SiblingTracksSheet extends HookConsumerWidget { margin: const EdgeInsets.all(8.0), decoration: BoxDecoration( borderRadius: borderRadius, - color: Theme.of(context) - .navigationRailTheme - .backgroundColor - ?.withOpacity(0.5), + color: PlatformTheme.of(context) + .scaffoldBackgroundColor! + .withOpacity(.3), ), child: Scaffold( backgroundColor: Colors.transparent, - appBar: AppBar( + appBar: PlatformAppBar( centerTitle: true, - title: const Text('Alternative Tracks Sources'), + title: PlatformText.subheading( + 'Alternative Tracks Sources', + ), automaticallyImplyLeading: false, backgroundColor: Colors.transparent, + toolbarOpacity: 0, ), - body: Padding( + body: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListView.builder( itemCount: playback.siblingYtVideos.length, itemBuilder: (context, index) { final video = playback.siblingYtVideos[index]; - return ListTile( - title: Text(video.title), - leading: UniversalImage( - path: video.thumbnails.lowResUrl, - height: 60, - width: 60, + return PlatformListTile( + title: PlatformText(video.title), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: UniversalImage( + path: video.thumbnails.lowResUrl, + height: 60, + width: 60, + ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), - horizontalTitleGap: 10, - trailing: Text( + trailing: PlatformText( PrimitiveUtils.toReadableDuration( video.duration ?? Duration.zero, ), ), - subtitle: Text(video.author), + subtitle: PlatformText(video.author), enabled: playback.status != PlaybackStatus.loading, selected: video.id == playback.track!.ytTrack.id, selectedTileColor: Theme.of(context).popupMenuTheme.color, diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart index 6cae933b..71fc1b3b 100644 --- a/lib/components/Playlist/PlaylistCreateDialog.dart +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -2,6 +2,8 @@ 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/components/Home/Sidebar.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -12,10 +14,10 @@ class PlaylistCreateDialog extends HookConsumerWidget { Widget build(BuildContext context, ref) { final spotify = ref.watch(spotifyProvider); - return TextButton( + return PlatformTextButton( onPressed: () { - showDialog( - context: context, + showPlatformAlertDialog( + context, builder: (context) { return HookBuilder(builder: (context) { final playlistName = useTextEditingController(); @@ -23,14 +25,11 @@ class PlaylistCreateDialog extends HookConsumerWidget { final public = useState(false); final collaborative = useState(false); - return AlertDialog( + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), title: const Text("Create a Playlist"), - actions: [ - TextButton( - child: const Text("Cancel"), - onPressed: () => Navigator.of(context).pop(), - ), - ElevatedButton( + primaryActions: [ + PlatformFilledButton( child: const Text("Create"), onPressed: () async { if (playlistName.text.isEmpty) return; @@ -52,38 +51,41 @@ class PlaylistCreateDialog extends HookConsumerWidget { }, ) ], + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, + onPressed: () => Navigator.of(context).pop(), + child: const Text("Cancel"), + ), + ], content: Container( width: MediaQuery.of(context).size.width, constraints: const BoxConstraints(maxWidth: 500), 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, ), const SizedBox(height: 10), - CheckboxListTile( + PlatformCheckbox( value: public.value, - title: const Text("Public"), + label: const PlatformText("Public"), onChanged: (val) => public.value = val ?? false, ), const SizedBox(height: 10), - CheckboxListTile( + PlatformCheckbox( value: collaborative.value, - title: const Text("Collaborative"), + label: const PlatformText("Collaborative"), onChanged: (val) => collaborative.value = val ?? false, ), ], diff --git a/lib/components/Playlist/PlaylistGenreView.dart b/lib/components/Playlist/PlaylistGenreView.dart index aa519f2c..892bca9b 100644 --- a/lib/components/Playlist/PlaylistGenreView.dart +++ b/lib/components/Playlist/PlaylistGenreView.dart @@ -1,5 +1,6 @@ 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/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -19,8 +20,8 @@ class PlaylistGenreView extends ConsumerWidget { @override Widget build(BuildContext context, ref) { return Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), + appBar: PageWindowTitleBar( + leading: const PlatformBackButton(), ), body: Column( children: [ @@ -47,7 +48,7 @@ class PlaylistGenreView extends ConsumerWidget { return const Center(child: Text("Error occurred")); } if (!snapshot.hasData) { - return const CircularProgressIndicator.adaptive(); + return const PlatformCircularProgressIndicator(); } return Center( child: Wrap( diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 3195b195..b2584750 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -3,12 +3,14 @@ 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'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Shared/AnonymousFallback.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; @@ -17,6 +19,7 @@ import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:tuple/tuple.dart'; @@ -56,10 +59,6 @@ class Search extends HookConsumerWidget { job: searchQueryJob(SearchType.artist.key), externalData: getVariables()); - if (auth.isAnonymous) { - return const AnonymousFallback(); - } - void onSearch() { for (final query in [ searchTrack, @@ -75,300 +74,297 @@ class Search extends HookConsumerWidget { } return SafeArea( - child: Material( - color: Theme.of(context).backgroundColor, - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - color: Theme.of(context).backgroundColor, - child: TextField( - 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...", - ), - onSubmitted: (value) { - onSearch(); - }, - ), - ), - HookBuilder( - builder: (context) { - Playback playback = ref.watch(playbackProvider); - List albums = []; - List artists = []; - List tracks = []; - List playlists = []; - final pages = [ - ...searchTrack.pages, - ...searchAlbum.pages, - ...searchPlaylist.pages, - ...searchArtist.pages, - ].expand((page) => page ?? []).toList(); - for (MapEntry page in pages.asMap().entries) { - for (var item in page.value.items ?? []) { - if (item is AlbumSimple) { - albums.add(item); - } else if (item is PlaylistSimple) { - playlists.add(item); - } else if (item is Artist) { - artists.add(item); - } else if (item is Track) { - tracks.add(item); - } - } - } - return Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 20, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - Text( - "Songs", - style: Theme.of(context).textTheme.headline5, - ), - if (searchTrack.isLoading && - !searchTrack.isFetchingNextPage) - const CircularProgressIndicator() - else if (searchTrack.hasError) - Text( - searchTrack.error?[searchTrack.pageParams.last]) - else - ...tracks.asMap().entries.map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return TrackTile( - playback, - track: track, - duration: duration, - isActive: playback.track?.id == track.value.id, - onTrackPlayButtonPressed: (currentTrack) async { - var isPlaylistPlaying = - playback.playlist?.id != null && - playback.playlist?.id == - currentTrack.id; - if (!isPlaylistPlaying) { - playback.playPlaylist( - CurrentPlaylist( - tracks: [currentTrack], - id: currentTrack.id!, - name: currentTrack.name!, - thumbnail: TypeConversionUtils - .image_X_UrlString( - currentTrack.album?.images, - placeholder: - ImagePlaceholder.albumArt, - ), - ), - ); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playback.track?.id) { - playback.play(currentTrack); - } - }, - ); - }), - if (searchTrack.hasNextPage && tracks.isNotEmpty) - Center( - child: TextButton( - onPressed: searchTrack.isFetchingNextPage - ? null - : () => searchTrack.fetchNextPage(), - child: searchTrack.isFetchingNextPage - ? const CircularProgressIndicator() - : const Text("Load more"), - ), - ), - if (playlists.isNotEmpty) - Text( - "Playlists", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - if (searchPlaylist.isLoading && - !searchPlaylist.isFetchingNextPage) - const CircularProgressIndicator() - else if (searchPlaylist.hasError) - Text(searchPlaylist - .error?[searchPlaylist.pageParams.last]) - else - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - scrollbarOrientation: - breakpoint > Breakpoints.md - ? ScrollbarOrientation.bottom - : ScrollbarOrientation.top, - controller: playlistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: playlistController, - child: Row( - children: [ - ...playlists.mapIndexed( - (i, playlist) { - if (i == playlists.length - 1 && - searchPlaylist.hasNextPage) { - return Waypoint( - onEnter: () { - searchPlaylist.fetchNextPage(); - }, - child: - const ShimmerPlaybuttonCard( - count: 1), - ); - } - return PlaylistCard(playlist); - }, - ), - ], - ), - ), - ), - ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - Text( - "Artists", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - if (searchArtist.isLoading && - !searchArtist.isFetchingNextPage) - const CircularProgressIndicator() - else if (searchArtist.hasError) - Text(searchArtist - .error?[searchArtist.pageParams.last]) - else - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: artistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: artistController, - child: Row( - children: [ - ...artists.mapIndexed( - (i, artist) { - if (i == artists.length - 1 && - searchArtist.hasNextPage) { - return Waypoint( - onEnter: () { - searchArtist.fetchNextPage(); - }, - child: - const ShimmerPlaybuttonCard( - count: 1), - ); - } - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 15), - child: ArtistCard(artist), - ); - }, - ), - ], - ), - ), - ), - ), - const SizedBox(height: 20), - if (albums.isNotEmpty) - Text( - "Albums", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - if (searchAlbum.isLoading && - !searchAlbum.isFetchingNextPage) - const CircularProgressIndicator() - else if (searchAlbum.hasError) - Text( - searchAlbum.error?[searchAlbum.pageParams.last]) - else - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: albumController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: albumController, - child: Row( - children: [ - ...albums.mapIndexed((i, album) { - if (i == albums.length - 1 && - searchAlbum.hasNextPage) { - return Waypoint( - onEnter: () { - searchAlbum.fetchNextPage(); - }, - child: const ShimmerPlaybuttonCard( - count: 1), - ); - } - return AlbumCard( - TypeConversionUtils - .simpleAlbum_X_Album( - album, - ), - ); - }), - ], - ), - ), - ), - ), - ], - ), + child: PlatformScaffold( + appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null, + body: auth.isAnonymous + ? const AnonymousFallback() + : Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + child: PlatformTextField( + onChanged: (value) { + ref.read(searchTermStateProvider.notifier).state = + value; + }, + prefixIcon: Icons.search_rounded, + prefixIconColor: PlatformProperty.only( + ios: + PlatformTheme.of(context).textTheme?.caption?.color, + other: null, + ).resolve(platform!), + placeholder: "Search...", + onSubmitted: (value) { + onSearch(); + }, ), ), - ); - }, - ) - ], - ), + HookBuilder( + builder: (context) { + Playback playback = ref.watch(playbackProvider); + List albums = []; + List artists = []; + List tracks = []; + List playlists = []; + final pages = [ + ...searchTrack.pages, + ...searchAlbum.pages, + ...searchPlaylist.pages, + ...searchArtist.pages, + ].expand((page) => page ?? []).toList(); + for (MapEntry page in pages.asMap().entries) { + for (var item in page.value.items ?? []) { + if (item is AlbumSimple) { + albums.add(item); + } else if (item is PlaylistSimple) { + playlists.add(item); + } else if (item is Artist) { + artists.add(item); + } else if (item is Track) { + tracks.add(item); + } + } + } + return Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (tracks.isNotEmpty) + PlatformText.headline("Songs"), + if (searchTrack.isLoading && + !searchTrack.isFetchingNextPage) + const PlatformCircularProgressIndicator() + else if (searchTrack.hasError) + PlatformText(searchTrack + .error?[searchTrack.pageParams.last]) + else + ...tracks.asMap().entries.map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return TrackTile( + playback, + track: track, + duration: duration, + isActive: + playback.track?.id == track.value.id, + onTrackPlayButtonPressed: + (currentTrack) async { + var isPlaylistPlaying = + playback.playlist?.id != null && + playback.playlist?.id == + currentTrack.id; + if (!isPlaylistPlaying) { + playback.playPlaylist( + CurrentPlaylist( + tracks: [currentTrack], + id: currentTrack.id!, + name: currentTrack.name!, + thumbnail: TypeConversionUtils + .image_X_UrlString( + currentTrack.album?.images, + placeholder: + ImagePlaceholder.albumArt, + ), + ), + ); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != + playback.track?.id) { + playback.play(currentTrack); + } + }, + ); + }), + if (searchTrack.hasNextPage && + tracks.isNotEmpty) + Center( + child: PlatformTextButton( + onPressed: searchTrack.isFetchingNextPage + ? null + : () => searchTrack.fetchNextPage(), + child: searchTrack.isFetchingNextPage + ? const PlatformCircularProgressIndicator() + : const PlatformText("Load more"), + ), + ), + if (playlists.isNotEmpty) + PlatformText.headline("Playlists"), + const SizedBox(height: 10), + if (searchPlaylist.isLoading && + !searchPlaylist.isFetchingNextPage) + const PlatformCircularProgressIndicator() + else if (searchPlaylist.hasError) + PlatformText(searchPlaylist + .error?[searchPlaylist.pageParams.last]) + else + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + scrollbarOrientation: + breakpoint > Breakpoints.md + ? ScrollbarOrientation.bottom + : ScrollbarOrientation.top, + controller: playlistController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: playlistController, + child: Row( + children: [ + ...playlists.mapIndexed( + (i, playlist) { + if (i == playlists.length - 1 && + searchPlaylist + .hasNextPage) { + return Waypoint( + onEnter: () { + searchPlaylist + .fetchNextPage(); + }, + child: + const ShimmerPlaybuttonCard( + count: 1), + ); + } + return PlaylistCard(playlist); + }, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + if (artists.isNotEmpty) + PlatformText.headline("Artists"), + const SizedBox(height: 10), + if (searchArtist.isLoading && + !searchArtist.isFetchingNextPage) + const PlatformCircularProgressIndicator() + else if (searchArtist.hasError) + PlatformText(searchArtist + .error?[searchArtist.pageParams.last]) + else + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: artistController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: artistController, + child: Row( + children: [ + ...artists.mapIndexed( + (i, artist) { + if (i == artists.length - 1 && + searchArtist.hasNextPage) { + return Waypoint( + onEnter: () { + searchArtist + .fetchNextPage(); + }, + child: + const ShimmerPlaybuttonCard( + count: 1), + ); + } + return Container( + margin: const EdgeInsets + .symmetric( + horizontal: 15), + child: ArtistCard(artist), + ); + }, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 20), + if (albums.isNotEmpty) + PlatformText( + "Albums", + style: + Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 10), + if (searchAlbum.isLoading && + !searchAlbum.isFetchingNextPage) + const PlatformCircularProgressIndicator() + else if (searchAlbum.hasError) + PlatformText(searchAlbum + .error?[searchAlbum.pageParams.last]) + else + ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: albumController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: albumController, + child: Row( + children: [ + ...albums.mapIndexed((i, album) { + if (i == albums.length - 1 && + searchAlbum.hasNextPage) { + return Waypoint( + onEnter: () { + searchAlbum.fetchNextPage(); + }, + child: + const ShimmerPlaybuttonCard( + count: 1), + ); + } + return AlbumCard( + TypeConversionUtils + .simpleAlbum_X_Album( + album, + ), + ); + }), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ) + ], + ), ), ); } diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart index 6a3eaeb1..789c413a 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,9 +30,12 @@ class About extends HookWidget { version: "2.5.0", ); - return ListTile( + return PlatformListTile( leading: const Icon(Icons.info_outline_rounded), - title: const Text("About Spotube"), + title: PlatformText( + "About Spotube", + style: PlatformTextTheme.of(context).body, + ), onTap: () { showAboutDialog( context: context, @@ -44,7 +48,7 @@ class About extends HookWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ - Text("Author: "), + PlatformText("Author: "), Hyperlink( "Kingkor Roy Tirtho", "https://github.com/KRTirtho", @@ -59,12 +63,12 @@ class About extends HookWidget { "💚 Sponsor/Donate 💚", "https://opencollective.com/spotube", ), - Text(" • "), + PlatformText(" • "), Hyperlink( "BSD-4-Clause LICENSE", "https://github.com/KRTirtho/spotube/blob/master/LICENSE", ), - Text(" • "), + PlatformText(" • "), Hyperlink( "Bug Report", "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", @@ -72,7 +76,8 @@ class About extends HookWidget { ], ), const SizedBox(height: 10), - const Center(child: Text("© Spotube 2022. All rights reserved")) + const Center( + child: PlatformText("© Spotube 2022. All rights reserved")) ]); }, ); diff --git a/lib/components/Settings/ColorSchemePickerDialog.dart b/lib/components/Settings/ColorSchemePickerDialog.dart index f96620d2..d965f0d4 100644 --- a/lib/components/Settings/ColorSchemePickerDialog.dart +++ b/lib/components/Settings/ColorSchemePickerDialog.dart @@ -1,6 +1,8 @@ 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/components/Home/Sidebar.dart'; import 'package:spotube/provider/UserPreferences.dart'; final highContrast = MaterialColor( @@ -65,16 +67,11 @@ class ColorSchemePickerDialog extends HookConsumerWidget { }, ).key); - return AlertDialog( + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), title: Text("Pick ${schemeType.name} color scheme"), - actions: [ - TextButton( - child: const Text("Cancel"), - onPressed: () { - Navigator.pop(context); - }, - ), - ElevatedButton( + primaryActions: [ + PlatformFilledButton( child: const Text("Save"), onPressed: () { switch (schemeType) { @@ -90,6 +87,15 @@ class ColorSchemePickerDialog extends HookConsumerWidget { }, ) ], + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, + child: const Text("Cancel"), + onPressed: () { + Navigator.pop(context); + }, + ), + ], content: SizedBox( height: 200, width: 400, diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index b32307b6..5efd3efd 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -4,11 +4,13 @@ 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'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/main.dart'; import 'package:spotube/models/SpotifyMarkets.dart'; import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/provider/Auth.dart'; @@ -26,9 +28,7 @@ class Settings extends HookConsumerWidget { final Auth auth = ref.watch(authProvider); final pickColorScheme = useCallback((ColorSchemeType schemeType) { - return () => showDialog( - context: context, - builder: (context) { + return () => showPlatformAlertDialog(context, builder: (context) { return ColorSchemePickerDialog( schemeType: schemeType, ); @@ -48,12 +48,10 @@ class Settings extends HookConsumerWidget { ); return SafeArea( - child: Scaffold( + child: PlatformScaffold( appBar: PageWindowTitleBar( - center: Text( - "Settings", - style: Theme.of(context).textTheme.headline5, - ), + center: PlatformText.headline("Settings"), + centerTitle: true, ), body: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -63,10 +61,11 @@ class Settings extends HookConsumerWidget { constraints: const BoxConstraints(maxWidth: 1366), child: ListView( children: [ - const Text( + PlatformText( " Account", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), if (auth.isAnonymous) AdaptiveListTile( @@ -88,7 +87,7 @@ class Settings extends HookConsumerWidget { ), ), ), - trailing: (context, update) => ElevatedButton( + trailing: (context, update) => PlatformFilledButton( onPressed: () { GoRouter.of(context).push("/login"); }, @@ -99,15 +98,16 @@ class Settings extends HookConsumerWidget { ), ), ), - child: Text("Connect with Spotify".toUpperCase()), + child: PlatformText( + "Connect with Spotify".toUpperCase()), ), ), if (auth.isLoggedIn) Builder(builder: (context) { Auth auth = ref.watch(authProvider); - return ListTile( + return PlatformListTile( leading: const Icon(Icons.logout_rounded), - title: const SizedBox( + title: SizedBox( height: 50, width: 180, child: Align( @@ -115,10 +115,11 @@ class Settings extends HookConsumerWidget { child: AutoSizeText( "Log out of this account", maxLines: 1, + style: PlatformTextTheme.of(context).body, ), ), ), - trailing: ElevatedButton( + trailing: PlatformFilledButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red), @@ -129,39 +130,41 @@ class Settings extends HookConsumerWidget { auth.logout(); GoRouter.of(context).pop(); }, - child: const Text("Logout"), + child: const PlatformText("Logout"), ), ); }), - const Text( + PlatformText( " Appearance", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), AdaptiveListTile( leading: const Icon(Icons.dashboard_rounded), - title: const Text("Layout Mode"), - subtitle: const Text( + title: const PlatformText("Layout Mode"), + subtitle: const PlatformText( "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 PlatformText( "Adaptive", ), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: LayoutMode.compact, - child: Text( + child: const PlatformText( "Compact", ), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: LayoutMode.extended, - child: Text("Extended"), + child: const PlatformText("Extended"), ), ], onChanged: (value) { @@ -174,25 +177,22 @@ class Settings extends HookConsumerWidget { ), AdaptiveListTile( leading: const Icon(Icons.dark_mode_outlined), - title: const Text("Theme"), - trailing: (context, update) => DropdownButton( + title: const PlatformText("Theme"), + trailing: (context, update) => + PlatformDropDownMenu( value: preferences.themeMode, - items: const [ - DropdownMenuItem( + items: [ + PlatformDropDownMenuItem( value: ThemeMode.dark, - child: Text( - "Dark", - ), + child: const PlatformText("Dark"), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: ThemeMode.light, - child: Text( - "Light", - ), + child: const PlatformText("Light"), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: ThemeMode.system, - child: Text("System"), + child: const PlatformText("System"), ), ], onChanged: (value) { @@ -203,9 +203,45 @@ class Settings extends HookConsumerWidget { }, ), ), - ListTile( + AdaptiveListTile( + leading: const Icon(Icons.ad_units_rounded), + title: const PlatformText("Mimic Platform"), + trailing: (context, update) => + PlatformDropDownMenu( + value: Spotube.of(context).appPlatform, + items: [ + PlatformDropDownMenuItem( + value: TargetPlatform.android, + child: const PlatformText("Android (Material You)"), + ), + PlatformDropDownMenuItem( + value: TargetPlatform.iOS, + child: const PlatformText("iOS (Cupertino)"), + ), + PlatformDropDownMenuItem( + value: TargetPlatform.macOS, + child: const PlatformText("macOS (Aqua)"), + ), + PlatformDropDownMenuItem( + value: TargetPlatform.linux, + child: const PlatformText("Linux (GTK+Libadwaita)"), + ), + PlatformDropDownMenuItem( + value: TargetPlatform.windows, + child: const PlatformText("Windows 11 (Fluent UI)"), + ), + ], + onChanged: (value) { + if (value != null) { + Spotube.of(context).changePlatform(value); + update?.call(() {}); + } + }, + ), + ), + PlatformListTile( leading: const Icon(Icons.palette_outlined), - title: const Text("Accent Color Scheme"), + title: const PlatformText("Accent Color Scheme"), contentPadding: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, @@ -217,9 +253,9 @@ class Settings extends HookConsumerWidget { ), onTap: pickColorScheme(ColorSchemeType.accent), ), - ListTile( + PlatformListTile( leading: const Icon(Icons.format_color_fill_rounded), - title: const Text("Background Color Scheme"), + title: const PlatformText("Background Color Scheme"), contentPadding: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, @@ -231,38 +267,38 @@ 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, + title: const PlatformText("Rotating Album Art"), + trailing: PlatformSwitch( value: preferences.rotatingAlbumArt, onChanged: (state) { preferences.setRotatingAlbumArt(state); }, ), ), - const Text( + PlatformText( " Playback", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), AdaptiveListTile( leading: const Icon(Icons.multitrack_audio_rounded), - title: const Text("Audio Quality"), + title: const PlatformText("Audio Quality"), trailing: (context, update) => - DropdownButton( + PlatformDropDownMenu( value: preferences.audioQuality, - items: const [ - DropdownMenuItem( + items: [ + PlatformDropDownMenuItem( value: AudioQuality.high, - child: Text( + child: const PlatformText( "High", ), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: AudioQuality.low, - child: Text("Low"), + child: const PlatformText("Low"), ), ], onChanged: (value) { @@ -274,60 +310,54 @@ class Settings extends HookConsumerWidget { ), ), if (kIsMobile) - ListTile( + PlatformListTile( leading: const Icon(Icons.download_for_offline_rounded), - title: const Text( + title: const PlatformText( "Pre download and play", ), - subtitle: const Text( + subtitle: const PlatformText( "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( + title: const PlatformText( "Skip non-music segments (SponsorBlock)", ), - trailing: Switch.adaptive( - activeColor: Theme.of(context).primaryColor, + trailing: PlatformSwitch( value: preferences.skipSponsorSegments, onChanged: (state) { preferences.setSkipSponsorSegments(state); }, ), ), - const Text( + PlatformText( " Search", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), AdaptiveListTile( leading: const Icon(Icons.shopping_bag_rounded), - title: Text( - "Market Place", - style: Theme.of(context).textTheme.bodyText1, - ), - subtitle: Text( + title: const PlatformText("Market Place"), + subtitle: PlatformText.caption( "Recommendation Country", - style: Theme.of(context).textTheme.caption, ), trailing: (context, update) => ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 250), - child: DropdownButton( - isExpanded: true, + constraints: const BoxConstraints(maxWidth: 350), + child: PlatformDropDownMenu( value: preferences.recommendationMarket, items: spotifyMarkets .map( - (country) => (DropdownMenuItem( + (country) => (PlatformDropDownMenuItem( value: country.first, - child: Text(country.last), + child: PlatformText(country.last), )), ) .toList(), @@ -354,22 +384,19 @@ class Settings extends HookConsumerWidget { ), ), ), - subtitle: const Text("(Case sensitive)"), + subtitle: const PlatformText("(Case sensitive)"), 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); @@ -380,7 +407,7 @@ class Settings extends HookConsumerWidget { ), AdaptiveListTile( leading: const Icon(Icons.low_priority_rounded), - title: const SizedBox( + title: SizedBox( height: 50, width: 180, child: Align( @@ -388,28 +415,29 @@ class Settings extends HookConsumerWidget { child: AutoSizeText( "Track Match Algorithm", maxLines: 1, + style: PlatformTextTheme.of(context).body, ), ), ), trailing: (context, update) => - DropdownButton( + PlatformDropDownMenu( value: preferences.trackMatchAlgorithm, - items: const [ - DropdownMenuItem( + items: [ + PlatformDropDownMenuItem( value: SpotubeTrackMatchAlgorithm.authenticPopular, - child: Text( + child: const PlatformText( "Popular from Author", ), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: SpotubeTrackMatchAlgorithm.popular, - child: Text( + child: const PlatformText( "Accurately Popular", ), ), - DropdownMenuItem( + PlatformDropDownMenuItem( value: SpotubeTrackMatchAlgorithm.youtube, - child: Text("YouTube's Top choice"), + child: const PlatformText("YouTube's Top choice"), ), ], onChanged: (value) { @@ -420,36 +448,38 @@ class Settings extends HookConsumerWidget { }, ), ), - const Text( + PlatformText( " Downloads", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), - ListTile( + PlatformListTile( leading: const Icon(Icons.file_download_outlined), - title: const Text("Download Location"), - subtitle: Text(preferences.downloadLocation), - trailing: ElevatedButton( + title: const PlatformText("Download Location"), + subtitle: PlatformText(preferences.downloadLocation), + 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, + title: const PlatformText( + "Download lyrics along with the Track"), + trailing: PlatformSwitch( value: preferences.saveTrackLyrics, onChanged: (state) { preferences.setSaveTrackLyrics(state); }, ), ), - const Text( + PlatformText( " About", - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: PlatformTextTheme.of(context) + .headline + ?.copyWith(fontWeight: FontWeight.bold), ), AdaptiveListTile( leading: const Icon( @@ -471,13 +501,14 @@ class Settings extends HookConsumerWidget { ), ), ), - trailing: (context, update) => ElevatedButton.icon( - icon: const Icon(Icons.favorite_outline_rounded), - label: const Text("Please Sponsor/Donate"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[100], - foregroundColor: Colors.pinkAccent, - padding: const EdgeInsets.all(15), + trailing: (context, update) => PlatformFilledButton( + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.red[100]), + foregroundColor: + const MaterialStatePropertyAll(Colors.pinkAccent), + padding: const MaterialStatePropertyAll( + EdgeInsets.all(15)), ), onPressed: () { launchUrlString( @@ -485,13 +516,20 @@ class Settings extends HookConsumerWidget { mode: LaunchMode.externalApplication, ); }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.favorite_outline_rounded), + SizedBox(width: 5), + PlatformText("Please Sponsor/Donate"), + ], + ), ), ), - ListTile( + PlatformListTile( leading: const Icon(Icons.update_rounded), - title: const Text("Check for Update"), - trailing: Switch.adaptive( - activeColor: Theme.of(context).primaryColor, + title: const PlatformText("Check for Update"), + 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 271c598d..48616d47 100644 --- a/lib/components/Shared/AdaptiveListTile.dart +++ b/lib/components/Shared/AdaptiveListTile.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:platform_ui/platform_ui.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; class AdaptiveListTile extends HookWidget { @@ -24,7 +26,7 @@ class AdaptiveListTile extends HookWidget { Widget build(BuildContext context) { final breakpoint = useBreakpoints(); - return ListTile( + return PlatformListTile( title: title, subtitle: subtitle, trailing: @@ -33,11 +35,13 @@ class AdaptiveListTile extends HookWidget { onTap: breakpoint.isLessThan(breakOn) ? () { onTap?.call(); - showDialog( - context: context, + showPlatformAlertDialog( + context, + barrierDismissible: true, builder: (context) { return StatefulBuilder(builder: (context, update) { - return AlertDialog( + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), title: title != null ? Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -49,7 +53,7 @@ class AdaptiveListTile extends HookWidget { Flexible(child: title!), ], ) - : null, + : Container(), content: trailing?.call(context, update), ); }); diff --git a/lib/components/Shared/AdaptivePopupMenuButton.dart b/lib/components/Shared/AdaptivePopupMenuButton.dart index dfa8b3dc..5dd34b3e 100644 --- a/lib/components/Shared/AdaptivePopupMenuButton.dart +++ b/lib/components/Shared/AdaptivePopupMenuButton.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:popover/popover.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; @@ -19,28 +20,30 @@ class Action extends StatelessWidget { @override Widget build(BuildContext context) { if (isExpanded != true) { - return Tooltip( - message: text.toStringShallow().split(",").last.replaceAll( - "\"", - "", - ), - child: IconButton( - icon: icon, - onPressed: onPressed, - ), + return PlatformIconButton( + icon: icon, + onPressed: onPressed, + tooltip: text is Text + ? (text as Text).data + : text.toStringShallow().split(",").last.replaceAll( + "\"", + "", + ), ); } - return TextButton.icon( + return PlatformTextButton( style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, padding: const EdgeInsets.all(20), ), - icon: icon, - label: Align( - alignment: Alignment.centerLeft, - child: text, - ), onPressed: onPressed, + child: Row( + children: [ + icon, + const SizedBox(width: 10), + text, + ], + ), ); } } @@ -59,7 +62,7 @@ class AdaptiveActions extends HookWidget { final breakpoint = useBreakpoints(); if (breakpoint.isLessThan(breakOn)) { - return IconButton( + return PlatformIconButton( icon: const Icon(Icons.more_horiz), onPressed: () { showPopover( diff --git a/lib/components/Shared/AnchorButton.dart b/lib/components/Shared/AnchorButton.dart index ede984e9..9660cfc1 100644 --- a/lib/components/Shared/AnchorButton.dart +++ b/lib/components/Shared/AnchorButton.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:platform_ui/platform_ui.dart'; class AnchorButton extends HookWidget { final String text; @@ -28,7 +29,7 @@ class AnchorButton extends HookWidget { onTap: onTap, child: MouseRegion( cursor: MaterialStateMouseCursor.clickable, - child: Text( + child: PlatformText( text, style: style.copyWith( decoration: diff --git a/lib/components/Shared/AnonymousFallback.dart b/lib/components/Shared/AnonymousFallback.dart index 3c8e1931..85aa9712 100644 --- a/lib/components/Shared/AnonymousFallback.dart +++ b/lib/components/Shared/AnonymousFallback.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -19,10 +20,10 @@ class AnonymousFallback extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("You're not logged in"), + const PlatformText("You're not logged in"), const SizedBox(height: 10), - ElevatedButton( - child: const Text("Login with Spotify"), + PlatformFilledButton( + child: const PlatformText("Login with Spotify"), onPressed: () => ServiceUtils.navigate(context, "/settings"), ) ], diff --git a/lib/components/Shared/DownloadConfirmationDialog.dart b/lib/components/Shared/DownloadConfirmationDialog.dart index 48c96a32..07ef294b 100644 --- a/lib/components/Shared/DownloadConfirmationDialog.dart +++ b/lib/components/Shared/DownloadConfirmationDialog.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:platform_ui/platform_ui.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; class DownloadConfirmationDialog extends StatelessWidget { @@ -6,22 +8,25 @@ class DownloadConfirmationDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return AlertDialog( - contentPadding: const EdgeInsets.all(15), - title: Row( - children: const [ - Text("Are you sure?"), - SizedBox(width: 10), - UniversalImage( - path: - "https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif", - height: 40, - width: 40, - ) - ], + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), + title: Padding( + padding: const EdgeInsets.all(15), + child: Row( + children: const [ + Text("Are you sure?"), + SizedBox(width: 10), + UniversalImage( + path: + "https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif", + height: 40, + width: 40, + ) + ], + ), ), - content: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), + content: Padding( + padding: const EdgeInsets.all(15), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -56,19 +61,22 @@ class DownloadConfirmationDialog extends StatelessWidget { ), ), ), - actions: [ - ElevatedButton( + primaryActions: [ + PlatformFilledButton( + style: const ButtonStyle( + foregroundColor: MaterialStatePropertyAll(Colors.white), + backgroundColor: MaterialStatePropertyAll(Colors.red), + ), + onPressed: () => Navigator.of(context).pop(true), + child: const Text("Accept"), + ) + ], + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, child: const Text("Decline"), onPressed: () => Navigator.of(context).pop(false), ), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(true), - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: Colors.red, - ), - child: const Text("Accept"), - ) ], ); } diff --git a/lib/components/Shared/HeartButton.dart b/lib/components/Shared/HeartButton.dart index 24954b34..6b87e6d9 100644 --- a/lib/components/Shared/HeartButton.dart +++ b/lib/components/Shared/HeartButton.dart @@ -3,6 +3,7 @@ import 'package:fl_query_hooks/fl_query_hooks.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:spotify/spotify.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/provider/Auth.dart'; @@ -32,7 +33,7 @@ class HeartButton extends ConsumerWidget { if (!auth.isLoggedIn) return Container(); - return IconButton( + return PlatformIconButton( tooltip: tooltip, icon: Icon( icon ?? @@ -121,7 +122,7 @@ class TrackHeartButton extends HookConsumerWidget { ); final toggler = useTrackToggleLike(track, ref); if (toggler.item3.isLoading || !toggler.item3.hasData) { - return const CircularProgressIndicator(); + return const PlatformCircularProgressIndicator(); } return HeartButton( @@ -181,7 +182,8 @@ class PlaylistHeartButton extends HookConsumerWidget { titleImage, ).dominantColor; - if (me.isLoading || !me.hasData) return const CircularProgressIndicator(); + if (me.isLoading || !me.hasData) + return const PlatformCircularProgressIndicator(); return HeartButton( isLiked: isLikedQuery.data ?? false, @@ -235,7 +237,8 @@ class AlbumHeartButton extends HookConsumerWidget { }, ); - if (me.isLoading || !me.hasData) return const CircularProgressIndicator(); + if (me.isLoading || !me.hasData) + return const PlatformCircularProgressIndicator(); return HeartButton( isLiked: isLiked, diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index cc39f499..58235126 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -1,130 +1,35 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotube/utils/platform.dart'; -class TitleBarActionButtons extends StatelessWidget { - final Color? color; - const TitleBarActionButtons({ - Key? key, - this.color, - }) : super(key: key); +class PageWindowTitleBar extends PlatformAppBar { + PageWindowTitleBar({ + super.backgroundColor, + List? actions, + super.actionsIconTheme, + super.automaticallyImplyLeading = false, + super.centerTitle, + super.foregroundColor, + super.key, + super.leading, + super.leadingWidth, + Widget? center, + super.titleSpacing, + super.titleTextStyle, + super.titleWidth, + super.toolbarOpacity, + super.toolbarTextStyle, + }) : super( + actions: [ + ...?actions, + if (!kIsMacOS && !kIsMobile) const PlatformWindowButtons(), + ], + title: center, + ); @override Widget build(BuildContext context) { - return TextButtonTheme( - data: TextButtonThemeData( - style: ButtonStyle( - splashFactory: NoSplash.splashFactory, - shape: MaterialStateProperty.all(const RoundedRectangleBorder()), - overlayColor: MaterialStateProperty.all(Colors.black12), - padding: MaterialStateProperty.all(EdgeInsets.zero), - minimumSize: MaterialStateProperty.all(const Size(50, 40)), - maximumSize: MaterialStateProperty.all(const Size(50, 40)), - ), - ), - child: IconTheme( - data: const IconThemeData(size: 16), - child: Row( - children: [ - TextButton( - onPressed: () { - appWindow.minimize(); - }, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - Theme.of(context).iconTheme.color), - ), - child: Icon( - Icons.minimize_rounded, - color: color, - )), - TextButton( - onPressed: () async { - appWindow.maximizeOrRestore(); - }, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - Theme.of(context).iconTheme.color), - ), - child: Icon( - Icons.crop_square_rounded, - color: color, - )), - TextButton( - onPressed: () { - appWindow.close(); - }, - style: ButtonStyle( - foregroundColor: MaterialStateProperty.all( - color ?? Theme.of(context).iconTheme.color), - overlayColor: MaterialStateProperty.all(Colors.redAccent), - ), - child: const Icon( - Icons.close_rounded, - )), - ], - ), - ), - ); - } -} - -class PageWindowTitleBar extends StatelessWidget - implements PreferredSizeWidget { - final Widget? leading; - final Widget? center; - final Color? backgroundColor; - final Color? foregroundColor; - final Size? _preferredSize; - const PageWindowTitleBar({ - Key? key, - Size? preferredSize, - this.leading, - this.center, - this.backgroundColor, - this.foregroundColor, - }) : _preferredSize = preferredSize, - super(key: key); - - static Size get staticPreferredSize => Size.fromHeight( - (kIsDesktop ? appWindow.titleBarHeight : 35), - ); - - @override - Size get preferredSize => _preferredSize ?? staticPreferredSize; - - @override - Widget build(BuildContext context) { - if (kIsMobile) { - return PreferredSize( - preferredSize: const Size.fromHeight(300), - child: Container( - color: backgroundColor, - child: Row( - children: [ - if (leading != null) leading!, - Expanded(child: Center(child: center)), - ], - ), - ), - ); - } - return WindowTitleBarBox( - child: Container( - color: backgroundColor, - child: Row( - children: [ - if (kIsMacOS) - SizedBox( - width: MediaQuery.of(context).size.width * 0.045, - ), - if (leading != null) leading!, - Expanded(child: MoveWindow(child: Center(child: center))), - if (!kIsMacOS && !kIsMobile) - TitleBarActionButtons(color: foregroundColor) - ], - ), - ), - ); + return MoveWindow(child: super.build(context)); } } diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index 772319ce..cb4cf3e3 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.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'; +import 'package:spotube/hooks/usePlatformProperty.dart'; -class PlaybuttonCard extends StatelessWidget { +class PlaybuttonCard extends HookWidget { final void Function()? onTap; final void Function()? onPlaybuttonPressed; final String? description; @@ -26,26 +29,65 @@ class PlaybuttonCard extends StatelessWidget { @override Widget build(BuildContext context) { + final backgroundColor = PlatformTheme.of(context).secondaryBackgroundColor; + + final boxShadow = usePlatformProperty( + (context) => PlatformProperty( + android: BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor, + ), + ios: null, + macos: null, + linux: BoxShadow( + blurRadius: 6, + color: Theme.of(context).shadowColor.withOpacity(0.3), + ), + windows: null, + ), + ); + + final splash = usePlatformProperty( + (context) => PlatformProperty.only( + android: InkRipple.splashFactory, + other: NoSplash.splashFactory, + ), + ); + + final iconBgColor = PlatformTheme.of(context).primaryColor; + return Container( margin: margin, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), + splashFactory: splash, + highlightColor: Colors.black12, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200), child: HoverBuilder(builder: (context, isHovering) { return Ink( decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), + color: backgroundColor, + borderRadius: BorderRadius.circular( + [TargetPlatform.windows, TargetPlatform.linux] + .contains(platform) + ? 5 + : 8, + ), boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor, - ) + if (boxShadow != null) boxShadow, ], + border: [TargetPlatform.windows, TargetPlatform.macOS] + .contains(platform) + ? Border.all( + color: PlatformTheme.of(context).borderColor ?? + Colors.transparent, + width: 1, + ) + : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -53,13 +95,23 @@ class PlaybuttonCard extends StatelessWidget { // thumbnail of the playlist Stack( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: UniversalImage( - path: imageUrl, - width: 200, - placeholder: (context, url) => - Image.asset("assets/placeholder.png"), + Padding( + padding: EdgeInsets.all( + platform == TargetPlatform.windows ? 5 : 0, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + [TargetPlatform.windows, TargetPlatform.linux] + .contains(platform) + ? 5 + : 8, + ), + child: UniversalImage( + path: imageUrl, + width: 200, + placeholder: (context, url) => + Image.asset("assets/placeholder.png"), + ), ), ), Positioned.directional( @@ -67,27 +119,32 @@ class PlaybuttonCard extends StatelessWidget { bottom: 10, end: 5, child: Builder(builder: (context) { - return ElevatedButton( - onPressed: onPlaybuttonPressed, - style: ButtonStyle( - shape: MaterialStateProperty.all( - const CircleBorder(), - ), - padding: MaterialStateProperty.all( - const EdgeInsets.all(16), - ), + return Container( + decoration: BoxDecoration( + color: iconBgColor, + shape: BoxShape.circle, + ), + child: PlatformIconButton( + onPressed: onPlaybuttonPressed, + backgroundColor: + PlatformTheme.of(context).primaryColor, + hoverColor: PlatformTheme.of(context) + .primaryColor + ?.withOpacity(0.5), + icon: isLoading + ? const SizedBox( + height: 23, + width: 23, + child: + PlatformCircularProgressIndicator(), + ) + : Icon( + isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: Colors.white, + ), ), - child: isLoading - ? const SizedBox( - height: 23, - width: 23, - child: CircularProgressIndicator(), - ) - : Icon( - isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), ); }), ) @@ -117,13 +174,7 @@ class PlaybuttonCard extends StatelessWidget { height: 30, child: SpotubeMarqueeText( text: description!, - style: TextStyle( - fontSize: 13, - color: Theme.of(context) - .textTheme - .headline4 - ?.color, - ), + style: PlatformTextTheme.of(context).caption, isHovering: isHovering, ), ), diff --git a/lib/components/Shared/ReplaceDownloadedFileDialog.dart b/lib/components/Shared/ReplaceDownloadedFileDialog.dart index 6b6591b7..883b1365 100644 --- a/lib/components/Shared/ReplaceDownloadedFileDialog.dart +++ b/lib/components/Shared/ReplaceDownloadedFileDialog.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; final replaceDownloadedFileState = StateProvider((ref) => null); @@ -13,7 +15,8 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget { Widget build(BuildContext context, ref) { final groupValue = ref.watch(replaceDownloadedFileState); - return AlertDialog( + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), title: Text("Track ${track.name} Already Exists"), content: Column( mainAxisSize: MainAxisSize.min, @@ -47,20 +50,23 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget { ), ], ), - actions: [ - TextButton( - child: const Text("No"), - onPressed: () { - Navigator.pop(context, false); - }, - ), - TextButton( + primaryActions: [ + PlatformFilledButton( child: const Text("Yes"), onPressed: () { Navigator.pop(context, true); }, ) ], + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, + child: const Text("No"), + onPressed: () { + Navigator.pop(context, false); + }, + ), + ], ); } } diff --git a/lib/components/Shared/SortTracksDropdown.dart b/lib/components/Shared/SortTracksDropdown.dart index 2b503613..0b42dd2a 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/components/Shared/SpotubeMarqueeText.dart b/lib/components/Shared/SpotubeMarqueeText.dart index 692a0027..65583b32 100644 --- a/lib/components/Shared/SpotubeMarqueeText.dart +++ b/lib/components/Shared/SpotubeMarqueeText.dart @@ -26,12 +26,12 @@ class SpotubeMarqueeText extends HookWidget { return AutoSizeText( text, minFontSize: 13, - style: style, + style: DefaultTextStyle.of(context).style.merge(style), maxLines: 1, overflowReplacement: Marquee( key: uKey.value, text: text, - style: style, + style: DefaultTextStyle.of(context).style.merge(style), scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, blankSpace: 40.0, diff --git a/lib/components/Shared/TrackCollectionView.dart b/lib/components/Shared/TrackCollectionView.dart index 85e75266..d1025372 100644 --- a/lib/components/Shared/TrackCollectionView.dart +++ b/lib/components/Shared/TrackCollectionView.dart @@ -2,6 +2,7 @@ import 'package:fl_query/fl_query.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/LoaderShimmers/ShimmerTrackTile.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; @@ -59,7 +60,7 @@ class TrackCollectionView extends HookConsumerWidget { final List buttons = [ if (showShare) - IconButton( + PlatformIconButton( icon: Icon( Icons.share_rounded, color: color?.titleTextColor, @@ -71,13 +72,9 @@ class TrackCollectionView extends HookConsumerWidget { // play playlist Container( margin: const EdgeInsets.symmetric(vertical: 10), - child: ElevatedButton( + child: PlatformFilledButton( style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Theme.of(context).primaryColor), - shape: MaterialStateProperty.all( - const CircleBorder(), - ), + shape: MaterialStateProperty.all(const CircleBorder()), ), onPressed: tracksSnapshot.data != null ? onPlay : null, child: Icon( @@ -112,14 +109,12 @@ class TrackCollectionView extends HookConsumerWidget { }, [collapsed.value]); return SafeArea( - child: Scaffold( + child: PlatformScaffold( appBar: kIsDesktop ? PageWindowTitleBar( backgroundColor: color?.color, foregroundColor: color?.titleTextColor, - leading: Row( - children: [BackButton(color: color?.titleTextColor)], - ), + leading: PlatformBackButton(color: color?.titleTextColor), ) : null, body: CustomScrollView( @@ -131,16 +126,19 @@ class TrackCollectionView extends HookConsumerWidget { pinned: true, expandedHeight: 400, automaticallyImplyLeading: kIsMobile, + leading: kIsMobile + ? PlatformBackButton(color: color?.titleTextColor) + : null, iconTheme: IconThemeData(color: color?.titleTextColor), primary: true, backgroundColor: color?.color, title: collapsed.value - ? Text( + ? PlatformText.headline( title, - style: Theme.of(context).textTheme.headline4?.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, - ), + style: TextStyle( + color: color?.titleTextColor, + fontWeight: FontWeight.w600, + ), ) : null, flexibleSpace: LayoutBuilder(builder: (context, constrains) { @@ -158,6 +156,7 @@ class TrackCollectionView extends HookConsumerWidget { ), ), child: Material( + textStyle: PlatformTheme.of(context).textTheme!.body!, type: MaterialType.transparency, child: Padding( padding: const EdgeInsets.symmetric( @@ -191,25 +190,19 @@ class TrackCollectionView extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( + PlatformText.headline( title, - style: Theme.of(context) - .textTheme - .headline4 - ?.copyWith( - color: color?.titleTextColor, - fontWeight: FontWeight.w600, - ), + style: TextStyle( + color: color?.titleTextColor, + fontWeight: FontWeight.w600, + ), ), if (description != null) - Text( + PlatformText( description!, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: color?.bodyTextColor, - ), + style: TextStyle( + color: color?.bodyTextColor, + ), maxLines: 2, overflow: TextOverflow.fade, ), @@ -235,7 +228,7 @@ class TrackCollectionView extends HookConsumerWidget { } else if (tracksSnapshot.hasError && tracksSnapshot.isError) { return SliverToBoxAdapter( - child: Text("Error ${tracksSnapshot.error}")); + child: PlatformText("Error ${tracksSnapshot.error}")); } final tracks = tracksSnapshot.data!; diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index 88f6bd80..232faa56 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -2,7 +2,9 @@ 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:platform_ui/platform_ui.dart'; import 'package:spotify/spotify.dart' hide Image; +import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/LinkText.dart'; @@ -73,7 +75,7 @@ class TrackTile extends HookConsumerWidget { SnackBar( width: 300, behavior: SnackBarBehavior.floating, - content: Text( + content: PlatformText( "Copied $data to clipboard", textAlign: TextAlign.center, ), @@ -83,81 +85,78 @@ class TrackTile extends HookConsumerWidget { } Future actionAddToPlaylist() async { - showDialog( - context: context, - builder: (context) { - return FutureBuilder>( - future: spotify.playlists.me.all().then((playlists) async { - final me = await spotify.me.get(); - return playlists.where((playlist) => - playlist.owner?.id != null && - playlist.owner!.id == me.id); - }), - builder: (context, snapshot) { - return HookBuilder(builder: (context) { - final playlistsCheck = useState({}); - return AlertDialog( - title: Text( - "Add `${track.value.name}` to following Playlists"), - titleTextStyle: - Theme.of(context).textTheme.bodyText1?.copyWith( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - actions: [ - TextButton( - child: const Text("Cancel"), - onPressed: () => Navigator.pop(context), - ), - ElevatedButton( - child: const Text("Add"), - onPressed: () async { - final selectedPlaylists = playlistsCheck - .value.entries - .where((entry) => entry.value) - .map((entry) => entry.key); + showPlatformAlertDialog(context, builder: (context) { + return FutureBuilder>( + future: spotify.playlists.me.all().then((playlists) async { + final me = await spotify.me.get(); + return playlists.where((playlist) => + playlist.owner?.id != null && playlist.owner!.id == me.id); + }), + builder: (context, snapshot) { + return HookBuilder(builder: (context) { + final playlistsCheck = useState({}); + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), + title: PlatformText( + "Add `${track.value.name}` to following Playlists", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + secondaryActions: [ + PlatformFilledButton( + isSecondary: true, + child: const PlatformText("Cancel"), + onPressed: () => Navigator.pop(context), + ), + ], + primaryActions: [ + PlatformFilledButton( + child: const PlatformText("Add"), + onPressed: () async { + final selectedPlaylists = playlistsCheck.value.entries + .where((entry) => entry.value) + .map((entry) => entry.key); - await Future.wait( - selectedPlaylists.map( - (playlistId) => spotify.playlists - .addTrack(track.value.uri!, playlistId), - ), - ).then((_) => Navigator.pop(context)); - }, - ) - ], - content: SizedBox( - height: 300, - width: 300, - child: !snapshot.hasData - ? const Center( - child: CircularProgressIndicator.adaptive()) - : ListView.builder( - shrinkWrap: true, - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - final playlist = - snapshot.data!.elementAt(index); - return CheckboxListTile( - title: Text(playlist.name!), - controlAffinity: - ListTileControlAffinity.leading, - value: playlistsCheck.value[playlist.id] ?? - false, - onChanged: (val) { - playlistsCheck.value = { - ...playlistsCheck.value, - playlist.id!: val == true - }; - }, - ); + await Future.wait( + selectedPlaylists.map( + (playlistId) => spotify.playlists + .addTrack(track.value.uri!, playlistId), + ), + ).then((_) => Navigator.pop(context)); + }, + ) + ], + content: SizedBox( + height: 300, + width: 300, + child: !snapshot.hasData + ? const Center( + child: PlatformCircularProgressIndicator()) + : ListView.builder( + shrinkWrap: true, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final playlist = snapshot.data!.elementAt(index); + return PlatformCheckbox( + label: PlatformText(playlist.name!), + value: + playlistsCheck.value[playlist.id] ?? false, + onChanged: (val) { + playlistsCheck.value = { + ...playlistsCheck.value, + playlist.id!: val == true + }; }, - ), - ), - ); - }); - }); - }); + ); + }, + ), + ), + ); + }); + }); + }); } final String thumbnailUrl = TypeConversionUtils.image_X_UrlString( @@ -178,10 +177,11 @@ class TrackTile extends HookConsumerWidget { ), child: Material( type: MaterialType.transparency, + textStyle: PlatformTheme.of(context).textTheme!.body!, child: Row( children: [ if (showCheck) - Checkbox( + PlatformCheckbox( value: isChecked, onChanged: (s) => onCheckChange?.call(s), ) @@ -190,7 +190,7 @@ class TrackTile extends HookConsumerWidget { height: 20, width: 25, child: Center( - child: Text((track.key + 1).toString()), + child: PlatformText((track.key + 1).toString()), ), ), Padding( @@ -214,23 +214,29 @@ class TrackTile extends HookConsumerWidget { ), ), ), - IconButton( - icon: Icon( - playback.track?.id != null && - playback.track?.id == track.value.id - ? Icons.pause_circle_rounded - : Icons.play_circle_rounded, - color: Theme.of(context).primaryColor, - ), - onPressed: () => onTrackPlayButtonPressed?.call( - track.value, + Padding( + padding: const EdgeInsets.all(8.0).copyWith(left: 0), + child: PlatformIconButton( + icon: Icon( + playback.track?.id != null && + playback.track?.id == track.value.id + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + color: Colors.white, + ), + backgroundColor: PlatformTheme.of(context).primaryColor, + hoverColor: + PlatformTheme.of(context).primaryColor?.withOpacity(0.5), + onPressed: () => onTrackPlayButtonPressed?.call( + track.value, + ), ), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + PlatformText( track.value.name ?? "", style: TextStyle( fontWeight: FontWeight.bold, @@ -239,7 +245,7 @@ class TrackTile extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), isReallyLocal - ? Text( + ? PlatformText( TypeConversionUtils.artists_X_String( track.value.artists ?? []), ) @@ -255,7 +261,7 @@ class TrackTile extends HookConsumerWidget { if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) Expanded( child: isReallyLocal - ? Text(track.value.album?.name ?? "") + ? PlatformText(track.value.album?.name ?? "") : LinkText( track.value.album!.name!, "/album/${track.value.album?.id}", @@ -265,7 +271,7 @@ class TrackTile extends HookConsumerWidget { ), if (!breakpoint.isSm) ...[ const SizedBox(width: 10), - Text(duration), + PlatformText(duration), ], const SizedBox(width: 10), if (!isReallyLocal) @@ -279,7 +285,7 @@ class TrackTile extends HookConsumerWidget { color: Colors.pink, ) : const Icon(Icons.favorite_border_rounded), - text: const Text("Save as favorite"), + text: const PlatformText("Save as favorite"), onPressed: () { toggler.item2.mutate(Tuple2(spotify, toggler.item1)); }, @@ -287,18 +293,18 @@ class TrackTile extends HookConsumerWidget { if (auth.isLoggedIn) Action( icon: const Icon(Icons.add_box_rounded), - text: const Text("Add To playlist"), + text: const PlatformText("Add To playlist"), onPressed: actionAddToPlaylist, ), if (userPlaylist && auth.isLoggedIn) Action( icon: const Icon(Icons.remove_circle_outline_rounded), - text: const Text("Remove from playlist"), + text: const PlatformText("Remove from playlist"), onPressed: actionRemoveFromPlaylist, ), Action( icon: const Icon(Icons.share_rounded), - text: const Text("Share"), + text: const PlatformText("Share"), onPressed: () { actionShare(track.value); }, diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index 60bbd55a..4dea4e04 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -1,6 +1,7 @@ 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:spotify/spotify.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart'; @@ -67,7 +68,7 @@ class TracksTableView extends HookConsumerWidget { if (heading != null) heading!, Row( children: [ - Checkbox( + PlatformCheckbox( value: selected.value.length == sortedTracks.length, onChanged: (checked) { if (!showCheck.value) showCheck.value = true; @@ -81,7 +82,7 @@ class TracksTableView extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.all(8.0), - child: Text( + child: PlatformText( "#", textAlign: TextAlign.center, style: tableHeadStyle, @@ -90,7 +91,7 @@ class TracksTableView extends HookConsumerWidget { Expanded( child: Row( children: [ - Text( + PlatformText( "Title", style: tableHeadStyle, overflow: TextOverflow.ellipsis, @@ -104,7 +105,7 @@ class TracksTableView extends HookConsumerWidget { Expanded( child: Row( children: [ - Text( + PlatformText( "Album", overflow: TextOverflow.ellipsis, style: tableHeadStyle, @@ -115,7 +116,7 @@ class TracksTableView extends HookConsumerWidget { ], if (!breakpoint.isSm) ...[ const SizedBox(width: 10), - Text("Time", style: tableHeadStyle), + PlatformText("Time", style: tableHeadStyle), const SizedBox(width: 10), ], SortTracksDropdown( @@ -126,32 +127,29 @@ class TracksTableView extends HookConsumerWidget { .state = value; }, ), - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( - enabled: selected.value.isNotEmpty, - value: "download", - child: Row( - children: [ - const Icon(Icons.file_download_outlined), - Text( - "Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}", - ), - ], - ), + PlatformPopupMenuButton( + items: [ + PlatformPopupMenuItem( + enabled: selected.value.isNotEmpty, + value: "download", + child: Row( + children: [ + const Icon(Icons.file_download_outlined), + PlatformText( + "Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}", + ), + ], ), - ]; - }, + ), + ], onSelected: (action) async { switch (action) { case "download": { - final isConfirmed = await showDialog( - context: context, - builder: (context) { - return const DownloadConfirmationDialog(); - }); + final isConfirmed = await showPlatformAlertDialog( + context, builder: (context) { + return const DownloadConfirmationDialog(); + }); if (isConfirmed != true) return; for (final selectedTrack in selectedTracks) { downloader.addToQueue(selectedTrack); @@ -163,6 +161,7 @@ class TracksTableView extends HookConsumerWidget { default: } }, + child: const Icon(Icons.more_vert), ), ], ), diff --git a/lib/hooks/usePlatformProperty.dart b/lib/hooks/usePlatformProperty.dart new file mode 100644 index 00000000..d074d574 --- /dev/null +++ b/lib/hooks/usePlatformProperty.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:platform_ui/platform_ui.dart'; + +T usePlatformProperty( + PlatformProperty Function(BuildContext context) getProperties) { + final context = useContext(); + + return getProperties(context).resolve(platform ?? Theme.of(context).platform); +} diff --git a/lib/hooks/useUpdateChecker.dart b/lib/hooks/useUpdateChecker.dart index 672bcb4b..448c9fb7 100644 --- a/lib/hooks/useUpdateChecker.dart +++ b/lib/hooks/useUpdateChecker.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; +import 'package:platform_ui/platform_ui.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; import 'package:spotube/hooks/usePackageInfo.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -51,41 +53,40 @@ void useUpdateChecker(WidgetRef ref) { final latestVersion = value.last; if (currentVersion == null || latestVersion == null) return; if (latestVersion <= currentVersion) return; - showDialog( - context: context, - builder: (context) { - const url = - "https://spotube.netlify.app/other-downloads/stable-downloads"; - return AlertDialog( - title: const Text("Spotube has an update"), - actions: [ - ElevatedButton( - child: const Text("Download Now"), - onPressed: () => download(url), - ), - ], - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + showPlatformAlertDialog(context, builder: (context) { + const url = + "https://spotube.netlify.app/other-downloads/stable-downloads"; + return PlatformAlertDialog( + macosAppIcon: Sidebar.brandLogo(), + title: const PlatformText("Spotube has an update"), + primaryActions: [ + PlatformFilledButton( + child: const Text("Download Now"), + onPressed: () => download(url), + ), + ], + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("Spotube v${value.last} has been released"), + Row( children: [ - Text("Spotube v${value.last} has been released"), - Row( - children: [ - const Text("Read the latest "), - AnchorButton( - "release notes", - style: const TextStyle(color: Colors.blue), - onTap: () => launchUrlString( - url, - mode: LaunchMode.externalApplication, - ), - ), - ], + const PlatformText("Read the latest "), + AnchorButton( + "release notes", + style: const TextStyle(color: Colors.blue), + onTap: () => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ), ), ], ), - ); - }); + ], + ), + ); + }); }); return null; }, [packageInfo, isCheckUpdateEnabled]); diff --git a/lib/main.dart b/lib/main.dart index ae6b3876..3d54019e 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'; @@ -106,8 +107,8 @@ void main() async { logger.v( "[onFileExists] download confirmation for ${track.name}", ); - return showDialog( - context: context, + return showPlatformAlertDialog( + context, builder: (_) => ReplaceDownloadedFileDialog(track: track), ).then((s) => s ?? false); @@ -140,6 +141,11 @@ class Spotube extends StatefulHookConsumerWidget { @override SpotubeState createState() => SpotubeState(); + + /// ↓↓ ADDED + /// InheritedWidget style accessor to our State object. + static SpotubeState of(BuildContext context) => + context.findAncestorStateOfType()!; } class SpotubeState extends ConsumerState with WidgetsBindingObserver { @@ -153,6 +159,11 @@ class SpotubeState extends ConsumerState with WidgetsBindingObserver { super.initState(); SharedPreferences.getInstance().then(((value) => localStorage = value)); WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + setState(() { + appPlatform = Theme.of(context).platform; + }); + }); } @override @@ -180,6 +191,13 @@ class SpotubeState extends ConsumerState with WidgetsBindingObserver { prevSize = appWindow.size; } + TargetPlatform appPlatform = TargetPlatform.android; + + void changePlatform(TargetPlatform targetPlatform) { + appPlatform = targetPlatform; + setState(() {}); + } + @override Widget build(BuildContext context) { final themeMode = @@ -198,57 +216,82 @@ class SpotubeState extends ConsumerState with WidgetsBindingObserver { }; }, []); - return MaterialApp.router( - routerConfig: router, + platform = appPlatform; + + 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, ), + linuxTheme: linuxTheme, + linuxDarkTheme: linuxDarkTheme, + iosTheme: themeMode == ThemeMode.dark ? iosDarkTheme : iosTheme, + windowsTheme: windowsTheme, + windowsDarkTheme: windowsDarkTheme, + macosTheme: macosTheme, + macosDarkTheme: macosDarkTheme, themeMode: themeMode, - shortcuts: { - ...WidgetsApp.defaultShortcuts, - const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref), - const SingleActivator(LogicalKeyboardKey.comma, control: true): + windowButtonConfig: kIsDesktop + ? PlatformWindowButtonConfig( + isMaximized: () => appWindow.isMaximized, + onClose: appWindow.close, + onRestore: appWindow.restore, + onMaximize: appWindow.maximize, + onMinimize: appWindow.minimize, + ) + : null, + 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/lib/themes/dark-theme.dart b/lib/themes/dark-theme.dart index 057b380d..42acc42e 100644 --- a/lib/themes/dark-theme.dart +++ b/lib/themes/dark-theme.dart @@ -72,7 +72,7 @@ ThemeData darkTheme({ ), dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]), cardColor: backgroundMaterialColor[800], - canvasColor: backgroundMaterialColor[900], + canvasColor: backgroundMaterialColor[800], listTileTheme: const ListTileThemeData(horizontalTitleGap: 0), checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty.resolveWith((states) { @@ -89,5 +89,8 @@ ThemeData darkTheme({ unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), ), + appBarTheme: AppBarTheme( + backgroundColor: backgroundMaterialColor[900], + ), ); } diff --git a/lib/themes/light-theme.dart b/lib/themes/light-theme.dart index 114a99ca..5503d5b4 100644 --- a/lib/themes/light-theme.dart +++ b/lib/themes/light-theme.dart @@ -1,5 +1,9 @@ +import 'package:adwaita/adwaita.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:spotube/extensions/ShimmerColorTheme.dart'; +import 'package:fluent_ui/fluent_ui.dart' as FluentUI; final materialWhite = MaterialColor(Colors.white.value, { 50: Colors.white, @@ -115,3 +119,49 @@ ThemeData lightTheme({ ), ); } + +final windowsTheme = FluentUI.ThemeData.light().copyWith( + buttonTheme: FluentUI.ButtonThemeData( + iconButtonStyle: FluentUI.ButtonStyle( + iconSize: FluentUI.ButtonState.all(20), + ), + ), +); +final windowsDarkTheme = FluentUI.ThemeData.dark().copyWith( + buttonTheme: FluentUI.ButtonThemeData( + iconButtonStyle: FluentUI.ButtonStyle( + iconSize: FluentUI.ButtonState.all(20), + ), + ), +); +final macosTheme = MacosThemeData.light().copyWith( + pushButtonTheme: const PushButtonThemeData( + secondaryColor: Colors.white, + ), + iconTheme: const MacosIconThemeData(size: 14), + typography: MacosTypography(color: Colors.grey[900]!), +); +final macosDarkTheme = MacosThemeData.dark().copyWith( + pushButtonTheme: const PushButtonThemeData( + secondaryColor: Colors.white, + ), + iconTheme: const MacosIconThemeData(size: 14), + typography: MacosTypography(color: MacosColors.textColor), +); +const iosTheme = CupertinoThemeData(brightness: Brightness.light); +const iosDarkTheme = CupertinoThemeData( + brightness: Brightness.dark, +); + +final linuxTheme = AdwaitaThemeData.light().copyWith( + listTileTheme: ListTileThemeData( + iconColor: Colors.grey[900], + horizontalTitleGap: 0, + ), +); +final linuxDarkTheme = AdwaitaThemeData.dark().copyWith( + listTileTheme: ListTileThemeData( + iconColor: Colors.grey[50], + horizontalTitleGap: 0, + ), +); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f18fb8ac..7beb29a8 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 7cfe2cb5..645a552d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "50.0.0" + adwaita: + dependency: "direct main" + description: + name: adwaita + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.2" analyzer: dependency: transitive description: @@ -254,7 +261,7 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.10" + version: "2.1.0" build_runner: dependency: "direct dev" description: @@ -493,6 +500,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.1" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.3" flutter: dependency: "direct main" description: flutter @@ -561,6 +575,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: @@ -582,6 +601,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.42.0" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" flutter_test: dependency: "direct dev" description: flutter @@ -627,6 +653,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + gsettings: + dependency: transitive + description: + name: gsettings + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" hive: dependency: "direct main" description: @@ -690,6 +723,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: @@ -718,6 +758,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.5.0" + libadwaita: + dependency: "direct main" + description: + name: libadwaita + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.5" + libadwaita_core: + dependency: transitive + description: + name: libadwaita_core + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.4" lints: dependency: transitive description: @@ -739,6 +793,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + macos_ui: + dependency: "direct main" + description: + name: macos_ui + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.5" marquee: dependency: "direct main" description: @@ -867,6 +928,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -972,6 +1047,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.1.0" plugin_platform_interface: dependency: transitive description: @@ -993,6 +1075,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.6+3" + popover_gtk: + dependency: transitive + description: + name: popover_gtk + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6+3" process: dependency: transitive description: @@ -1021,6 +1110,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 +1131,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 3a5f2fcc..bce3be6f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,12 @@ dependencies: flutter_inappwebview: ^5.4.3+7 tuple: ^2.0.1 uuid: ^3.0.6 + platform_ui: + path: ../platform_ui + fluent_ui: ^4.0.3 + macos_ui: ^1.7.5 + libadwaita: ^1.2.5 + adwaita: ^0.5.2 dev_dependencies: flutter_test: