mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge pull request #323 from KRTirtho/experimental/platform_ui
Experimental/platform UI
This commit is contained in:
commit
a254e1e2f9
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -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": []
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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) {
|
||||||
|
@ -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]!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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]);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>[],
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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: [
|
||||||
|
@ -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),
|
||||||
|
@ -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(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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";
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
|
@ -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 ?? [],
|
||||||
|
@ -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: () {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"))
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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"),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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!;
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
10
lib/hooks/usePlatformProperty.dart
Normal file
10
lib/hooks/usePlatformProperty.dart
Normal 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);
|
||||||
|
}
|
@ -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]);
|
||||||
|
@ -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(),
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
@ -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"))
|
||||||
|
105
pubspec.lock
105
pubspec.lock
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user