Merge pull request #323 from KRTirtho/experimental/platform_ui

Experimental/platform UI
This commit is contained in:
Kingkor Roy Tirtho 2022-11-24 13:02:07 +06:00 committed by GitHub
commit a254e1e2f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1949 additions and 1513 deletions

2
.vscode/launch.json vendored
View File

@ -5,7 +5,7 @@
"name": "Flutter", "name": "Flutter",
"type": "dart", "type": "dart",
"request": "launch", "request": "launch",
"program": "lib/main.dart" "program": "${workspaceFolder}/lib/main.dart"
}, },
], ],
"compounds": [] "compounds": []

View File

@ -1,12 +1,16 @@
import 'package:auto_size_text/auto_size_text.dart'; 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/UniversalImage.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/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends StatelessWidget { class ArtistCard extends HookWidget {
final Artist artist; final Artist artist;
const ArtistCard(this.artist, {Key? key}) : super(key: key); const ArtistCard(this.artist, {Key? key}) : super(key: key);
@ -18,28 +22,70 @@ class ArtistCard extends StatelessWidget {
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
), ),
); );
final boxShadow = usePlatformProperty<BoxShadow?>(
(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<InteractiveInkFeatureFactory?>(
(context) => PlatformProperty.only(
android: InkRipple.splashFactory,
other: NoSplash.splashFactory,
),
);
return SizedBox( return SizedBox(
height: 240, height: 240,
width: 200, width: 200,
child: InkWell( child: InkWell(
splashFactory: splash,
onTap: () { onTap: () {
ServiceUtils.navigate(context, "/artist/${artist.id}"); 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) { child: HoverBuilder(builder: (context, isHovering) {
return Ink( return Ink(
width: 200, width: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).backgroundColor, color: PlatformTheme.of(context).secondaryBackgroundColor,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(
platform == TargetPlatform.windows ? 5 : 8,
),
boxShadow: [ boxShadow: [
BoxShadow( if (boxShadow != null) boxShadow,
blurRadius: 10,
offset: const Offset(0, 3),
spreadRadius: 5,
color: Theme.of(context).shadowColor,
)
], ],
border: [TargetPlatform.windows, TargetPlatform.macOS]
.contains(platform)
? Border.all(
color: PlatformTheme.of(context).borderColor ??
Colors.transparent,
width: 1,
)
: null,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
@ -79,7 +125,7 @@ class ArtistCard extends StatelessWidget {
artist.name!, artist.name!,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge!.copyWith( style: PlatformTextTheme.of(context).body?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistAlbumList.dart'; import 'package:spotube/components/Artist/ArtistAlbumList.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
@ -30,13 +31,13 @@ class ArtistProfile extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
SpotifyApi spotify = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
final parentScrollController = useScrollController(); final parentScrollController = useScrollController();
final textTheme = Theme.of(context).textTheme; final textTheme = PlatformTheme.of(context).textTheme;
final chipTextVariant = useBreakpointValue( final chipTextVariant = useBreakpointValue(
sm: textTheme.bodySmall, sm: textTheme!.caption,
md: textTheme.bodyMedium, md: textTheme.body,
lg: textTheme.headline6, lg: textTheme.subheading,
xl: textTheme.headline6, xl: textTheme.headline,
xxl: textTheme.headline6, xxl: textTheme.headline,
); );
final avatarWidth = useBreakpointValue( final avatarWidth = useBreakpointValue(
@ -52,9 +53,9 @@ class ArtistProfile extends HookConsumerWidget {
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
return SafeArea( return SafeArea(
child: Scaffold( child: PlatformScaffold(
appBar: const PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: BackButton(), leading: const PlatformBackButton(),
), ),
body: HookBuilder( body: HookBuilder(
builder: (context) { builder: (context) {
@ -67,7 +68,7 @@ class ArtistProfile extends HookConsumerWidget {
return const ShimmerArtistProfile(); return const ShimmerArtistProfile();
} else if (artistsQuery.hasError) { } else if (artistsQuery.hasError) {
return Center( return Center(
child: Text(artistsQuery.error.toString()), child: PlatformText(artistsQuery.error.toString()),
); );
} }
@ -105,21 +106,22 @@ class ArtistProfile extends HookConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue, color: Colors.blue,
borderRadius: BorderRadius.circular(50)), borderRadius: BorderRadius.circular(50)),
child: Text(data.type!.toUpperCase(), child: PlatformText(data.type!.toUpperCase(),
style: chipTextVariant?.copyWith( style: chipTextVariant?.copyWith(
color: Colors.white)), color: Colors.white)),
), ),
Text( PlatformText(
data.name!, data.name!,
style: breakpoint.isSm style: breakpoint.isSm
? textTheme.headline4 ? textTheme.subheading
: textTheme.headline2, : textTheme.headline,
), ),
Text( PlatformText(
"${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers", "${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers",
style: breakpoint.isSm style: breakpoint.isSm
? textTheme.bodyText1 ? textTheme.body
: textTheme.headline5, : textTheme.body
?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
@ -138,11 +140,12 @@ class ArtistProfile extends HookConsumerWidget {
return const SizedBox( return const SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: CircularProgressIndicator(), child:
PlatformCircularProgressIndicator(),
); );
} }
return OutlinedButton( return PlatformFilledButton(
onPressed: () async { onPressed: () async {
try { try {
isFollowingQuery.data! isFollowingQuery.data!
@ -168,7 +171,7 @@ class ArtistProfile extends HookConsumerWidget {
]); ]);
} }
}, },
child: Text( child: PlatformText(
isFollowingQuery.data! isFollowingQuery.data!
? "Following" ? "Following"
: "Follow", : "Follow",
@ -176,7 +179,7 @@ class ArtistProfile extends HookConsumerWidget {
); );
}, },
), ),
IconButton( PlatformIconButton(
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),
onPressed: () { onPressed: () {
Clipboard.setData( Clipboard.setData(
@ -188,7 +191,7 @@ class ArtistProfile extends HookConsumerWidget {
const SnackBar( const SnackBar(
width: 300, width: 300,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
content: Text( content: PlatformText(
"Artist URL copied to clipboard", "Artist URL copied to clipboard",
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -213,10 +216,10 @@ class ArtistProfile extends HookConsumerWidget {
); );
if (topTracksQuery.isLoading || !topTracksQuery.hasData) { if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
return const CircularProgressIndicator.adaptive(); return const PlatformCircularProgressIndicator();
} else if (topTracksQuery.hasError) { } else if (topTracksQuery.hasError) {
return Center( return Center(
child: Text(topTracksQuery.error.toString()), child: PlatformText(topTracksQuery.error.toString()),
); );
} }
@ -250,9 +253,10 @@ class ArtistProfile extends HookConsumerWidget {
return Column(children: [ return Column(children: [
Row( Row(
children: [ children: [
Text( PlatformText(
"Top Tracks", "Top Tracks",
style: Theme.of(context).textTheme.headline4, style:
PlatformTheme.of(context).textTheme?.headline,
), ),
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 5), margin: const EdgeInsets.symmetric(horizontal: 5),
@ -260,11 +264,13 @@ class ArtistProfile extends HookConsumerWidget {
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
child: IconButton( child: PlatformIconButton(
icon: Icon(isPlaylistPlaying icon: Icon(
? Icons.stop_rounded isPlaylistPlaying
: Icons.play_arrow_rounded), ? Icons.stop_rounded
color: Colors.white, : Icons.play_arrow_rounded,
color: Colors.white,
),
onPressed: () => onPressed: () =>
playPlaylist(topTracks.toList()), playPlaylist(topTracks.toList()),
), ),
@ -290,16 +296,16 @@ class ArtistProfile extends HookConsumerWidget {
}, },
), ),
const SizedBox(height: 50), const SizedBox(height: 50),
Text( PlatformText(
"Albums", "Albums",
style: Theme.of(context).textTheme.headline4, style: PlatformTheme.of(context).textTheme?.headline,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
ArtistAlbumList(artistId), ArtistAlbumList(artistId),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( PlatformText(
"Fans also likes", "Fans also likes",
style: Theme.of(context).textTheme.headline4, style: PlatformTheme.of(context).textTheme?.headline,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
HookBuilder( HookBuilder(
@ -310,10 +316,10 @@ class ArtistProfile extends HookConsumerWidget {
); );
if (relatedArtists.isLoading || !relatedArtists.hasData) { if (relatedArtists.isLoading || !relatedArtists.hasData) {
return const CircularProgressIndicator.adaptive(); return const PlatformCircularProgressIndicator();
} else if (relatedArtists.hasError) { } else if (relatedArtists.hasError) {
return Center( return Center(
child: Text(relatedArtists.error.toString()), child: PlatformText(relatedArtists.error.toString()),
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
@ -46,15 +47,13 @@ class CategoryCard extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
children: [ children: [
Text( PlatformText.headline(category.name ?? "Unknown"),
category.name ?? "Unknown",
style: Theme.of(context).textTheme.headline5,
),
], ],
), ),
), ),
playlistQuery.hasError playlistQuery.hasError
? Text("Something Went Wrong\n${playlistQuery.errors.first}") ? PlatformText(
"Something Went Wrong\n${playlistQuery.errors.first}")
: SizedBox( : SizedBox(
height: 245, height: 245,
child: ScrollConfiguration( child: ScrollConfiguration(

View File

@ -2,13 +2,16 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Category/CategoryCard.dart'; import 'package:spotube/components/Category/CategoryCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.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/components/Shared/Waypoint.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
class Genres extends HookConsumerWidget { class Genres extends HookConsumerWidget {
const Genres({Key? key}) : super(key: key); const Genres({Key? key}) : super(key: key);
@ -40,7 +43,8 @@ class Genres extends HookConsumerWidget {
.toList() .toList()
]; ];
return Scaffold( return PlatformScaffold(
appBar: kIsDesktop ? PageWindowTitleBar() : null,
body: ListView.builder( body: ListView.builder(
itemCount: categories.length, itemCount: categories.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/Sidebar.dart';
import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
import 'package:spotube/components/Player/Player.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/provider/Downloader.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
const _path = { const rootPaths = {
0: "/", 0: "/",
1: "/search", 1: "/search",
2: "/library", 2: "/library",
@ -35,8 +36,8 @@ class Shell extends HookConsumerWidget {
useEffect(() { useEffect(() {
downloader.onFileExists = (track) async { downloader.onFileExists = (track) async {
if (!isMounted()) return false; if (!isMounted()) return false;
return await showDialog<bool>( return await showPlatformAlertDialog<bool>(
context: context, context,
builder: (context) => ReplaceDownloadedFileDialog( builder: (context) => ReplaceDownloadedFileDialog(
track: track, track: track,
), ),
@ -63,38 +64,14 @@ class Shell extends HookConsumerWidget {
return null; return null;
}, [backgroundColor]); }, [backgroundColor]);
final allowedPath = _path.values.contains(GoRouter.of(context).location); return PlatformScaffold(
final preferredSize = body: Sidebar(
allowedPath ? PageWindowTitleBar.staticPreferredSize : Size.zero; selectedIndex: index.value,
return Scaffold( onSelectedIndexChanged: (i) {
appBar: kIsDesktop index.value = i;
? PreferredSize( GoRouter.of(context).go(rootPaths[index.value]!);
preferredSize: preferredSize, },
child: AnimatedContainer( child: child,
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),
],
), ),
extendBody: true, extendBody: true,
bottomNavigationBar: Column( bottomNavigationBar: Column(
@ -105,7 +82,7 @@ class Shell extends HookConsumerWidget {
selectedIndex: index.value, selectedIndex: index.value,
onSelectedIndexChanged: (selectedIndex) { onSelectedIndexChanged: (selectedIndex) {
index.value = selectedIndex; index.value = selectedIndex;
GoRouter.of(context).go(_path[selectedIndex]!); GoRouter.of(context).go(rootPaths[selectedIndex]!);
}, },
), ),
], ],

View File

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

View File

@ -1,7 +1,7 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/Sidebar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/models/sideBarTiles.dart';
@ -37,37 +37,23 @@ class SpotubeNavigationBar extends HookConsumerWidget {
if (layoutMode == LayoutMode.extended || if (layoutMode == LayoutMode.extended ||
(breakpoint.isMoreThan(Breakpoints.sm) && (breakpoint.isMoreThan(Breakpoints.sm) &&
layoutMode == LayoutMode.adaptive)) return const SizedBox(); layoutMode == LayoutMode.adaptive)) return const SizedBox();
return NavigationBar( return PlatformBottomNavigationBar(
destinations: [ items: [
...sidebarTileList.map( ...sidebarTileList.map(
(e) { (e) {
final icon = Icon(e.icon); return PlatformBottomNavigationBarItem(
return NavigationDestination( icon: e.icon,
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: e.title, label: e.title,
); );
}, },
), ),
const NavigationDestination( const PlatformBottomNavigationBarItem(
icon: Icon(Icons.settings_rounded), icon: Icons.settings_rounded,
label: "Settings", label: "Settings",
) )
], ],
selectedIndex: insideSelectedIndex.value, selectedIndex: insideSelectedIndex.value,
onDestinationSelected: (i) { onSelectedIndexChanged: (i) {
if (i == 4) { if (i == 4) {
insideSelectedIndex.value = 4; insideSelectedIndex.value = 4;
Sidebar.goToSettings(context); Sidebar.goToSettings(context);

View File

@ -1,8 +1,11 @@
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/Album/AlbumCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.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/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -12,6 +15,10 @@ class UserAlbums extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final albumsQuery = useQuery( final albumsQuery = useQuery(
job: currentUserAlbumsQueryJob, job: currentUserAlbumsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),
@ -22,16 +29,22 @@ class UserAlbums extends HookConsumerWidget {
} }
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Material(
padding: const EdgeInsets.all(8.0), type: MaterialType.transparency,
child: Wrap( textStyle: PlatformTheme.of(context).textTheme!.body!,
spacing: 20, // gap between adjacent chips color: PlatformTheme.of(context).scaffoldBackgroundColor,
runSpacing: 20, // gap between lines child: Container(
alignment: WrapAlignment.center, width: double.infinity,
children: albumsQuery.data! padding: const EdgeInsets.all(8.0),
.map((album) => child: Wrap(
AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album))) spacing: 20, // gap between adjacent chips
.toList(), runSpacing: 20, // gap between lines
alignment: WrapAlignment.center,
children: albumsQuery.data!
.map((album) =>
AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album)))
.toList(),
),
), ),
), ),
); );

View File

@ -2,9 +2,12 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistCard.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/components/Shared/Waypoint.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -13,6 +16,10 @@ class UserArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final artistQuery = useInfiniteQuery( final artistQuery = useInfiniteQuery(
job: currentUserFollowingArtistsQueryJob, job: currentUserFollowingArtistsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),
@ -28,26 +35,31 @@ class UserArtists extends HookConsumerWidget {
? false ? false
: (artistQuery.pages.last?.items?.length ?? 0) == 15; : (artistQuery.pages.last?.items?.length ?? 0) == 15;
return GridView.builder( return Material(
itemCount: artists.length, type: MaterialType.transparency,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( textStyle: PlatformTheme.of(context).textTheme!.body!,
maxCrossAxisExtent: 200, color: PlatformTheme.of(context).scaffoldBackgroundColor,
mainAxisExtent: 250, child: GridView.builder(
crossAxisSpacing: 20, itemCount: artists.length,
mainAxisSpacing: 20, 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]);
},
); );
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/Downloader.dart';
@ -25,19 +26,20 @@ class UserDownloads extends HookConsumerWidget {
child: AutoSizeText( child: AutoSizeText(
"Currently downloading (${downloader.currentlyRunning})", "Currently downloading (${downloader.currentlyRunning})",
maxLines: 1, maxLines: 1,
style: Theme.of(context).textTheme.headline5, style: PlatformTextTheme.of(context).headline,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
ElevatedButton( PlatformFilledButton(
style: ElevatedButton.styleFrom( style: ButtonStyle(
backgroundColor: Colors.red[50], backgroundColor: MaterialStatePropertyAll(Colors.red[50]),
foregroundColor: Colors.red[400], foregroundColor: MaterialStatePropertyAll(Colors.red[400]),
), ),
onPressed: downloader.currentlyRunning > 0 onPressed: downloader.currentlyRunning > 0
? downloader.cancelAll ? downloader.cancelAll
: null, : 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, itemCount: downloader.inQueue.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final track = downloader.inQueue.elementAt(index); final track = downloader.inQueue.elementAt(index);
return ListTile( return PlatformListTile(
title: Text(track.name!), title: Text(track.name ?? ''),
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.symmetric(horizontal: 5),
child: ClipRRect( child: ClipRRect(
@ -66,9 +68,8 @@ class UserDownloads extends HookConsumerWidget {
trailing: const SizedBox( trailing: const SizedBox(
width: 30, width: 30,
height: 30, height: 30,
child: CircularProgressIndicator.adaptive(), child: PlatformCircularProgressIndicator(),
), ),
horizontalTitleGap: 5,
subtitle: Text( subtitle: Text(
TypeConversionUtils.artists_X_String( TypeConversionUtils.artists_X_String(
track.artists ?? <Artist>[], track.artists ?? <Artist>[],

View File

@ -1,42 +1,49 @@
import 'package:flutter/material.dart' hide Image; 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/UserAlbums.dart';
import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserDownloads.dart'; import 'package:spotube/components/Library/UserDownloads.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/ColoredTabBar.dart';
class UserLibrary extends ConsumerWidget { class UserLibrary extends HookConsumerWidget {
const UserLibrary({Key? key}) : super(key: key); const UserLibrary({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return DefaultTabController( final index = useState(0);
length: 5,
child: SafeArea( final body = [
child: Scaffold( const UserPlaylists(),
appBar: ColoredTabBar( const UserLocalTracks(),
color: Theme.of(context).backgroundColor, const UserDownloads(),
child: const TabBar( const UserArtists(),
isScrollable: true, const UserAlbums(),
tabs: [ ][index.value];
Tab(text: "Playlist"),
Tab(text: "Downloads"), return SafeArea(
Tab(text: "Local"), child: PlatformScaffold(
Tab(text: "Artists"), appBar: PageWindowTitleBar(
Tab(text: "Album"), 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,
), ),
); );
} }

View File

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

View File

@ -1,10 +1,13 @@
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Playlist/PlaylistCreateDialog.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/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -13,6 +16,11 @@ class UserPlaylists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final playlistsQuery = useQuery( final playlistsQuery = useQuery(
job: currentUserPlaylistsQueryJob, job: currentUserPlaylistsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),
@ -34,19 +42,24 @@ class UserPlaylists extends HookConsumerWidget {
} }
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Material(
padding: const EdgeInsets.all(8.0), type: MaterialType.transparency,
child: Wrap( textStyle: PlatformTheme.of(context).textTheme!.body!,
spacing: 20, // gap between adjacent chips child: Container(
runSpacing: 20, // gap between lines width: double.infinity,
alignment: WrapAlignment.center, padding: const EdgeInsets.all(8.0),
children: [ child: Wrap(
const PlaylistCreateDialog(), spacing: 20, // gap between adjacent chips
PlaylistCard(likedTracksPlaylist), runSpacing: 20, // gap between lines
...playlistsQuery.data! alignment: WrapAlignment.center,
.map((playlist) => PlaylistCard(playlist)) children: [
.toList(), const PlaylistCreateDialog(),
], PlaylistCard(likedTracksPlaylist),
...playlistsQuery.data!
.map((playlist) => PlaylistCard(playlist))
.toList(),
],
),
), ),
), ),
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:introduction_screen/introduction_screen.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/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -13,46 +14,56 @@ class LoginTutorial extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final key = GlobalKey<State<IntroductionScreen>>();
return Scaffold( return PlatformScaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: TextButton( leading: PlatformTextButton(
child: const Text("Exit"), child: const PlatformText("Exit"),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
), ),
body: IntroductionScreen( body: IntroductionScreen(
next: const Text("Next"), key: key,
back: const Text("Previous"), 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, showBackButton: true,
overrideDone: TextButton( overrideDone: PlatformFilledButton(
onPressed: auth.isLoggedIn onPressed: auth.isLoggedIn
? () { ? () {
ServiceUtils.navigate(context, "/"); ServiceUtils.navigate(context, "/");
} }
: null, : null,
child: const Text("Done"), child: const Center(child: PlatformText("Done")),
), ),
pages: [ pages: [
PageViewModel( PageViewModel(
title: "Step 1", title: "Step 1",
image: Image.asset("assets/tutorial/step-1.png"), image: Image.asset("assets/tutorial/step-1.png"),
bodyWidget: Wrap( bodyWidget: Wrap(
children: [ children: const [
Text( PlatformText(
"First, Go to ", "First, Go to ",
style: Theme.of(context).textTheme.bodyText1,
), ),
Hyperlink( Hyperlink(
"accounts.spotify.com ", "accounts.spotify.com ",
"https://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", "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( PageViewModel(
title: "Step 2", title: "Step 2",
image: Image.asset("assets/tutorial/step-2.png"), 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", "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, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
PageViewModel( PageViewModel(
@ -71,10 +81,9 @@ class LoginTutorial extends ConsumerWidget {
image: Image.asset( image: Image.asset(
"assets/tutorial/step-3.png", "assets/tutorial/step-3.png",
), ),
bodyWidget: Text( bodyWidget: const PlatformText(
"Copy the values of \"sp_dc\" and \"sp_key\" Cookies", "Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
if (auth.isLoggedIn) if (auth.isLoggedIn)
@ -91,13 +100,12 @@ class LoginTutorial extends ConsumerWidget {
PageViewModel( PageViewModel(
title: "Step 5", title: "Step 5",
bodyWidget: Column( bodyWidget: Column(
children: [ children: const [
Text( PlatformText(
"Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields", "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
style: Theme.of(context).textTheme.bodyText1,
), ),
const SizedBox(height: 10), SizedBox(height: 10),
const TokenLoginForm(), TokenLoginForm(),
], ],
), ),
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
@ -14,12 +15,17 @@ class TokenLogin extends HookConsumerWidget {
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return SafeArea( return SafeArea(
child: Scaffold( child: PlatformScaffold(
appBar: const PageWindowTitleBar(leading: BackButton()), appBar: PageWindowTitleBar(leading: const PlatformBackButton()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
child: Container( 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( child: Column(
children: [ children: [
Image.asset( Image.asset(
@ -27,11 +33,11 @@ class TokenLogin extends HookConsumerWidget {
width: MediaQuery.of(context).size.width * width: MediaQuery.of(context).size.width *
(breakpoint <= Breakpoints.md ? .5 : .3), (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 style: breakpoint <= Breakpoints.md
? textTheme.headline5 ? textTheme.headline5
: textTheme.headline4), : textTheme.headline4),
Text( PlatformText(
"Don't worry, any of your credentials won't be collected or shared with anyone", "Don't worry, any of your credentials won't be collected or shared with anyone",
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
), ),
@ -44,9 +50,9 @@ class TokenLogin extends HookConsumerWidget {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
const Text("Don't know how to do this?"), const PlatformText("Don't know how to do this?"),
TextButton( PlatformTextButton(
child: const Text( child: const PlatformText(
"Follow along the Step by Step guide", "Follow along the Step by Step guide",
), ),
onPressed: () => GoRouter.of(context).push( onPressed: () => GoRouter.of(context).push(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -24,31 +25,27 @@ class TokenLoginForm extends HookConsumerWidget {
), ),
child: Column( child: Column(
children: [ children: [
TextField( PlatformTextField(
controller: directCodeController, controller: directCodeController,
decoration: const InputDecoration( placeholder: "Spotify \"sp_dc\" Cookie",
hintText: "Spotify \"sp_dc\" Cookie", label: "sp_dc Cookie",
label: Text("sp_dc Cookie"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( PlatformTextField(
controller: keyCodeController, controller: keyCodeController,
decoration: const InputDecoration( placeholder: "Spotify \"sp_key\" Cookie",
hintText: "Spotify \"sp_key\" Cookie", label: "sp_key Cookie",
label: Text("sp_key Cookie"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( PlatformFilledButton(
onPressed: () async { onPressed: () async {
if (keyCodeController.text.isEmpty || if (keyCodeController.text.isEmpty ||
directCodeController.text.isEmpty) { directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please fill in all fields"), content: PlatformText("Please fill in all fields"),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
@ -67,7 +64,7 @@ class TokenLoginForm extends HookConsumerWidget {
onDone?.call(); onDone?.call();
} }
}, },
child: const Text("Submit"), child: const PlatformText("Submit"),
) )
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -23,7 +24,7 @@ class WebViewLogin extends HookConsumerWidget {
); );
} }
return Scaffold( return PlatformScaffold(
body: SafeArea( body: SafeArea(
child: InAppWebView( child: InAppWebView(
initialOptions: InAppWebViewGroupOptions( initialOptions: InAppWebViewGroupOptions(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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'; import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
class LyricDelayAdjustDialog extends HookConsumerWidget { class LyricDelayAdjustDialog extends HookConsumerWidget {
@ -15,16 +17,20 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
double getValue() => double getValue() =>
double.tryParse(controller.text.replaceAll("ms", "")) ?? 0; double.tryParse(controller.text.replaceAll("ms", "")) ?? 0;
return AlertDialog( return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: const Center(child: Text("Adjust Lyrics Delay")), title: const Center(child: Text("Adjust Lyrics Delay")),
actions: [ secondaryActions: [
ElevatedButton( PlatformFilledButton(
child: const Text("Cancel"), isSecondary: true,
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text("Cancel"),
), ),
ElevatedButton( ],
primaryActions: [
PlatformFilledButton(
child: const Text("Done"), child: const Text("Done"),
onPressed: () { onPressed: () {
Navigator.of(context).pop( Navigator.of(context).pop(
@ -35,39 +41,39 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
}, },
) )
], ],
content: Row( content: SizedBox(
mainAxisSize: MainAxisSize.min, height: 100,
children: [ child: Row(
IconButton( mainAxisSize: MainAxisSize.min,
icon: const Icon(Icons.remove_rounded), children: [
onPressed: () { PlatformIconButton(
controller.text = "${getValue() - 25}ms"; 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(),
),
);
}, },
), ),
), Flexible(
IconButton( child: PlatformTextField(
icon: const Icon(Icons.add_rounded), keyboardType: TextInputType.number,
onPressed: () { controller: controller,
controller.text = "${getValue() + 25}ms"; 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";
},
),
],
),
), ),
); );
} }

View File

@ -3,12 +3,15 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/GeniusLyrics.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.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/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class Lyrics extends HookConsumerWidget { class Lyrics extends HookConsumerWidget {
@ -26,6 +29,7 @@ class Lyrics extends HookConsumerWidget {
[playback.track?.album?.images], [playback.track?.album?.images],
); );
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final index = useState(0);
useCustomStatusBarColor( useCustomStatusBarColor(
palette.color, palette.color,
@ -33,38 +37,52 @@ class Lyrics extends HookConsumerWidget {
noSetBGColor: true, noSetBGColor: true,
); );
return DefaultTabController( final body = [
length: 2, SyncedLyrics(palette: palette),
child: Scaffold( GeniusLyrics(palette: palette),
extendBodyBehindAppBar: true, ][index.value];
appBar: const TabBar(
isScrollable: true, return PlatformScaffold(
tabs: [ extendBodyBehindAppBar: true,
Tab(text: "Synced Lyrics"), appBar: !kIsMacOS
Tab(text: "Lyrics (genius.com)"), ? PageWindowTitleBar(
], toolbarOpacity: 0,
), backgroundColor: Colors.transparent,
body: Container( center: PlatformTabBar(
clipBehavior: Clip.hardEdge, isNavigational:
decoration: BoxDecoration( PlatformProperty.only(linux: true, other: false),
image: DecorationImage( selectedIndex: index.value,
image: UniversalImage.imageProvider(albumArt), onSelectedIndexChanged: (value) => index.value = value,
fit: BoxFit.cover, backgroundColor:
), PlatformTheme.of(context).scaffoldBackgroundColor,
), tabs: [
child: BackdropFilter( PlatformTab(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), label: "Synced",
child: Container( icon: const SizedBox.shrink(),
color: palette.color.withOpacity(.7), color: PlatformTextTheme.of(context).caption?.color,
child: SafeArea( ),
child: TabBarView( PlatformTab(
children: [ label: "Genius",
SyncedLyrics(palette: palette), icon: const SizedBox.shrink(),
GeniusLyrics(palette: palette), 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),
), ),
), ),
), ),

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
@ -73,6 +74,7 @@ class SyncedLyrics extends HookConsumerWidget {
height: breakpoint >= Breakpoints.md ? 50 : 30, height: breakpoint >= Breakpoints.md ? 50 : 30,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: PlatformTheme.of(context).textTheme!.body!,
child: Stack( child: Stack(
children: [ children: [
Center( Center(
@ -85,18 +87,24 @@ class SyncedLyrics extends HookConsumerWidget {
Positioned.fill( Positioned.fill(
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: IconButton( child: Padding(
tooltip: "Lyrics Delay", padding: const EdgeInsets.all(8.0),
icon: const Icon(Icons.av_timer_rounded), child: PlatformFilledButton(
onPressed: () async { child: const Icon(
final delay = await showDialog( Icons.av_timer_rounded,
context: context, size: 16,
builder: (context) => const LyricDelayAdjustDialog(), ),
); onPressed: () async {
if (delay != null) { final delay = await showPlatformAlertDialog(
ref.read(lyricDelayState.notifier).state = delay; context,
} builder: (context) =>
}, const LyricDelayAdjustDialog(),
);
if (delay != null) {
ref.read(lyricDelayState.notifier).state = delay;
}
},
),
), ),
), ),
), ),

View File

@ -1,11 +1,16 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart';
import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/usePlatformProperty.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -46,10 +51,51 @@ class Player extends HookConsumerWidget {
); );
} }
return Container( final backgroundColor = usePlatformProperty<Color?>(
color: Theme.of(context).backgroundColor, (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<BoxBorder?>(
(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( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: PlatformTheme.of(context).textTheme!.body!,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@ -90,7 +136,7 @@ class Player extends HookConsumerWidget {
} }
} }
}, },
child: Slider.adaptive( child: PlatformSlider(
min: 0, min: 0,
max: 1, max: 1,
value: volume.value, value: volume.value,

View File

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Player/PlayerQueue.dart'; import 'package:spotube/components/Player/PlayerQueue.dart';
@ -48,7 +49,7 @@ class PlayerActions extends HookConsumerWidget {
return Row( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ children: [
IconButton( PlatformIconButton(
icon: const Icon(Icons.queue_music_rounded), icon: const Icon(Icons.queue_music_rounded),
tooltip: 'Queue', tooltip: 'Queue',
onPressed: playback.playlist != null onPressed: playback.playlist != null
@ -73,7 +74,7 @@ class PlayerActions extends HookConsumerWidget {
} }
: null, : null,
), ),
IconButton( PlatformIconButton(
icon: const Icon(Icons.alt_route_rounded), icon: const Icon(Icons.alt_route_rounded),
tooltip: "Alternative Track Sources", tooltip: "Alternative Track Sources",
onPressed: playback.track != null onPressed: playback.track != null
@ -108,7 +109,7 @@ class PlayerActions extends HookConsumerWidget {
), ),
) )
else else
IconButton( PlatformIconButton(
tooltip: 'Download track', tooltip: 'Download track',
icon: Icon( icon: Icon(
isDownloaded isDownloaded

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/playback.dart';
import 'package:spotube/models/Intents.dart'; import 'package:spotube/models/Intents.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
@ -94,10 +95,9 @@ class PlayerControls extends HookConsumerWidget {
return Column( return Column(
children: [ children: [
Tooltip( PlatformTooltip(
message: "Slide to seek forward or backward", message: "Slide to seek forward or backward",
child: Slider.adaptive( child: PlatformSlider(
focusNode: FocusNode(),
// cannot divide by zero // cannot divide by zero
// there's an edge case for value being bigger // there's an edge case for value being bigger
// than total duration. Keeping it resolved // than total duration. Keeping it resolved
@ -122,10 +122,10 @@ class PlayerControls extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( PlatformText(
"$currentMinutes:$currentSeconds", "$currentMinutes:$currentSeconds",
), ),
Text("$totalMinutes:$totalSeconds"), PlatformText("$totalMinutes:$totalSeconds"),
], ],
), ),
), ),
@ -138,7 +138,7 @@ class PlayerControls extends HookConsumerWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
IconButton( PlatformIconButton(
tooltip: playback.isLoop tooltip: playback.isLoop
? "Repeat playlist" ? "Repeat playlist"
: playback.isShuffled : playback.isShuffled
@ -156,14 +156,16 @@ class PlayerControls extends HookConsumerWidget {
? null ? null
: playback.cyclePlaybackMode, : playback.cyclePlaybackMode,
), ),
IconButton( PlatformIconButton(
tooltip: "Previous track", tooltip: "Previous track",
icon: const Icon(Icons.skip_previous_rounded), icon: Icon(
color: iconColor, Icons.skip_previous_rounded,
color: iconColor,
),
onPressed: () { onPressed: () {
onPrevious(); onPrevious();
}), }),
IconButton( PlatformIconButton(
tooltip: playback.isPlaying tooltip: playback.isPlaying
? "Pause playback" ? "Pause playback"
: "Resume playback", : "Resume playback",
@ -171,29 +173,33 @@ class PlayerControls extends HookConsumerWidget {
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
width: 20, width: 20,
child: CircularProgressIndicator(), child: PlatformCircularProgressIndicator(),
) )
: Icon( : Icon(
playback.isPlaying playback.isPlaying
? Icons.pause_rounded ? Icons.pause_rounded
: Icons.play_arrow_rounded, : Icons.play_arrow_rounded,
color: iconColor,
), ),
color: iconColor,
onPressed: Actions.handler<PlayPauseIntent>( onPressed: Actions.handler<PlayPauseIntent>(
context, context,
PlayPauseIntent(ref), PlayPauseIntent(ref),
), ),
), ),
IconButton( PlatformIconButton(
tooltip: "Next track", tooltip: "Next track",
icon: const Icon(Icons.skip_next_rounded), icon: Icon(
Icons.skip_next_rounded,
color: iconColor,
),
onPressed: () => onNext(), onPressed: () => onNext(),
color: iconColor,
), ),
IconButton( PlatformIconButton(
tooltip: "Stop playback", tooltip: "Stop playback",
icon: const Icon(Icons.stop_rounded), icon: Icon(
color: iconColor, Icons.stop_rounded,
color: iconColor,
),
onPressed: playback.track != null onPressed: playback.track != null
? () async { ? () async {
try { try {

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/playback.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
@ -61,6 +62,7 @@ class PlayerOverlay extends HookConsumerWidget {
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
opacity: canShow ? 1 : 0, opacity: canShow ? 1 : 0,
child: Material( child: Material(
textStyle: PlatformTheme.of(context).textTheme!.body!,
type: MaterialType.transparency, type: MaterialType.transparency,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -80,8 +82,10 @@ class PlayerOverlay extends HookConsumerWidget {
Row( Row(
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.skip_previous_rounded), icon: Icon(
color: paletteColor.bodyTextColor, Icons.skip_previous_rounded,
color: paletteColor.bodyTextColor,
),
onPressed: () { onPressed: () {
onPrevious(); onPrevious();
}), }),
@ -92,8 +96,8 @@ class PlayerOverlay extends HookConsumerWidget {
ref.read(playbackProvider).isPlaying ref.read(playbackProvider).isPlaying
? Icons.pause_rounded ? Icons.pause_rounded
: Icons.play_arrow_rounded, : Icons.play_arrow_rounded,
color: paletteColor.bodyTextColor,
), ),
color: paletteColor.bodyTextColor,
onPressed: Actions.handler<PlayPauseIntent>( onPressed: Actions.handler<PlayPauseIntent>(
context, context,
PlayPauseIntent(ref), PlayPauseIntent(ref),
@ -102,9 +106,11 @@ class PlayerOverlay extends HookConsumerWidget {
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.skip_next_rounded), icon: Icon(
Icons.skip_next_rounded,
color: paletteColor.bodyTextColor,
),
onPressed: () => onNext(), onPressed: () => onNext(),
color: paletteColor.bodyTextColor,
), ),
], ],
), ),

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/components/Shared/NotFound.dart'; import 'package:spotube/components/Shared/NotFound.dart';
import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/components/Shared/TrackTile.dart';
@ -47,9 +48,6 @@ class PlayerQueue extends HookConsumerWidget {
return null; return null;
}, []); }, []);
var titleStyle = Theme.of(context).textTheme.headline4?.copyWith(
fontWeight: FontWeight.bold,
);
return BackdropFilter( return BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: 12.0, sigmaX: 12.0,
@ -61,9 +59,8 @@ class PlayerQueue extends HookConsumerWidget {
top: 5.0, top: 5.0,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context) color: PlatformTheme.of(context)
.navigationRailTheme .scaffoldBackgroundColor
.backgroundColor
?.withOpacity(0.5), ?.withOpacity(0.5),
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
@ -78,16 +75,13 @@ class PlayerQueue extends HookConsumerWidget {
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
), ),
Text("Queue", style: titleStyle), PlatformText.subheading("Queue"),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: PlatformText(
playback.playlist?.name ?? "", playback.playlist?.name ?? "",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: PlatformTextTheme.of(context).body,
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
@ -36,13 +37,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
), ),
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
Flexible( Flexible(
child: Text( child: PlatformText(
playback.track?.name ?? "Not playing", playback.track?.name ?? "Not playing",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: TextStyle(fontWeight: FontWeight.bold, color: color),
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold, color: color),
), ),
), ),
@ -52,13 +50,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
flex: 1, flex: 1,
child: Column( child: Column(
children: [ children: [
Text( PlatformText(
playback.track?.name ?? "Not playing", playback.track?.name ?? "Not playing",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: TextStyle(fontWeight: FontWeight.bold, color: color),
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold, color: color),
), ),
TypeConversionUtils.artists_X_ClickableArtists( TypeConversionUtils.artists_X_ClickableArtists(
playback.track?.artists ?? [], playback.track?.artists ?? [],

View File

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.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/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -57,7 +58,19 @@ class PlayerView extends HookConsumerWidget {
noSetBGColor: true, 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( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
@ -68,15 +81,11 @@ class PlayerView extends HookConsumerWidget {
child: BackdropFilter( child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Material( child: Material(
textStyle: PlatformTheme.of(context).textTheme!.body!,
color: paletteColor.color.withOpacity(.5), color: paletteColor.color.withOpacity(.5),
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
PageWindowTitleBar(
leading: const BackButton(),
backgroundColor: Colors.transparent,
foregroundColor: paletteColor.titleTextColor,
),
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Column( child: Column(
@ -162,7 +171,7 @@ class PlayerView extends HookConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
floatingQueue: false, floatingQueue: false,
extraActions: [ extraActions: [
IconButton( PlatformIconButton(
tooltip: "Open Lyrics", tooltip: "Open Lyrics",
icon: const Icon(Icons.lyrics_rounded), icon: const Icon(Icons.lyrics_rounded),
onPressed: () { onPressed: () {

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/components/Shared/UniversalImage.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
@ -40,42 +41,46 @@ class SiblingTracksSheet extends HookConsumerWidget {
margin: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius, borderRadius: borderRadius,
color: Theme.of(context) color: PlatformTheme.of(context)
.navigationRailTheme .scaffoldBackgroundColor!
.backgroundColor .withOpacity(.3),
?.withOpacity(0.5),
), ),
child: Scaffold( child: Scaffold(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
appBar: AppBar( appBar: PlatformAppBar(
centerTitle: true, centerTitle: true,
title: const Text('Alternative Tracks Sources'), title: PlatformText.subheading(
'Alternative Tracks Sources',
),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
toolbarOpacity: 0,
), ),
body: Padding( body: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListView.builder( child: ListView.builder(
itemCount: playback.siblingYtVideos.length, itemCount: playback.siblingYtVideos.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final video = playback.siblingYtVideos[index]; final video = playback.siblingYtVideos[index];
return ListTile( return PlatformListTile(
title: Text(video.title), title: PlatformText(video.title),
leading: UniversalImage( leading: Padding(
path: video.thumbnails.lowResUrl, padding: const EdgeInsets.all(8.0),
height: 60, child: UniversalImage(
width: 60, path: video.thumbnails.lowResUrl,
height: 60,
width: 60,
),
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
horizontalTitleGap: 10, trailing: PlatformText(
trailing: Text(
PrimitiveUtils.toReadableDuration( PrimitiveUtils.toReadableDuration(
video.duration ?? Duration.zero, video.duration ?? Duration.zero,
), ),
), ),
subtitle: Text(video.author), subtitle: PlatformText(video.author),
enabled: playback.status != PlaybackStatus.loading, enabled: playback.status != PlaybackStatus.loading,
selected: video.id == playback.track!.ytTrack.id, selected: video.id == playback.track!.ytTrack.id,
selectedTileColor: Theme.of(context).popupMenuTheme.color, selectedTileColor: Theme.of(context).popupMenuTheme.color,

View File

@ -2,6 +2,8 @@ import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -12,10 +14,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return TextButton( return PlatformTextButton(
onPressed: () { onPressed: () {
showDialog( showPlatformAlertDialog(
context: context, context,
builder: (context) { builder: (context) {
return HookBuilder(builder: (context) { return HookBuilder(builder: (context) {
final playlistName = useTextEditingController(); final playlistName = useTextEditingController();
@ -23,14 +25,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final public = useState(false); final public = useState(false);
final collaborative = useState(false); final collaborative = useState(false);
return AlertDialog( return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: const Text("Create a Playlist"), title: const Text("Create a Playlist"),
actions: [ primaryActions: [
TextButton( PlatformFilledButton(
child: const Text("Cancel"),
onPressed: () => Navigator.of(context).pop(),
),
ElevatedButton(
child: const Text("Create"), child: const Text("Create"),
onPressed: () async { onPressed: () async {
if (playlistName.text.isEmpty) return; 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( content: Container(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
constraints: const BoxConstraints(maxWidth: 500), constraints: const BoxConstraints(maxWidth: 500),
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
TextField( PlatformTextField(
controller: playlistName, controller: playlistName,
decoration: const InputDecoration( placeholder: "Name of the playlist",
hintText: "Name of the playlist", label: "Playlist Name",
label: Text("Playlist Name"),
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( PlatformTextField(
controller: description, controller: description,
decoration: const InputDecoration( placeholder: "Description...",
hintText: "Description...",
),
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: 5, maxLines: 5,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
CheckboxListTile( PlatformCheckbox(
value: public.value, value: public.value,
title: const Text("Public"), label: const PlatformText("Public"),
onChanged: (val) => public.value = val ?? false, onChanged: (val) => public.value = val ?? false,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
CheckboxListTile( PlatformCheckbox(
value: collaborative.value, value: collaborative.value,
title: const Text("Collaborative"), label: const PlatformText("Collaborative"),
onChanged: (val) => collaborative.value = val ?? false, onChanged: (val) => collaborative.value = val ?? false,
), ),
], ],

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -19,8 +20,8 @@ class PlaylistGenreView extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: BackButton(), leading: const PlatformBackButton(),
), ),
body: Column( body: Column(
children: [ children: [
@ -47,7 +48,7 @@ class PlaylistGenreView extends ConsumerWidget {
return const Center(child: Text("Error occurred")); return const Center(child: Text("Error occurred"));
} }
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const CircularProgressIndicator.adaptive(); return const PlatformCircularProgressIndicator();
} }
return Center( return Center(
child: Wrap( child: Wrap(

View File

@ -3,12 +3,14 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Shared/AnonymousFallback.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/TrackTile.dart';
import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/components/Shared/Waypoint.dart';
import 'package:spotube/hooks/useBreakpoints.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/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -56,10 +59,6 @@ class Search extends HookConsumerWidget {
job: searchQueryJob(SearchType.artist.key), job: searchQueryJob(SearchType.artist.key),
externalData: getVariables()); externalData: getVariables());
if (auth.isAnonymous) {
return const AnonymousFallback();
}
void onSearch() { void onSearch() {
for (final query in [ for (final query in [
searchTrack, searchTrack,
@ -75,300 +74,297 @@ class Search extends HookConsumerWidget {
} }
return SafeArea( return SafeArea(
child: Material( child: PlatformScaffold(
color: Theme.of(context).backgroundColor, appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null,
child: Column( body: auth.isAnonymous
children: [ ? const AnonymousFallback()
Container( : Column(
padding: const EdgeInsets.symmetric( children: [
horizontal: 20, Container(
vertical: 10, padding: const EdgeInsets.symmetric(
), horizontal: 20,
color: Theme.of(context).backgroundColor, vertical: 10,
child: TextField( ),
onChanged: (value) { child: PlatformTextField(
ref.read(searchTermStateProvider.notifier).state = value; onChanged: (value) {
}, ref.read(searchTermStateProvider.notifier).state =
decoration: InputDecoration( value;
isDense: true, },
suffix: ElevatedButton( prefixIcon: Icons.search_rounded,
onPressed: onSearch, prefixIconColor: PlatformProperty.only(
child: const Icon(Icons.search_rounded), ios:
), PlatformTheme.of(context).textTheme?.caption?.color,
contentPadding: const EdgeInsets.symmetric( other: null,
horizontal: 10, ).resolve(platform!),
vertical: 7, placeholder: "Search...",
), onSubmitted: (value) {
hintStyle: const TextStyle(height: 2), onSearch();
hintText: "Search...", },
),
onSubmitted: (value) {
onSearch();
},
),
),
HookBuilder(
builder: (context) {
Playback playback = ref.watch(playbackProvider);
List<AlbumSimple> albums = [];
List<Artist> artists = [];
List<Track> tracks = [];
List<PlaylistSimple> playlists = [];
final pages = [
...searchTrack.pages,
...searchAlbum.pages,
...searchPlaylist.pages,
...searchArtist.pages,
].expand<Page>((page) => page ?? []).toList();
for (MapEntry<int, Page> 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,
),
);
}),
],
),
),
),
),
],
),
), ),
), ),
); HookBuilder(
}, builder: (context) {
) Playback playback = ref.watch(playbackProvider);
], List<AlbumSimple> albums = [];
), List<Artist> artists = [];
List<Track> tracks = [];
List<PlaylistSimple> playlists = [];
final pages = [
...searchTrack.pages,
...searchAlbum.pages,
...searchPlaylist.pages,
...searchArtist.pages,
].expand<Page>((page) => page ?? []).toList();
for (MapEntry<int, Page> 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,
),
);
}),
],
),
),
),
),
],
),
),
),
);
},
)
],
),
), ),
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/components/Shared/Hyperlink.dart';
import 'package:spotube/hooks/usePackageInfo.dart'; import 'package:spotube/hooks/usePackageInfo.dart';
@ -29,9 +30,12 @@ class About extends HookWidget {
version: "2.5.0", version: "2.5.0",
); );
return ListTile( return PlatformListTile(
leading: const Icon(Icons.info_outline_rounded), leading: const Icon(Icons.info_outline_rounded),
title: const Text("About Spotube"), title: PlatformText(
"About Spotube",
style: PlatformTextTheme.of(context).body,
),
onTap: () { onTap: () {
showAboutDialog( showAboutDialog(
context: context, context: context,
@ -44,7 +48,7 @@ class About extends HookWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: const [ children: const [
Text("Author: "), PlatformText("Author: "),
Hyperlink( Hyperlink(
"Kingkor Roy Tirtho", "Kingkor Roy Tirtho",
"https://github.com/KRTirtho", "https://github.com/KRTirtho",
@ -59,12 +63,12 @@ class About extends HookWidget {
"💚 Sponsor/Donate 💚", "💚 Sponsor/Donate 💚",
"https://opencollective.com/spotube", "https://opencollective.com/spotube",
), ),
Text(""), PlatformText(""),
Hyperlink( Hyperlink(
"BSD-4-Clause LICENSE", "BSD-4-Clause LICENSE",
"https://github.com/KRTirtho/spotube/blob/master/LICENSE", "https://github.com/KRTirtho/spotube/blob/master/LICENSE",
), ),
Text(""), PlatformText(""),
Hyperlink( Hyperlink(
"Bug Report", "Bug Report",
"https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", "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 SizedBox(height: 10),
const Center(child: Text("© Spotube 2022. All rights reserved")) const Center(
child: PlatformText("© Spotube 2022. All rights reserved"))
]); ]);
}, },
); );

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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'; import 'package:spotube/provider/UserPreferences.dart';
final highContrast = MaterialColor( final highContrast = MaterialColor(
@ -65,16 +67,11 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
}, },
).key); ).key);
return AlertDialog( return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: Text("Pick ${schemeType.name} color scheme"), title: Text("Pick ${schemeType.name} color scheme"),
actions: [ primaryActions: [
TextButton( PlatformFilledButton(
child: const Text("Cancel"),
onPressed: () {
Navigator.pop(context);
},
),
ElevatedButton(
child: const Text("Save"), child: const Text("Save"),
onPressed: () { onPressed: () {
switch (schemeType) { switch (schemeType) {
@ -90,6 +87,15 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
}, },
) )
], ],
secondaryActions: [
PlatformFilledButton(
isSecondary: true,
child: const Text("Cancel"),
onPressed: () {
Navigator.pop(context);
},
),
],
content: SizedBox( content: SizedBox(
height: 200, height: 200,
width: 400, width: 400,

View File

@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/About.dart';
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart'; import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
import 'package:spotube/components/Shared/AdaptiveListTile.dart'; import 'package:spotube/components/Shared/AdaptiveListTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/main.dart';
import 'package:spotube/models/SpotifyMarkets.dart'; import 'package:spotube/models/SpotifyMarkets.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
@ -26,9 +28,7 @@ class Settings extends HookConsumerWidget {
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final pickColorScheme = useCallback((ColorSchemeType schemeType) { final pickColorScheme = useCallback((ColorSchemeType schemeType) {
return () => showDialog( return () => showPlatformAlertDialog(context, builder: (context) {
context: context,
builder: (context) {
return ColorSchemePickerDialog( return ColorSchemePickerDialog(
schemeType: schemeType, schemeType: schemeType,
); );
@ -48,12 +48,10 @@ class Settings extends HookConsumerWidget {
); );
return SafeArea( return SafeArea(
child: Scaffold( child: PlatformScaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
center: Text( center: PlatformText.headline("Settings"),
"Settings", centerTitle: true,
style: Theme.of(context).textTheme.headline5,
),
), ),
body: Row( body: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -63,10 +61,11 @@ class Settings extends HookConsumerWidget {
constraints: const BoxConstraints(maxWidth: 1366), constraints: const BoxConstraints(maxWidth: 1366),
child: ListView( child: ListView(
children: [ children: [
const Text( PlatformText(
" Account", " Account",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
if (auth.isAnonymous) if (auth.isAnonymous)
AdaptiveListTile( AdaptiveListTile(
@ -88,7 +87,7 @@ class Settings extends HookConsumerWidget {
), ),
), ),
), ),
trailing: (context, update) => ElevatedButton( trailing: (context, update) => PlatformFilledButton(
onPressed: () { onPressed: () {
GoRouter.of(context).push("/login"); 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) if (auth.isLoggedIn)
Builder(builder: (context) { Builder(builder: (context) {
Auth auth = ref.watch(authProvider); Auth auth = ref.watch(authProvider);
return ListTile( return PlatformListTile(
leading: const Icon(Icons.logout_rounded), leading: const Icon(Icons.logout_rounded),
title: const SizedBox( title: SizedBox(
height: 50, height: 50,
width: 180, width: 180,
child: Align( child: Align(
@ -115,10 +115,11 @@ class Settings extends HookConsumerWidget {
child: AutoSizeText( child: AutoSizeText(
"Log out of this account", "Log out of this account",
maxLines: 1, maxLines: 1,
style: PlatformTextTheme.of(context).body,
), ),
), ),
), ),
trailing: ElevatedButton( trailing: PlatformFilledButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor:
MaterialStateProperty.all(Colors.red), MaterialStateProperty.all(Colors.red),
@ -129,39 +130,41 @@ class Settings extends HookConsumerWidget {
auth.logout(); auth.logout();
GoRouter.of(context).pop(); GoRouter.of(context).pop();
}, },
child: const Text("Logout"), child: const PlatformText("Logout"),
), ),
); );
}), }),
const Text( PlatformText(
" Appearance", " Appearance",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon(Icons.dashboard_rounded), leading: const Icon(Icons.dashboard_rounded),
title: const Text("Layout Mode"), title: const PlatformText("Layout Mode"),
subtitle: const Text( subtitle: const PlatformText(
"Override responsive layout mode settings", "Override responsive layout mode settings",
), ),
trailing: (context, update) => DropdownButton<LayoutMode>( trailing: (context, update) =>
PlatformDropDownMenu<LayoutMode>(
value: preferences.layoutMode, value: preferences.layoutMode,
items: const [ items: [
DropdownMenuItem( PlatformDropDownMenuItem(
value: LayoutMode.adaptive, value: LayoutMode.adaptive,
child: Text( child: const PlatformText(
"Adaptive", "Adaptive",
), ),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: LayoutMode.compact, value: LayoutMode.compact,
child: Text( child: const PlatformText(
"Compact", "Compact",
), ),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: LayoutMode.extended, value: LayoutMode.extended,
child: Text("Extended"), child: const PlatformText("Extended"),
), ),
], ],
onChanged: (value) { onChanged: (value) {
@ -174,25 +177,22 @@ class Settings extends HookConsumerWidget {
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon(Icons.dark_mode_outlined), leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Theme"), title: const PlatformText("Theme"),
trailing: (context, update) => DropdownButton<ThemeMode>( trailing: (context, update) =>
PlatformDropDownMenu<ThemeMode>(
value: preferences.themeMode, value: preferences.themeMode,
items: const [ items: [
DropdownMenuItem( PlatformDropDownMenuItem(
value: ThemeMode.dark, value: ThemeMode.dark,
child: Text( child: const PlatformText("Dark"),
"Dark",
),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: ThemeMode.light, value: ThemeMode.light,
child: Text( child: const PlatformText("Light"),
"Light",
),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: ThemeMode.system, value: ThemeMode.system,
child: Text("System"), child: const PlatformText("System"),
), ),
], ],
onChanged: (value) { 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<TargetPlatform>(
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), leading: const Icon(Icons.palette_outlined),
title: const Text("Accent Color Scheme"), title: const PlatformText("Accent Color Scheme"),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 15, horizontal: 15,
vertical: 5, vertical: 5,
@ -217,9 +253,9 @@ class Settings extends HookConsumerWidget {
), ),
onTap: pickColorScheme(ColorSchemeType.accent), onTap: pickColorScheme(ColorSchemeType.accent),
), ),
ListTile( PlatformListTile(
leading: const Icon(Icons.format_color_fill_rounded), leading: const Icon(Icons.format_color_fill_rounded),
title: const Text("Background Color Scheme"), title: const PlatformText("Background Color Scheme"),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 15, horizontal: 15,
vertical: 5, vertical: 5,
@ -231,38 +267,38 @@ class Settings extends HookConsumerWidget {
), ),
onTap: pickColorScheme(ColorSchemeType.background), onTap: pickColorScheme(ColorSchemeType.background),
), ),
ListTile( PlatformListTile(
leading: const Icon(Icons.album_rounded), leading: const Icon(Icons.album_rounded),
title: const Text("Rotating Album Art"), title: const PlatformText("Rotating Album Art"),
trailing: Switch.adaptive( trailing: PlatformSwitch(
activeColor: Theme.of(context).primaryColor,
value: preferences.rotatingAlbumArt, value: preferences.rotatingAlbumArt,
onChanged: (state) { onChanged: (state) {
preferences.setRotatingAlbumArt(state); preferences.setRotatingAlbumArt(state);
}, },
), ),
), ),
const Text( PlatformText(
" Playback", " Playback",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon(Icons.multitrack_audio_rounded), leading: const Icon(Icons.multitrack_audio_rounded),
title: const Text("Audio Quality"), title: const PlatformText("Audio Quality"),
trailing: (context, update) => trailing: (context, update) =>
DropdownButton<AudioQuality>( PlatformDropDownMenu<AudioQuality>(
value: preferences.audioQuality, value: preferences.audioQuality,
items: const [ items: [
DropdownMenuItem( PlatformDropDownMenuItem(
value: AudioQuality.high, value: AudioQuality.high,
child: Text( child: const PlatformText(
"High", "High",
), ),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: AudioQuality.low, value: AudioQuality.low,
child: Text("Low"), child: const PlatformText("Low"),
), ),
], ],
onChanged: (value) { onChanged: (value) {
@ -274,60 +310,54 @@ class Settings extends HookConsumerWidget {
), ),
), ),
if (kIsMobile) if (kIsMobile)
ListTile( PlatformListTile(
leading: const Icon(Icons.download_for_offline_rounded), leading: const Icon(Icons.download_for_offline_rounded),
title: const Text( title: const PlatformText(
"Pre download and play", "Pre download and play",
), ),
subtitle: const Text( subtitle: const PlatformText(
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)", "Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
), ),
trailing: Switch.adaptive( trailing: PlatformSwitch(
activeColor: Theme.of(context).primaryColor,
value: preferences.androidBytesPlay, value: preferences.androidBytesPlay,
onChanged: (state) { onChanged: (state) {
preferences.setAndroidBytesPlay(state); preferences.setAndroidBytesPlay(state);
}, },
), ),
), ),
ListTile( PlatformListTile(
leading: const Icon(Icons.fast_forward_rounded), leading: const Icon(Icons.fast_forward_rounded),
title: const Text( title: const PlatformText(
"Skip non-music segments (SponsorBlock)", "Skip non-music segments (SponsorBlock)",
), ),
trailing: Switch.adaptive( trailing: PlatformSwitch(
activeColor: Theme.of(context).primaryColor,
value: preferences.skipSponsorSegments, value: preferences.skipSponsorSegments,
onChanged: (state) { onChanged: (state) {
preferences.setSkipSponsorSegments(state); preferences.setSkipSponsorSegments(state);
}, },
), ),
), ),
const Text( PlatformText(
" Search", " Search",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon(Icons.shopping_bag_rounded), leading: const Icon(Icons.shopping_bag_rounded),
title: Text( title: const PlatformText("Market Place"),
"Market Place", subtitle: PlatformText.caption(
style: Theme.of(context).textTheme.bodyText1,
),
subtitle: Text(
"Recommendation Country", "Recommendation Country",
style: Theme.of(context).textTheme.caption,
), ),
trailing: (context, update) => ConstrainedBox( trailing: (context, update) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250), constraints: const BoxConstraints(maxWidth: 350),
child: DropdownButton( child: PlatformDropDownMenu(
isExpanded: true,
value: preferences.recommendationMarket, value: preferences.recommendationMarket,
items: spotifyMarkets items: spotifyMarkets
.map( .map(
(country) => (DropdownMenuItem( (country) => (PlatformDropDownMenuItem(
value: country.first, value: country.first,
child: Text(country.last), child: PlatformText(country.last),
)), )),
) )
.toList(), .toList(),
@ -354,22 +384,19 @@ class Settings extends HookConsumerWidget {
), ),
), ),
), ),
subtitle: const Text("(Case sensitive)"), subtitle: const PlatformText("(Case sensitive)"),
breakOn: Breakpoints.lg, breakOn: Breakpoints.lg,
trailing: (context, update) => ConstrainedBox( trailing: (context, update) => ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450), constraints: const BoxConstraints(maxWidth: 450),
child: TextField( child: PlatformTextField(
controller: ytSearchFormatController, controller: ytSearchFormatController,
decoration: InputDecoration( suffix: PlatformFilledButton(
isDense: true, child: const Icon(Icons.save_rounded),
suffix: ElevatedButton( onPressed: () {
child: const Icon(Icons.save_rounded), preferences.setYtSearchFormat(
onPressed: () { ytSearchFormatController.value.text,
preferences.setYtSearchFormat( );
ytSearchFormatController.value.text, },
);
},
),
), ),
onSubmitted: (value) { onSubmitted: (value) {
preferences.setYtSearchFormat(value); preferences.setYtSearchFormat(value);
@ -380,7 +407,7 @@ class Settings extends HookConsumerWidget {
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon(Icons.low_priority_rounded), leading: const Icon(Icons.low_priority_rounded),
title: const SizedBox( title: SizedBox(
height: 50, height: 50,
width: 180, width: 180,
child: Align( child: Align(
@ -388,28 +415,29 @@ class Settings extends HookConsumerWidget {
child: AutoSizeText( child: AutoSizeText(
"Track Match Algorithm", "Track Match Algorithm",
maxLines: 1, maxLines: 1,
style: PlatformTextTheme.of(context).body,
), ),
), ),
), ),
trailing: (context, update) => trailing: (context, update) =>
DropdownButton<SpotubeTrackMatchAlgorithm>( PlatformDropDownMenu<SpotubeTrackMatchAlgorithm>(
value: preferences.trackMatchAlgorithm, value: preferences.trackMatchAlgorithm,
items: const [ items: [
DropdownMenuItem( PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.authenticPopular, value: SpotubeTrackMatchAlgorithm.authenticPopular,
child: Text( child: const PlatformText(
"Popular from Author", "Popular from Author",
), ),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.popular, value: SpotubeTrackMatchAlgorithm.popular,
child: Text( child: const PlatformText(
"Accurately Popular", "Accurately Popular",
), ),
), ),
DropdownMenuItem( PlatformDropDownMenuItem(
value: SpotubeTrackMatchAlgorithm.youtube, value: SpotubeTrackMatchAlgorithm.youtube,
child: Text("YouTube's Top choice"), child: const PlatformText("YouTube's Top choice"),
), ),
], ],
onChanged: (value) { onChanged: (value) {
@ -420,36 +448,38 @@ class Settings extends HookConsumerWidget {
}, },
), ),
), ),
const Text( PlatformText(
" Downloads", " Downloads",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
ListTile( PlatformListTile(
leading: const Icon(Icons.file_download_outlined), leading: const Icon(Icons.file_download_outlined),
title: const Text("Download Location"), title: const PlatformText("Download Location"),
subtitle: Text(preferences.downloadLocation), subtitle: PlatformText(preferences.downloadLocation),
trailing: ElevatedButton( trailing: PlatformFilledButton(
onPressed: pickDownloadLocation, onPressed: pickDownloadLocation,
child: const Icon(Icons.folder_rounded), child: const Icon(Icons.folder_rounded),
), ),
onTap: pickDownloadLocation, onTap: pickDownloadLocation,
), ),
ListTile( PlatformListTile(
leading: const Icon(Icons.lyrics_rounded), leading: const Icon(Icons.lyrics_rounded),
title: const Text("Download lyrics along with the Track"), title: const PlatformText(
trailing: Switch.adaptive( "Download lyrics along with the Track"),
activeColor: Theme.of(context).primaryColor, trailing: PlatformSwitch(
value: preferences.saveTrackLyrics, value: preferences.saveTrackLyrics,
onChanged: (state) { onChanged: (state) {
preferences.setSaveTrackLyrics(state); preferences.setSaveTrackLyrics(state);
}, },
), ),
), ),
const Text( PlatformText(
" About", " About",
style: style: PlatformTextTheme.of(context)
TextStyle(fontWeight: FontWeight.bold, fontSize: 20), .headline
?.copyWith(fontWeight: FontWeight.bold),
), ),
AdaptiveListTile( AdaptiveListTile(
leading: const Icon( leading: const Icon(
@ -471,13 +501,14 @@ class Settings extends HookConsumerWidget {
), ),
), ),
), ),
trailing: (context, update) => ElevatedButton.icon( trailing: (context, update) => PlatformFilledButton(
icon: const Icon(Icons.favorite_outline_rounded), style: ButtonStyle(
label: const Text("Please Sponsor/Donate"), backgroundColor:
style: ElevatedButton.styleFrom( MaterialStatePropertyAll(Colors.red[100]),
backgroundColor: Colors.red[100], foregroundColor:
foregroundColor: Colors.pinkAccent, const MaterialStatePropertyAll(Colors.pinkAccent),
padding: const EdgeInsets.all(15), padding: const MaterialStatePropertyAll(
EdgeInsets.all(15)),
), ),
onPressed: () { onPressed: () {
launchUrlString( launchUrlString(
@ -485,13 +516,20 @@ class Settings extends HookConsumerWidget {
mode: LaunchMode.externalApplication, 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), leading: const Icon(Icons.update_rounded),
title: const Text("Check for Update"), title: const PlatformText("Check for Update"),
trailing: Switch.adaptive( trailing: PlatformSwitch(
activeColor: Theme.of(context).primaryColor,
value: preferences.checkUpdate, value: preferences.checkUpdate,
onChanged: (checked) => onChanged: (checked) =>
preferences.setCheckUpdate(checked), preferences.setCheckUpdate(checked),

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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'; import 'package:spotube/hooks/useBreakpoints.dart';
class AdaptiveListTile extends HookWidget { class AdaptiveListTile extends HookWidget {
@ -24,7 +26,7 @@ class AdaptiveListTile extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
return ListTile( return PlatformListTile(
title: title, title: title,
subtitle: subtitle, subtitle: subtitle,
trailing: trailing:
@ -33,11 +35,13 @@ class AdaptiveListTile extends HookWidget {
onTap: breakpoint.isLessThan(breakOn) onTap: breakpoint.isLessThan(breakOn)
? () { ? () {
onTap?.call(); onTap?.call();
showDialog( showPlatformAlertDialog(
context: context, context,
barrierDismissible: true,
builder: (context) { builder: (context) {
return StatefulBuilder(builder: (context, update) { return StatefulBuilder(builder: (context, update) {
return AlertDialog( return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: title != null title: title != null
? Row( ? Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -49,7 +53,7 @@ class AdaptiveListTile extends HookWidget {
Flexible(child: title!), Flexible(child: title!),
], ],
) )
: null, : Container(),
content: trailing?.call(context, update), content: trailing?.call(context, update),
); );
}); });

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:popover/popover.dart'; import 'package:popover/popover.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
@ -19,28 +20,30 @@ class Action extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isExpanded != true) { if (isExpanded != true) {
return Tooltip( return PlatformIconButton(
message: text.toStringShallow().split(",").last.replaceAll( icon: icon,
"\"", onPressed: onPressed,
"", tooltip: text is Text
), ? (text as Text).data
child: IconButton( : text.toStringShallow().split(",").last.replaceAll(
icon: icon, "\"",
onPressed: onPressed, "",
), ),
); );
} }
return TextButton.icon( return PlatformTextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, foregroundColor: Theme.of(context).textTheme.bodyMedium?.color,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
), ),
icon: icon,
label: Align(
alignment: Alignment.centerLeft,
child: text,
),
onPressed: onPressed, onPressed: onPressed,
child: Row(
children: [
icon,
const SizedBox(width: 10),
text,
],
),
); );
} }
} }
@ -59,7 +62,7 @@ class AdaptiveActions extends HookWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
if (breakpoint.isLessThan(breakOn)) { if (breakpoint.isLessThan(breakOn)) {
return IconButton( return PlatformIconButton(
icon: const Icon(Icons.more_horiz), icon: const Icon(Icons.more_horiz),
onPressed: () { onPressed: () {
showPopover( showPopover(

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
class AnchorButton<T> extends HookWidget { class AnchorButton<T> extends HookWidget {
final String text; final String text;
@ -28,7 +29,7 @@ class AnchorButton<T> extends HookWidget {
onTap: onTap, onTap: onTap,
child: MouseRegion( child: MouseRegion(
cursor: MaterialStateMouseCursor.clickable, cursor: MaterialStateMouseCursor.clickable,
child: Text( child: PlatformText(
text, text,
style: style.copyWith( style: style.copyWith(
decoration: decoration:

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -19,10 +20,10 @@ class AnonymousFallback extends ConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text("You're not logged in"), const PlatformText("You're not logged in"),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( PlatformFilledButton(
child: const Text("Login with Spotify"), child: const PlatformText("Login with Spotify"),
onPressed: () => ServiceUtils.navigate(context, "/settings"), onPressed: () => ServiceUtils.navigate(context, "/settings"),
) )
], ],

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; 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'; import 'package:spotube/components/Shared/UniversalImage.dart';
class DownloadConfirmationDialog extends StatelessWidget { class DownloadConfirmationDialog extends StatelessWidget {
@ -6,22 +8,25 @@ class DownloadConfirmationDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return PlatformAlertDialog(
contentPadding: const EdgeInsets.all(15), macosAppIcon: Sidebar.brandLogo(),
title: Row( title: Padding(
children: const [ padding: const EdgeInsets.all(15),
Text("Are you sure?"), child: Row(
SizedBox(width: 10), children: const [
UniversalImage( Text("Are you sure?"),
path: SizedBox(width: 10),
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif", UniversalImage(
height: 40, path:
width: 40, "https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
) height: 40,
], width: 40,
)
],
),
), ),
content: ConstrainedBox( content: Padding(
constraints: const BoxConstraints(maxWidth: 400), padding: const EdgeInsets.all(15),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -56,19 +61,22 @@ class DownloadConfirmationDialog extends StatelessWidget {
), ),
), ),
), ),
actions: [ primaryActions: [
ElevatedButton( 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"), child: const Text("Decline"),
onPressed: () => Navigator.of(context).pop(false), 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"),
)
], ],
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
@ -32,7 +33,7 @@ class HeartButton extends ConsumerWidget {
if (!auth.isLoggedIn) return Container(); if (!auth.isLoggedIn) return Container();
return IconButton( return PlatformIconButton(
tooltip: tooltip, tooltip: tooltip,
icon: Icon( icon: Icon(
icon ?? icon ??
@ -121,7 +122,7 @@ class TrackHeartButton extends HookConsumerWidget {
); );
final toggler = useTrackToggleLike(track, ref); final toggler = useTrackToggleLike(track, ref);
if (toggler.item3.isLoading || !toggler.item3.hasData) { if (toggler.item3.isLoading || !toggler.item3.hasData) {
return const CircularProgressIndicator(); return const PlatformCircularProgressIndicator();
} }
return HeartButton( return HeartButton(
@ -181,7 +182,8 @@ class PlaylistHeartButton extends HookConsumerWidget {
titleImage, titleImage,
).dominantColor; ).dominantColor;
if (me.isLoading || !me.hasData) return const CircularProgressIndicator(); if (me.isLoading || !me.hasData)
return const PlatformCircularProgressIndicator();
return HeartButton( return HeartButton(
isLiked: isLikedQuery.data ?? false, 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( return HeartButton(
isLiked: isLiked, isLiked: isLiked,

View File

@ -1,130 +1,35 @@
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class TitleBarActionButtons extends StatelessWidget { class PageWindowTitleBar extends PlatformAppBar {
final Color? color; PageWindowTitleBar({
const TitleBarActionButtons({ super.backgroundColor,
Key? key, List<Widget>? actions,
this.color, super.actionsIconTheme,
}) : super(key: key); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextButtonTheme( return MoveWindow(child: super.build(context));
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)
],
),
),
);
} }
} }

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; 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/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.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()? onTap;
final void Function()? onPlaybuttonPressed; final void Function()? onPlaybuttonPressed;
final String? description; final String? description;
@ -26,26 +29,65 @@ class PlaybuttonCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backgroundColor = PlatformTheme.of(context).secondaryBackgroundColor;
final boxShadow = usePlatformProperty<BoxShadow?>(
(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<InteractiveInkFeatureFactory?>(
(context) => PlatformProperty.only(
android: InkRipple.splashFactory,
other: NoSplash.splashFactory,
),
);
final iconBgColor = PlatformTheme.of(context).primaryColor;
return Container( return Container(
margin: margin, margin: margin,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
splashFactory: splash,
highlightColor: Colors.black12,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: HoverBuilder(builder: (context, isHovering) { child: HoverBuilder(builder: (context, isHovering) {
return Ink( return Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).backgroundColor, color: backgroundColor,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(
[TargetPlatform.windows, TargetPlatform.linux]
.contains(platform)
? 5
: 8,
),
boxShadow: [ boxShadow: [
BoxShadow( if (boxShadow != null) boxShadow,
blurRadius: 10,
offset: const Offset(0, 3),
spreadRadius: 5,
color: Theme.of(context).shadowColor,
)
], ],
border: [TargetPlatform.windows, TargetPlatform.macOS]
.contains(platform)
? Border.all(
color: PlatformTheme.of(context).borderColor ??
Colors.transparent,
width: 1,
)
: null,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -53,13 +95,23 @@ class PlaybuttonCard extends StatelessWidget {
// thumbnail of the playlist // thumbnail of the playlist
Stack( Stack(
children: [ children: [
ClipRRect( Padding(
borderRadius: BorderRadius.circular(8), padding: EdgeInsets.all(
child: UniversalImage( platform == TargetPlatform.windows ? 5 : 0,
path: imageUrl, ),
width: 200, child: ClipRRect(
placeholder: (context, url) => borderRadius: BorderRadius.circular(
Image.asset("assets/placeholder.png"), [TargetPlatform.windows, TargetPlatform.linux]
.contains(platform)
? 5
: 8,
),
child: UniversalImage(
path: imageUrl,
width: 200,
placeholder: (context, url) =>
Image.asset("assets/placeholder.png"),
),
), ),
), ),
Positioned.directional( Positioned.directional(
@ -67,27 +119,32 @@ class PlaybuttonCard extends StatelessWidget {
bottom: 10, bottom: 10,
end: 5, end: 5,
child: Builder(builder: (context) { child: Builder(builder: (context) {
return ElevatedButton( return Container(
onPressed: onPlaybuttonPressed, decoration: BoxDecoration(
style: ButtonStyle( color: iconBgColor,
shape: MaterialStateProperty.all( shape: BoxShape.circle,
const CircleBorder(), ),
), child: PlatformIconButton(
padding: MaterialStateProperty.all( onPressed: onPlaybuttonPressed,
const EdgeInsets.all(16), 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, height: 30,
child: SpotubeMarqueeText( child: SpotubeMarqueeText(
text: description!, text: description!,
style: TextStyle( style: PlatformTextTheme.of(context).caption,
fontSize: 13,
color: Theme.of(context)
.textTheme
.headline4
?.color,
),
isHovering: isHovering, isHovering: isHovering,
), ),
), ),

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home/Sidebar.dart';
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null); final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
@ -13,7 +15,8 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final groupValue = ref.watch(replaceDownloadedFileState); final groupValue = ref.watch(replaceDownloadedFileState);
return AlertDialog( return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: Text("Track ${track.name} Already Exists"), title: Text("Track ${track.name} Already Exists"),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -47,20 +50,23 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget {
), ),
], ],
), ),
actions: [ primaryActions: [
TextButton( PlatformFilledButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
TextButton(
child: const Text("Yes"), child: const Text("Yes"),
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.pop(context, true);
}, },
) )
], ],
secondaryActions: [
PlatformFilledButton(
isSecondary: true,
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
],
); );
} }
} }

View File

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

View File

@ -26,12 +26,12 @@ class SpotubeMarqueeText extends HookWidget {
return AutoSizeText( return AutoSizeText(
text, text,
minFontSize: 13, minFontSize: 13,
style: style, style: DefaultTextStyle.of(context).style.merge(style),
maxLines: 1, maxLines: 1,
overflowReplacement: Marquee( overflowReplacement: Marquee(
key: uKey.value, key: uKey.value,
text: text, text: text,
style: style, style: DefaultTextStyle.of(context).style.merge(style),
scrollAxis: Axis.horizontal, scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 40.0, blankSpace: 40.0,

View File

@ -2,6 +2,7 @@ import 'package:fl_query/fl_query.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/LoaderShimmers/ShimmerTrackTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
@ -59,7 +60,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
final List<Widget> buttons = [ final List<Widget> buttons = [
if (showShare) if (showShare)
IconButton( PlatformIconButton(
icon: Icon( icon: Icon(
Icons.share_rounded, Icons.share_rounded,
color: color?.titleTextColor, color: color?.titleTextColor,
@ -71,13 +72,9 @@ class TrackCollectionView<T> extends HookConsumerWidget {
// play playlist // play playlist
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 10), margin: const EdgeInsets.symmetric(vertical: 10),
child: ElevatedButton( child: PlatformFilledButton(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: shape: MaterialStateProperty.all(const CircleBorder()),
MaterialStateProperty.all(Theme.of(context).primaryColor),
shape: MaterialStateProperty.all(
const CircleBorder(),
),
), ),
onPressed: tracksSnapshot.data != null ? onPlay : null, onPressed: tracksSnapshot.data != null ? onPlay : null,
child: Icon( child: Icon(
@ -112,14 +109,12 @@ class TrackCollectionView<T> extends HookConsumerWidget {
}, [collapsed.value]); }, [collapsed.value]);
return SafeArea( return SafeArea(
child: Scaffold( child: PlatformScaffold(
appBar: kIsDesktop appBar: kIsDesktop
? PageWindowTitleBar( ? PageWindowTitleBar(
backgroundColor: color?.color, backgroundColor: color?.color,
foregroundColor: color?.titleTextColor, foregroundColor: color?.titleTextColor,
leading: Row( leading: PlatformBackButton(color: color?.titleTextColor),
children: [BackButton(color: color?.titleTextColor)],
),
) )
: null, : null,
body: CustomScrollView( body: CustomScrollView(
@ -131,16 +126,19 @@ class TrackCollectionView<T> extends HookConsumerWidget {
pinned: true, pinned: true,
expandedHeight: 400, expandedHeight: 400,
automaticallyImplyLeading: kIsMobile, automaticallyImplyLeading: kIsMobile,
leading: kIsMobile
? PlatformBackButton(color: color?.titleTextColor)
: null,
iconTheme: IconThemeData(color: color?.titleTextColor), iconTheme: IconThemeData(color: color?.titleTextColor),
primary: true, primary: true,
backgroundColor: color?.color, backgroundColor: color?.color,
title: collapsed.value title: collapsed.value
? Text( ? PlatformText.headline(
title, title,
style: Theme.of(context).textTheme.headline4?.copyWith( style: TextStyle(
color: color?.titleTextColor, color: color?.titleTextColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
) )
: null, : null,
flexibleSpace: LayoutBuilder(builder: (context, constrains) { flexibleSpace: LayoutBuilder(builder: (context, constrains) {
@ -158,6 +156,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
), ),
), ),
child: Material( child: Material(
textStyle: PlatformTheme.of(context).textTheme!.body!,
type: MaterialType.transparency, type: MaterialType.transparency,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -191,25 +190,19 @@ class TrackCollectionView<T> extends HookConsumerWidget {
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
Text( PlatformText.headline(
title, title,
style: Theme.of(context) style: TextStyle(
.textTheme color: color?.titleTextColor,
.headline4 fontWeight: FontWeight.w600,
?.copyWith( ),
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
), ),
if (description != null) if (description != null)
Text( PlatformText(
description!, description!,
style: Theme.of(context) style: TextStyle(
.textTheme color: color?.bodyTextColor,
.bodyLarge ),
?.copyWith(
color: color?.bodyTextColor,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
), ),
@ -235,7 +228,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
} else if (tracksSnapshot.hasError && } else if (tracksSnapshot.hasError &&
tracksSnapshot.isError) { tracksSnapshot.isError) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Text("Error ${tracksSnapshot.error}")); child: PlatformText("Error ${tracksSnapshot.error}"));
} }
final tracks = tracksSnapshot.data!; final tracks = tracksSnapshot.data!;

View File

@ -2,7 +2,9 @@ import 'package:flutter/material.dart' hide Action;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart' hide Image; 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/AdaptivePopupMenuButton.dart';
import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotube/components/Shared/LinkText.dart';
@ -73,7 +75,7 @@ class TrackTile extends HookConsumerWidget {
SnackBar( SnackBar(
width: 300, width: 300,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
content: Text( content: PlatformText(
"Copied $data to clipboard", "Copied $data to clipboard",
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -83,81 +85,78 @@ class TrackTile extends HookConsumerWidget {
} }
Future<void> actionAddToPlaylist() async { Future<void> actionAddToPlaylist() async {
showDialog( showPlatformAlertDialog(context, builder: (context) {
context: context, return FutureBuilder<Iterable<PlaylistSimple>>(
builder: (context) { future: spotify.playlists.me.all().then((playlists) async {
return FutureBuilder<Iterable<PlaylistSimple>>( final me = await spotify.me.get();
future: spotify.playlists.me.all().then((playlists) async { return playlists.where((playlist) =>
final me = await spotify.me.get(); playlist.owner?.id != null && playlist.owner!.id == me.id);
return playlists.where((playlist) => }),
playlist.owner?.id != null && builder: (context, snapshot) {
playlist.owner!.id == me.id); return HookBuilder(builder: (context) {
}), final playlistsCheck = useState(<String, bool>{});
builder: (context, snapshot) { return PlatformAlertDialog(
return HookBuilder(builder: (context) { macosAppIcon: Sidebar.brandLogo(),
final playlistsCheck = useState(<String, bool>{}); title: PlatformText(
return AlertDialog( "Add `${track.value.name}` to following Playlists",
title: Text( style: const TextStyle(
"Add `${track.value.name}` to following Playlists"), fontSize: 18,
titleTextStyle: fontWeight: FontWeight.bold,
Theme.of(context).textTheme.bodyText1?.copyWith( ),
fontSize: 18, ),
fontWeight: FontWeight.bold, secondaryActions: [
), PlatformFilledButton(
actions: [ isSecondary: true,
TextButton( child: const PlatformText("Cancel"),
child: const Text("Cancel"), onPressed: () => Navigator.pop(context),
onPressed: () => Navigator.pop(context), ),
), ],
ElevatedButton( primaryActions: [
child: const Text("Add"), PlatformFilledButton(
onPressed: () async { child: const PlatformText("Add"),
final selectedPlaylists = playlistsCheck onPressed: () async {
.value.entries final selectedPlaylists = playlistsCheck.value.entries
.where((entry) => entry.value) .where((entry) => entry.value)
.map((entry) => entry.key); .map((entry) => entry.key);
await Future.wait( await Future.wait(
selectedPlaylists.map( selectedPlaylists.map(
(playlistId) => spotify.playlists (playlistId) => spotify.playlists
.addTrack(track.value.uri!, playlistId), .addTrack(track.value.uri!, playlistId),
), ),
).then((_) => Navigator.pop(context)); ).then((_) => Navigator.pop(context));
}, },
) )
], ],
content: SizedBox( content: SizedBox(
height: 300, height: 300,
width: 300, width: 300,
child: !snapshot.hasData child: !snapshot.hasData
? const Center( ? const Center(
child: CircularProgressIndicator.adaptive()) child: PlatformCircularProgressIndicator())
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final playlist = final playlist = snapshot.data!.elementAt(index);
snapshot.data!.elementAt(index); return PlatformCheckbox(
return CheckboxListTile( label: PlatformText(playlist.name!),
title: Text(playlist.name!), value:
controlAffinity: playlistsCheck.value[playlist.id] ?? false,
ListTileControlAffinity.leading, onChanged: (val) {
value: playlistsCheck.value[playlist.id] ?? playlistsCheck.value = {
false, ...playlistsCheck.value,
onChanged: (val) { playlist.id!: val == true
playlistsCheck.value = { };
...playlistsCheck.value,
playlist.id!: val == true
};
},
);
}, },
), );
), },
); ),
}); ),
}); );
}); });
});
});
} }
final String thumbnailUrl = TypeConversionUtils.image_X_UrlString( final String thumbnailUrl = TypeConversionUtils.image_X_UrlString(
@ -178,10 +177,11 @@ class TrackTile extends HookConsumerWidget {
), ),
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: PlatformTheme.of(context).textTheme!.body!,
child: Row( child: Row(
children: [ children: [
if (showCheck) if (showCheck)
Checkbox( PlatformCheckbox(
value: isChecked, value: isChecked,
onChanged: (s) => onCheckChange?.call(s), onChanged: (s) => onCheckChange?.call(s),
) )
@ -190,7 +190,7 @@ class TrackTile extends HookConsumerWidget {
height: 20, height: 20,
width: 25, width: 25,
child: Center( child: Center(
child: Text((track.key + 1).toString()), child: PlatformText((track.key + 1).toString()),
), ),
), ),
Padding( Padding(
@ -214,23 +214,29 @@ class TrackTile extends HookConsumerWidget {
), ),
), ),
), ),
IconButton( Padding(
icon: Icon( padding: const EdgeInsets.all(8.0).copyWith(left: 0),
playback.track?.id != null && child: PlatformIconButton(
playback.track?.id == track.value.id icon: Icon(
? Icons.pause_circle_rounded playback.track?.id != null &&
: Icons.play_circle_rounded, playback.track?.id == track.value.id
color: Theme.of(context).primaryColor, ? Icons.pause_rounded
), : Icons.play_arrow_rounded,
onPressed: () => onTrackPlayButtonPressed?.call( color: Colors.white,
track.value, ),
backgroundColor: PlatformTheme.of(context).primaryColor,
hoverColor:
PlatformTheme.of(context).primaryColor?.withOpacity(0.5),
onPressed: () => onTrackPlayButtonPressed?.call(
track.value,
),
), ),
), ),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( PlatformText(
track.value.name ?? "", track.value.name ?? "",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -239,7 +245,7 @@ class TrackTile extends HookConsumerWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
isReallyLocal isReallyLocal
? Text( ? PlatformText(
TypeConversionUtils.artists_X_String<Artist>( TypeConversionUtils.artists_X_String<Artist>(
track.value.artists ?? []), track.value.artists ?? []),
) )
@ -255,7 +261,7 @@ class TrackTile extends HookConsumerWidget {
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
Expanded( Expanded(
child: isReallyLocal child: isReallyLocal
? Text(track.value.album?.name ?? "") ? PlatformText(track.value.album?.name ?? "")
: LinkText( : LinkText(
track.value.album!.name!, track.value.album!.name!,
"/album/${track.value.album?.id}", "/album/${track.value.album?.id}",
@ -265,7 +271,7 @@ class TrackTile extends HookConsumerWidget {
), ),
if (!breakpoint.isSm) ...[ if (!breakpoint.isSm) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Text(duration), PlatformText(duration),
], ],
const SizedBox(width: 10), const SizedBox(width: 10),
if (!isReallyLocal) if (!isReallyLocal)
@ -279,7 +285,7 @@ class TrackTile extends HookConsumerWidget {
color: Colors.pink, color: Colors.pink,
) )
: const Icon(Icons.favorite_border_rounded), : const Icon(Icons.favorite_border_rounded),
text: const Text("Save as favorite"), text: const PlatformText("Save as favorite"),
onPressed: () { onPressed: () {
toggler.item2.mutate(Tuple2(spotify, toggler.item1)); toggler.item2.mutate(Tuple2(spotify, toggler.item1));
}, },
@ -287,18 +293,18 @@ class TrackTile extends HookConsumerWidget {
if (auth.isLoggedIn) if (auth.isLoggedIn)
Action( Action(
icon: const Icon(Icons.add_box_rounded), icon: const Icon(Icons.add_box_rounded),
text: const Text("Add To playlist"), text: const PlatformText("Add To playlist"),
onPressed: actionAddToPlaylist, onPressed: actionAddToPlaylist,
), ),
if (userPlaylist && auth.isLoggedIn) if (userPlaylist && auth.isLoggedIn)
Action( Action(
icon: const Icon(Icons.remove_circle_outline_rounded), icon: const Icon(Icons.remove_circle_outline_rounded),
text: const Text("Remove from playlist"), text: const PlatformText("Remove from playlist"),
onPressed: actionRemoveFromPlaylist, onPressed: actionRemoveFromPlaylist,
), ),
Action( Action(
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),
text: const Text("Share"), text: const PlatformText("Share"),
onPressed: () { onPressed: () {
actionShare(track.value); actionShare(track.value);
}, },

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart'; import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart';
@ -67,7 +68,7 @@ class TracksTableView extends HookConsumerWidget {
if (heading != null) heading!, if (heading != null) heading!,
Row( Row(
children: [ children: [
Checkbox( PlatformCheckbox(
value: selected.value.length == sortedTracks.length, value: selected.value.length == sortedTracks.length,
onChanged: (checked) { onChanged: (checked) {
if (!showCheck.value) showCheck.value = true; if (!showCheck.value) showCheck.value = true;
@ -81,7 +82,7 @@ class TracksTableView extends HookConsumerWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: PlatformText(
"#", "#",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: tableHeadStyle, style: tableHeadStyle,
@ -90,7 +91,7 @@ class TracksTableView extends HookConsumerWidget {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Text( PlatformText(
"Title", "Title",
style: tableHeadStyle, style: tableHeadStyle,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -104,7 +105,7 @@ class TracksTableView extends HookConsumerWidget {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Text( PlatformText(
"Album", "Album",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: tableHeadStyle, style: tableHeadStyle,
@ -115,7 +116,7 @@ class TracksTableView extends HookConsumerWidget {
], ],
if (!breakpoint.isSm) ...[ if (!breakpoint.isSm) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Text("Time", style: tableHeadStyle), PlatformText("Time", style: tableHeadStyle),
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
SortTracksDropdown( SortTracksDropdown(
@ -126,32 +127,29 @@ class TracksTableView extends HookConsumerWidget {
.state = value; .state = value;
}, },
), ),
PopupMenuButton( PlatformPopupMenuButton(
itemBuilder: (context) { items: [
return [ PlatformPopupMenuItem(
PopupMenuItem( enabled: selected.value.isNotEmpty,
enabled: selected.value.isNotEmpty, value: "download",
value: "download", child: Row(
child: Row( children: [
children: [ const Icon(Icons.file_download_outlined),
const Icon(Icons.file_download_outlined), PlatformText(
Text( "Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}", ),
), ],
],
),
), ),
]; ),
}, ],
onSelected: (action) async { onSelected: (action) async {
switch (action) { switch (action) {
case "download": case "download":
{ {
final isConfirmed = await showDialog( final isConfirmed = await showPlatformAlertDialog(
context: context, context, builder: (context) {
builder: (context) { return const DownloadConfirmationDialog();
return const DownloadConfirmationDialog(); });
});
if (isConfirmed != true) return; if (isConfirmed != true) return;
for (final selectedTrack in selectedTracks) { for (final selectedTrack in selectedTracks) {
downloader.addToQueue(selectedTrack); downloader.addToQueue(selectedTrack);
@ -163,6 +161,7 @@ class TracksTableView extends HookConsumerWidget {
default: default:
} }
}, },
child: const Icon(Icons.more_vert),
), ),
], ],
), ),

View File

@ -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<T>(
PlatformProperty<T> Function(BuildContext context) getProperties) {
final context = useContext();
return getProperties(context).resolve(platform ?? Theme.of(context).platform);
}

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; 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/components/Shared/AnchorButton.dart';
import 'package:spotube/hooks/usePackageInfo.dart'; import 'package:spotube/hooks/usePackageInfo.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -51,41 +53,40 @@ void useUpdateChecker(WidgetRef ref) {
final latestVersion = value.last; final latestVersion = value.last;
if (currentVersion == null || latestVersion == null) return; if (currentVersion == null || latestVersion == null) return;
if (latestVersion <= currentVersion) return; if (latestVersion <= currentVersion) return;
showDialog( showPlatformAlertDialog(context, builder: (context) {
context: context, const url =
builder: (context) { "https://spotube.netlify.app/other-downloads/stable-downloads";
const url = return PlatformAlertDialog(
"https://spotube.netlify.app/other-downloads/stable-downloads"; macosAppIcon: Sidebar.brandLogo(),
return AlertDialog( title: const PlatformText("Spotube has an update"),
title: const Text("Spotube has an update"), primaryActions: [
actions: [ PlatformFilledButton(
ElevatedButton( child: const Text("Download Now"),
child: const Text("Download Now"), onPressed: () => download(url),
onPressed: () => download(url), ),
), ],
], content: Column(
content: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
Text("Spotube v${value.last} has been released"),
Row(
children: [ children: [
Text("Spotube v${value.last} has been released"), const PlatformText("Read the latest "),
Row( AnchorButton(
children: [ "release notes",
const Text("Read the latest "), style: const TextStyle(color: Colors.blue),
AnchorButton( onTap: () => launchUrlString(
"release notes", url,
style: const TextStyle(color: Colors.blue), mode: LaunchMode.externalApplication,
onTap: () => launchUrlString( ),
url,
mode: LaunchMode.externalApplication,
),
),
],
), ),
], ],
), ),
); ],
}); ),
);
});
}); });
return null; return null;
}, [packageInfo, isCheckUpdateEnabled]); }, [packageInfo, isCheckUpdateEnabled]);

View File

@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart'; import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
import 'package:spotube/entities/CacheTrack.dart'; import 'package:spotube/entities/CacheTrack.dart';
@ -106,8 +107,8 @@ void main() async {
logger.v( logger.v(
"[onFileExists] download confirmation for ${track.name}", "[onFileExists] download confirmation for ${track.name}",
); );
return showDialog<bool>( return showPlatformAlertDialog<bool>(
context: context, context,
builder: (_) => builder: (_) =>
ReplaceDownloadedFileDialog(track: track), ReplaceDownloadedFileDialog(track: track),
).then((s) => s ?? false); ).then((s) => s ?? false);
@ -140,6 +141,11 @@ class Spotube extends StatefulHookConsumerWidget {
@override @override
SpotubeState createState() => SpotubeState(); SpotubeState createState() => SpotubeState();
/// ADDED
/// InheritedWidget style accessor to our State object.
static SpotubeState of(BuildContext context) =>
context.findAncestorStateOfType<SpotubeState>()!;
} }
class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver { class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
@ -153,6 +159,11 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
super.initState(); super.initState();
SharedPreferences.getInstance().then(((value) => localStorage = value)); SharedPreferences.getInstance().then(((value) => localStorage = value));
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
appPlatform = Theme.of(context).platform;
});
});
} }
@override @override
@ -180,6 +191,13 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
prevSize = appWindow.size; prevSize = appWindow.size;
} }
TargetPlatform appPlatform = TargetPlatform.android;
void changePlatform(TargetPlatform targetPlatform) {
appPlatform = targetPlatform;
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeMode = final themeMode =
@ -198,57 +216,82 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
}; };
}, []); }, []);
return MaterialApp.router( platform = appPlatform;
routerConfig: router,
return PlatformApp.router(
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
routeInformationProvider: router.routeInformationProvider,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Spotube', title: 'Spotube',
theme: lightTheme( androidTheme: lightTheme(
accentMaterialColor: accentMaterialColor, accentMaterialColor: accentMaterialColor,
backgroundMaterialColor: backgroundMaterialColor, backgroundMaterialColor: backgroundMaterialColor,
), ),
darkTheme: darkTheme( androidDarkTheme: darkTheme(
accentMaterialColor: accentMaterialColor, accentMaterialColor: accentMaterialColor,
backgroundMaterialColor: backgroundMaterialColor, backgroundMaterialColor: backgroundMaterialColor,
), ),
linuxTheme: linuxTheme,
linuxDarkTheme: linuxDarkTheme,
iosTheme: themeMode == ThemeMode.dark ? iosDarkTheme : iosTheme,
windowsTheme: windowsTheme,
windowsDarkTheme: windowsDarkTheme,
macosTheme: macosTheme,
macosDarkTheme: macosDarkTheme,
themeMode: themeMode, themeMode: themeMode,
shortcuts: { windowButtonConfig: kIsDesktop
...WidgetsApp.defaultShortcuts, ? PlatformWindowButtonConfig(
const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref), isMaximized: () => appWindow.isMaximized,
const SingleActivator(LogicalKeyboardKey.comma, control: true): 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"), NavigationIntent(router, "/settings"),
const SingleActivator( LogicalKeySet(
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
control: true, LogicalKeyboardKey.control,
shift: true, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.browse), ): HomeTabIntent(ref, tab: HomeTabs.browse),
const SingleActivator( LogicalKeySet(
LogicalKeyboardKey.keyS, LogicalKeyboardKey.keyS,
control: true, LogicalKeyboardKey.control,
shift: true, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.search), ): HomeTabIntent(ref, tab: HomeTabs.search),
const SingleActivator( LogicalKeySet(
LogicalKeyboardKey.keyL, LogicalKeyboardKey.keyL,
control: true, LogicalKeyboardKey.control,
shift: true, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.library), ): HomeTabIntent(ref, tab: HomeTabs.library),
const SingleActivator( LogicalKeySet(
LogicalKeyboardKey.keyY, LogicalKeyboardKey.keyY,
control: true, LogicalKeyboardKey.control,
shift: true, LogicalKeyboardKey.shift,
): HomeTabIntent(ref, tab: HomeTabs.lyrics), ): HomeTabIntent(ref, tab: HomeTabs.lyrics),
const SingleActivator( LogicalKeySet(
LogicalKeyboardKey.keyW, LogicalKeyboardKey.keyW,
control: true, LogicalKeyboardKey.control,
shift: true, LogicalKeyboardKey.shift,
): CloseAppIntent(), ): CloseAppIntent(),
}, }),
actions: { actions: PlatformProperty.all({
...WidgetsApp.defaultActions, ...WidgetsApp.defaultActions,
PlayPauseIntent: PlayPauseAction(), PlayPauseIntent: PlayPauseAction(),
NavigationIntent: NavigationAction(), NavigationIntent: NavigationAction(),
HomeTabIntent: HomeTabAction(), HomeTabIntent: HomeTabAction(),
CloseAppIntent: CloseAppAction(), CloseAppIntent: CloseAppAction(),
}, }),
); );
} }
} }

View File

@ -72,7 +72,7 @@ ThemeData darkTheme({
), ),
dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]), dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]),
cardColor: backgroundMaterialColor[800], cardColor: backgroundMaterialColor[800],
canvasColor: backgroundMaterialColor[900], canvasColor: backgroundMaterialColor[800],
listTileTheme: const ListTileThemeData(horizontalTitleGap: 0), listTileTheme: const ListTileThemeData(horizontalTitleGap: 0),
checkboxTheme: CheckboxThemeData( checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith((states) { fillColor: MaterialStateProperty.resolveWith((states) {
@ -89,5 +89,8 @@ ThemeData darkTheme({
unselectedLabelStyle: unselectedLabelStyle:
const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
), ),
appBarTheme: AppBarTheme(
backgroundColor: backgroundMaterialColor[900],
),
); );
} }

View File

@ -1,5 +1,9 @@
import 'package:adwaita/adwaita.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:macos_ui/macos_ui.dart';
import 'package:spotube/extensions/ShimmerColorTheme.dart'; import 'package:spotube/extensions/ShimmerColorTheme.dart';
import 'package:fluent_ui/fluent_ui.dart' as FluentUI;
final materialWhite = MaterialColor(Colors.white.value, { final materialWhite = MaterialColor(Colors.white.value, {
50: Colors.white, 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,
),
);

View File

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

View File

@ -8,6 +8,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "50.0.0" version: "50.0.0"
adwaita:
dependency: "direct main"
description:
name: adwaita
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.2"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -254,7 +261,7 @@ packages:
name: build_resolvers name: build_resolvers
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.10" version: "2.1.0"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -493,6 +500,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.1" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -561,6 +575,11 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -582,6 +601,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.42.0" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -627,6 +653,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
gsettings:
dependency: transitive
description:
name: gsettings
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@ -690,6 +723,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
introduction_screen: introduction_screen:
dependency: "direct main" dependency: "direct main"
description: description:
@ -718,6 +758,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.5.0" 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: lints:
dependency: transitive dependency: transitive
description: description:
@ -739,6 +793,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" 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: marquee:
dependency: "direct main" dependency: "direct main"
description: description:
@ -867,6 +928,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.2" 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: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -972,6 +1047,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
platform_ui:
dependency: "direct main"
description:
path: "../platform_ui"
relative: true
source: path
version: "0.1.0"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -993,6 +1075,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.6+3" 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: process:
dependency: transitive dependency: transitive
description: description:
@ -1021,6 +1110,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0+1" version: "3.1.0+1"
recase:
dependency: transitive
description:
name: recase
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@ -1035,6 +1131,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.27.3" 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: scroll_to_index:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -62,6 +62,12 @@ dependencies:
flutter_inappwebview: ^5.4.3+7 flutter_inappwebview: ^5.4.3+7
tuple: ^2.0.1 tuple: ^2.0.1
uuid: ^3.0.6 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: dev_dependencies:
flutter_test: flutter_test: