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",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart"
|
||||
"program": "${workspaceFolder}/lib/main.dart"
|
||||
},
|
||||
],
|
||||
"compounds": []
|
||||
|
@ -1,12 +1,16 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide Colors;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/usePlatformProperty.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class ArtistCard extends StatelessWidget {
|
||||
class ArtistCard extends HookWidget {
|
||||
final Artist artist;
|
||||
const ArtistCard(this.artist, {Key? key}) : super(key: key);
|
||||
|
||||
@ -18,28 +22,70 @@ class ArtistCard extends StatelessWidget {
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
),
|
||||
);
|
||||
final boxShadow = usePlatformProperty<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(
|
||||
height: 240,
|
||||
width: 200,
|
||||
child: InkWell(
|
||||
splashFactory: splash,
|
||||
onTap: () {
|
||||
ServiceUtils.navigate(context, "/artist/${artist.id}");
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
customBorder: platform == TargetPlatform.windows
|
||||
? Border.all(
|
||||
color: FluentTheme.maybeOf(context)
|
||||
?.micaBackgroundColor
|
||||
.withOpacity(.7) ??
|
||||
Colors.transparent,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(
|
||||
platform == TargetPlatform.windows ? 5 : 8,
|
||||
),
|
||||
child: HoverBuilder(builder: (context, isHovering) {
|
||||
return Ink(
|
||||
width: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: PlatformTheme.of(context).secondaryBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
platform == TargetPlatform.windows ? 5 : 8,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 5,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
if (boxShadow != null) boxShadow,
|
||||
],
|
||||
border: [TargetPlatform.windows, TargetPlatform.macOS]
|
||||
.contains(platform)
|
||||
? Border.all(
|
||||
color: PlatformTheme.of(context).borderColor ??
|
||||
Colors.transparent,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
@ -79,7 +125,7 @@ class ArtistCard extends StatelessWidget {
|
||||
artist.name!,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
style: PlatformTextTheme.of(context).body?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Artist/ArtistAlbumList.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
@ -30,13 +31,13 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final parentScrollController = useScrollController();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final textTheme = PlatformTheme.of(context).textTheme;
|
||||
final chipTextVariant = useBreakpointValue(
|
||||
sm: textTheme.bodySmall,
|
||||
md: textTheme.bodyMedium,
|
||||
lg: textTheme.headline6,
|
||||
xl: textTheme.headline6,
|
||||
xxl: textTheme.headline6,
|
||||
sm: textTheme!.caption,
|
||||
md: textTheme.body,
|
||||
lg: textTheme.subheading,
|
||||
xl: textTheme.headline,
|
||||
xxl: textTheme.headline,
|
||||
);
|
||||
|
||||
final avatarWidth = useBreakpointValue(
|
||||
@ -52,9 +53,9 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
final Playback playback = ref.watch(playbackProvider);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
child: PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
leading: const PlatformBackButton(),
|
||||
),
|
||||
body: HookBuilder(
|
||||
builder: (context) {
|
||||
@ -67,7 +68,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
return const ShimmerArtistProfile();
|
||||
} else if (artistsQuery.hasError) {
|
||||
return Center(
|
||||
child: Text(artistsQuery.error.toString()),
|
||||
child: PlatformText(artistsQuery.error.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,21 +106,22 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(50)),
|
||||
child: Text(data.type!.toUpperCase(),
|
||||
child: PlatformText(data.type!.toUpperCase(),
|
||||
style: chipTextVariant?.copyWith(
|
||||
color: Colors.white)),
|
||||
),
|
||||
Text(
|
||||
PlatformText(
|
||||
data.name!,
|
||||
style: breakpoint.isSm
|
||||
? textTheme.headline4
|
||||
: textTheme.headline2,
|
||||
? textTheme.subheading
|
||||
: textTheme.headline,
|
||||
),
|
||||
Text(
|
||||
PlatformText(
|
||||
"${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers",
|
||||
style: breakpoint.isSm
|
||||
? textTheme.bodyText1
|
||||
: textTheme.headline5,
|
||||
? textTheme.body
|
||||
: textTheme.body
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
@ -138,11 +140,12 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
child:
|
||||
PlatformCircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton(
|
||||
return PlatformFilledButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowingQuery.data!
|
||||
@ -168,7 +171,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
]);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
child: PlatformText(
|
||||
isFollowingQuery.data!
|
||||
? "Following"
|
||||
: "Follow",
|
||||
@ -176,7 +179,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
onPressed: () {
|
||||
Clipboard.setData(
|
||||
@ -188,7 +191,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
const SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
content: PlatformText(
|
||||
"Artist URL copied to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@ -213,10 +216,10 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
return const PlatformCircularProgressIndicator();
|
||||
} else if (topTracksQuery.hasError) {
|
||||
return Center(
|
||||
child: Text(topTracksQuery.error.toString()),
|
||||
child: PlatformText(topTracksQuery.error.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
@ -250,9 +253,10 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
return Column(children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
"Top Tracks",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style:
|
||||
PlatformTheme.of(context).textTheme?.headline,
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5),
|
||||
@ -260,11 +264,13 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded),
|
||||
color: Colors.white,
|
||||
child: PlatformIconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () =>
|
||||
playPlaylist(topTracks.toList()),
|
||||
),
|
||||
@ -290,16 +296,16 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Text(
|
||||
PlatformText(
|
||||
"Albums",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: PlatformTheme.of(context).textTheme?.headline,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ArtistAlbumList(artistId),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
PlatformText(
|
||||
"Fans also likes",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
style: PlatformTheme.of(context).textTheme?.headline,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
HookBuilder(
|
||||
@ -310,10 +316,10 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (relatedArtists.isLoading || !relatedArtists.hasData) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
return const PlatformCircularProgressIndicator();
|
||||
} else if (relatedArtists.hasError) {
|
||||
return Center(
|
||||
child: Text(relatedArtists.error.toString()),
|
||||
child: PlatformText(relatedArtists.error.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
@ -46,15 +47,13 @@ class CategoryCard extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
category.name ?? "Unknown",
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
PlatformText.headline(category.name ?? "Unknown"),
|
||||
],
|
||||
),
|
||||
),
|
||||
playlistQuery.hasError
|
||||
? Text("Something Went Wrong\n${playlistQuery.errors.first}")
|
||||
? PlatformText(
|
||||
"Something Went Wrong\n${playlistQuery.errors.first}")
|
||||
: SizedBox(
|
||||
height: 245,
|
||||
child: ScrollConfiguration(
|
||||
|
@ -2,13 +2,16 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Category/CategoryCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class Genres extends HookConsumerWidget {
|
||||
const Genres({Key? key}) : super(key: key);
|
||||
@ -40,7 +43,8 @@ class Genres extends HookConsumerWidget {
|
||||
.toList()
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
return PlatformScaffold(
|
||||
appBar: kIsDesktop ? PageWindowTitleBar() : null,
|
||||
body: ListView.builder(
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
|
||||
import 'package:spotube/components/Player/Player.dart';
|
||||
@ -12,7 +13,7 @@ import 'package:spotube/hooks/useUpdateChecker.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
const _path = {
|
||||
const rootPaths = {
|
||||
0: "/",
|
||||
1: "/search",
|
||||
2: "/library",
|
||||
@ -35,8 +36,8 @@ class Shell extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
downloader.onFileExists = (track) async {
|
||||
if (!isMounted()) return false;
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
return await showPlatformAlertDialog<bool>(
|
||||
context,
|
||||
builder: (context) => ReplaceDownloadedFileDialog(
|
||||
track: track,
|
||||
),
|
||||
@ -63,38 +64,14 @@ class Shell extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [backgroundColor]);
|
||||
|
||||
final allowedPath = _path.values.contains(GoRouter.of(context).location);
|
||||
final preferredSize =
|
||||
allowedPath ? PageWindowTitleBar.staticPreferredSize : Size.zero;
|
||||
return Scaffold(
|
||||
appBar: kIsDesktop
|
||||
? PreferredSize(
|
||||
preferredSize: preferredSize,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
height: allowedPath
|
||||
? PageWindowTitleBar.staticPreferredSize.height
|
||||
: 0,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: allowedPath ? 1 : 0,
|
||||
child: PageWindowTitleBar(preferredSize: preferredSize),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Row(
|
||||
children: [
|
||||
Sidebar(
|
||||
selectedIndex: index.value,
|
||||
onSelectedIndexChanged: (selectedIndex) {
|
||||
index.value = selectedIndex;
|
||||
GoRouter.of(context).go(_path[selectedIndex]!);
|
||||
},
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
return PlatformScaffold(
|
||||
body: Sidebar(
|
||||
selectedIndex: index.value,
|
||||
onSelectedIndexChanged: (i) {
|
||||
index.value = i;
|
||||
GoRouter.of(context).go(rootPaths[index.value]!);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
extendBody: true,
|
||||
bottomNavigationBar: Column(
|
||||
@ -105,7 +82,7 @@ class Shell extends HookConsumerWidget {
|
||||
selectedIndex: index.value,
|
||||
onSelectedIndexChanged: (selectedIndex) {
|
||||
index.value = selectedIndex;
|
||||
GoRouter.of(context).go(_path[selectedIndex]!);
|
||||
GoRouter.of(context).go(rootPaths[selectedIndex]!);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/models/sideBarTiles.dart';
|
||||
@ -15,20 +16,23 @@ import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as FluentUI;
|
||||
|
||||
final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null);
|
||||
|
||||
class Sidebar extends HookConsumerWidget {
|
||||
final int selectedIndex;
|
||||
final void Function(int) onSelectedIndexChanged;
|
||||
final Widget child;
|
||||
|
||||
const Sidebar({
|
||||
required this.selectedIndex,
|
||||
required this.onSelectedIndexChanged,
|
||||
required this.child,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget _buildSmallLogo() {
|
||||
static Widget brandLogo() {
|
||||
return Image.asset(
|
||||
"assets/spotube-logo.png",
|
||||
height: 50,
|
||||
@ -45,7 +49,6 @@ class Sidebar extends HookConsumerWidget {
|
||||
final breakpoints = useBreakpoints();
|
||||
final extended = useState(false);
|
||||
|
||||
final auth = ref.watch(authProvider);
|
||||
final downloadCount = ref.watch(
|
||||
downloaderProvider.select((s) => s.currentlyRunning),
|
||||
);
|
||||
@ -72,7 +75,7 @@ class Sidebar extends HookConsumerWidget {
|
||||
|
||||
if (layoutMode == LayoutMode.compact ||
|
||||
(breakpoints.isSm && layoutMode == LayoutMode.adaptive)) {
|
||||
return Container();
|
||||
return PlatformScaffold(body: child);
|
||||
}
|
||||
|
||||
void toggleExtended() =>
|
||||
@ -81,19 +84,40 @@ class Sidebar extends HookConsumerWidget {
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Material(
|
||||
color: Theme.of(context).navigationRailTheme.backgroundColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
child: PlatformSidebar(
|
||||
currentIndex: selectedIndex,
|
||||
onIndexChanged: onSelectedIndexChanged,
|
||||
body: Map.fromEntries(
|
||||
sidebarTileList.map(
|
||||
(e) {
|
||||
final icon = Icon(e.icon);
|
||||
return MapEntry(
|
||||
PlatformSidebarItem(
|
||||
icon: icon,
|
||||
title: Text(
|
||||
e.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
expanded: extended.value,
|
||||
header: Column(
|
||||
children: [
|
||||
if (kIsDesktop)
|
||||
if (kIsMacOS)
|
||||
SizedBox(
|
||||
height: appWindow.titleBarHeight,
|
||||
width: extended.value ? 256 : 80,
|
||||
child: MoveWindow(
|
||||
child: !extended.value
|
||||
? Center(
|
||||
child: IconButton(
|
||||
child: PlatformIconButton(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
onPressed: toggleExtended,
|
||||
),
|
||||
@ -103,161 +127,144 @@ class Sidebar extends HookConsumerWidget {
|
||||
),
|
||||
if (!kIsDesktop && !extended.value)
|
||||
Center(
|
||||
child: IconButton(
|
||||
child: PlatformIconButton(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
onPressed: toggleExtended,
|
||||
),
|
||||
),
|
||||
(extended.value)
|
||||
? Row(
|
||||
children: [
|
||||
_buildSmallLogo(),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
"Spotube",
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
onPressed: toggleExtended,
|
||||
),
|
||||
],
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
brandLogo(),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
PlatformText.headline("Spotube"),
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.menu_rounded),
|
||||
onPressed: toggleExtended,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _buildSmallLogo(),
|
||||
Expanded(
|
||||
child: NavigationRail(
|
||||
destinations: sidebarTileList.map(
|
||||
(e) {
|
||||
final icon = Icon(e.icon);
|
||||
return NavigationRailDestination(
|
||||
icon: e.title == "Library" && downloadCount > 0
|
||||
? Badge(
|
||||
badgeColor: Colors.red[100]!,
|
||||
badgeContent: Text(
|
||||
downloadCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
animationType: BadgeAnimationType.fade,
|
||||
child: icon,
|
||||
)
|
||||
: icon,
|
||||
label: Text(
|
||||
e.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
selectedIndex: selectedIndex,
|
||||
onDestinationSelected: onSelectedIndexChanged,
|
||||
extended: extended.value,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: extended.value ? 256 : 80,
|
||||
child: HookBuilder(
|
||||
builder: (context) {
|
||||
final me = useQuery(
|
||||
job: currentUserQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
);
|
||||
final data = me.data;
|
||||
|
||||
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
||||
data?.images,
|
||||
index: (data?.images?.length ?? 1) - 1,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (auth.isLoggedIn && !me.hasData) {
|
||||
me.setExternalData(ref.read(spotifyProvider));
|
||||
me.refetch();
|
||||
}
|
||||
return;
|
||||
}, [auth.isLoggedIn, me.hasData]);
|
||||
|
||||
if (extended.value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16).copyWith(left: 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (auth.isLoggedIn && data == null)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (data != null)
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage:
|
||||
UniversalImage.imageProvider(
|
||||
avatarImg),
|
||||
onBackgroundImageError:
|
||||
(exception, stackTrace) =>
|
||||
Image.asset(
|
||||
"assets/user-placeholder.png",
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
data.displayName ?? "Guest",
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => goToSettings(context)),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
onTap: () => goToSettings(context),
|
||||
child: CircleAvatar(
|
||||
backgroundImage:
|
||||
UniversalImage.imageProvider(avatarImg),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
Image.asset(
|
||||
"assets/user-placeholder.png",
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
: brandLogo(),
|
||||
],
|
||||
),
|
||||
windowsFooterItems: [
|
||||
FluentUI.PaneItemAction(
|
||||
icon: const FluentUI.Icon(FluentUI.FluentIcons.settings),
|
||||
onTap: () => goToSettings(context),
|
||||
),
|
||||
],
|
||||
footer: SidebarFooter(extended: extended.value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SidebarFooter extends HookConsumerWidget {
|
||||
final bool extended;
|
||||
const SidebarFooter({
|
||||
Key? key,
|
||||
required this.extended,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
|
||||
return SizedBox(
|
||||
width: extended ? 256 : 80,
|
||||
child: HookBuilder(
|
||||
builder: (context) {
|
||||
final me = useQuery(
|
||||
job: currentUserQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
);
|
||||
final data = me.data;
|
||||
|
||||
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
||||
data?.images,
|
||||
index: (data?.images?.length ?? 1) - 1,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (auth.isLoggedIn && !me.hasData) {
|
||||
me.setExternalData(ref.read(spotifyProvider));
|
||||
me.refetch();
|
||||
}
|
||||
return;
|
||||
}, [auth.isLoggedIn, me.hasData]);
|
||||
|
||||
if (extended) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16).copyWith(left: 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (auth.isLoggedIn && data == null)
|
||||
const Center(
|
||||
child: PlatformCircularProgressIndicator(),
|
||||
)
|
||||
else if (data != null)
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage:
|
||||
UniversalImage.imageProvider(avatarImg),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
Image.asset(
|
||||
"assets/user-placeholder.png",
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
data.displayName ?? "Guest",
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style: PlatformTheme.of(context)
|
||||
.textTheme
|
||||
?.body
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => Sidebar.goToSettings(context)),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
onTap: () => Sidebar.goToSettings(context),
|
||||
child: CircleAvatar(
|
||||
backgroundImage: UniversalImage.imageProvider(avatarImg),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
Image.asset(
|
||||
"assets/user-placeholder.png",
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:badges/badges.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/models/sideBarTiles.dart';
|
||||
@ -37,37 +37,23 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
||||
if (layoutMode == LayoutMode.extended ||
|
||||
(breakpoint.isMoreThan(Breakpoints.sm) &&
|
||||
layoutMode == LayoutMode.adaptive)) return const SizedBox();
|
||||
return NavigationBar(
|
||||
destinations: [
|
||||
return PlatformBottomNavigationBar(
|
||||
items: [
|
||||
...sidebarTileList.map(
|
||||
(e) {
|
||||
final icon = Icon(e.icon);
|
||||
return NavigationDestination(
|
||||
icon: e.title == "Library" && downloadCount > 0
|
||||
? Badge(
|
||||
badgeColor: Colors.red[100]!,
|
||||
badgeContent: Text(
|
||||
downloadCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
animationType: BadgeAnimationType.fade,
|
||||
child: icon,
|
||||
)
|
||||
: icon,
|
||||
return PlatformBottomNavigationBarItem(
|
||||
icon: e.icon,
|
||||
label: e.title,
|
||||
);
|
||||
},
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.settings_rounded),
|
||||
const PlatformBottomNavigationBarItem(
|
||||
icon: Icons.settings_rounded,
|
||||
label: "Settings",
|
||||
)
|
||||
],
|
||||
selectedIndex: insideSelectedIndex.value,
|
||||
onDestinationSelected: (i) {
|
||||
onSelectedIndexChanged: (i) {
|
||||
if (i == 4) {
|
||||
insideSelectedIndex.value = 4;
|
||||
Sidebar.goToSettings(context);
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
@ -12,6 +15,10 @@ class UserAlbums extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
final albumsQuery = useQuery(
|
||||
job: currentUserAlbumsQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
@ -22,16 +29,22 @@ class UserAlbums extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
children: albumsQuery.data!
|
||||
.map((album) =>
|
||||
AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album)))
|
||||
.toList(),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
color: PlatformTheme.of(context).scaffoldBackgroundColor,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
children: albumsQuery.data!
|
||||
.map((album) =>
|
||||
AlbumCard(TypeConversionUtils.simpleAlbum_X_Album(album)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,9 +2,12 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
@ -13,6 +16,10 @@ class UserArtists extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
final artistQuery = useInfiniteQuery(
|
||||
job: currentUserFollowingArtistsQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
@ -28,26 +35,31 @@ class UserArtists extends HookConsumerWidget {
|
||||
? false
|
||||
: (artistQuery.pages.last?.items?.length ?? 0) == 15;
|
||||
|
||||
return GridView.builder(
|
||||
itemCount: artists.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: 250,
|
||||
crossAxisSpacing: 20,
|
||||
mainAxisSpacing: 20,
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
color: PlatformTheme.of(context).scaffoldBackgroundColor,
|
||||
child: GridView.builder(
|
||||
itemCount: artists.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: 250,
|
||||
crossAxisSpacing: 20,
|
||||
mainAxisSpacing: 20,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == artists.length - 1 && hasNextPage) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
artistQuery.fetchNextPage();
|
||||
},
|
||||
child: ArtistCard(artists[index]),
|
||||
);
|
||||
}
|
||||
return ArtistCard(artists[index]);
|
||||
},
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == artists.length - 1 && hasNextPage) {
|
||||
return Waypoint(
|
||||
onEnter: () {
|
||||
artistQuery.fetchNextPage();
|
||||
},
|
||||
child: ArtistCard(artists[index]),
|
||||
);
|
||||
}
|
||||
return ArtistCard(artists[index]);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
@ -25,19 +26,20 @@ class UserDownloads extends HookConsumerWidget {
|
||||
child: AutoSizeText(
|
||||
"Currently downloading (${downloader.currentlyRunning})",
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
style: PlatformTextTheme.of(context).headline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[50],
|
||||
foregroundColor: Colors.red[400],
|
||||
PlatformFilledButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red[50]),
|
||||
foregroundColor: MaterialStatePropertyAll(Colors.red[400]),
|
||||
),
|
||||
onPressed: downloader.currentlyRunning > 0
|
||||
? downloader.cancelAll
|
||||
: null,
|
||||
child: const Text("Cancel All"),
|
||||
isSecondary: true,
|
||||
child: const PlatformText("Cancel All"),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -47,8 +49,8 @@ class UserDownloads extends HookConsumerWidget {
|
||||
itemCount: downloader.inQueue.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = downloader.inQueue.elementAt(index);
|
||||
return ListTile(
|
||||
title: Text(track.name!),
|
||||
return PlatformListTile(
|
||||
title: Text(track.name ?? ''),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
child: ClipRRect(
|
||||
@ -66,9 +68,8 @@ class UserDownloads extends HookConsumerWidget {
|
||||
trailing: const SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
child: PlatformCircularProgressIndicator(),
|
||||
),
|
||||
horizontalTitleGap: 5,
|
||||
subtitle: Text(
|
||||
TypeConversionUtils.artists_X_String(
|
||||
track.artists ?? <Artist>[],
|
||||
|
@ -1,42 +1,49 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Library/UserAlbums.dart';
|
||||
import 'package:spotube/components/Library/UserArtists.dart';
|
||||
import 'package:spotube/components/Library/UserDownloads.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Library/UserPlaylists.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/components/Shared/ColoredTabBar.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
|
||||
class UserLibrary extends ConsumerWidget {
|
||||
class UserLibrary extends HookConsumerWidget {
|
||||
const UserLibrary({Key? key}) : super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return DefaultTabController(
|
||||
length: 5,
|
||||
child: SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: ColoredTabBar(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: "Playlist"),
|
||||
Tab(text: "Downloads"),
|
||||
Tab(text: "Local"),
|
||||
Tab(text: "Artists"),
|
||||
Tab(text: "Album"),
|
||||
],
|
||||
),
|
||||
final index = useState(0);
|
||||
|
||||
final body = [
|
||||
const UserPlaylists(),
|
||||
const UserLocalTracks(),
|
||||
const UserDownloads(),
|
||||
const UserArtists(),
|
||||
const UserAlbums(),
|
||||
][index.value];
|
||||
|
||||
return SafeArea(
|
||||
child: PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
titleWidth: 347,
|
||||
centerTitle: true,
|
||||
center: PlatformTabBar(
|
||||
androidIsScrollable: true,
|
||||
selectedIndex: index.value,
|
||||
onSelectedIndexChanged: (value) => index.value = value,
|
||||
isNavigational:
|
||||
PlatformProperty.byPlatformGroup(mobile: false, desktop: true),
|
||||
tabs: [
|
||||
PlatformTab(label: 'Playlists', icon: const SizedBox.shrink()),
|
||||
PlatformTab(label: 'Tracks', icon: const SizedBox.shrink()),
|
||||
PlatformTab(label: 'Downloads', icon: const SizedBox.shrink()),
|
||||
PlatformTab(label: 'Artists', icon: const SizedBox.shrink()),
|
||||
PlatformTab(label: 'Albums', icon: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
body: const TabBarView(children: [
|
||||
AnonymousFallback(child: UserPlaylists()),
|
||||
UserDownloads(),
|
||||
UserLocalTracks(),
|
||||
AnonymousFallback(child: UserArtists()),
|
||||
AnonymousFallback(child: UserAlbums()),
|
||||
]),
|
||||
),
|
||||
body: body,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/SortTracksDropdown.dart';
|
||||
@ -169,13 +170,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton.icon(
|
||||
label: const Text("Play"),
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
PlatformFilledButton(
|
||||
onPressed: trackSnapshot.value != null
|
||||
? () {
|
||||
if (trackSnapshot.value?.isNotEmpty == true) {
|
||||
@ -187,6 +182,16 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
const Text("Play"),
|
||||
Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SortTracksDropdown(
|
||||
@ -196,7 +201,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
PlatformFilledButton(
|
||||
child: const Icon(Icons.refresh_rounded),
|
||||
onPressed: () {
|
||||
ref.refresh(localTracksProvider);
|
||||
|
@ -1,10 +1,13 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
@ -13,6 +16,11 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
final playlistsQuery = useQuery(
|
||||
job: currentUserPlaylistsQueryJob,
|
||||
externalData: ref.watch(spotifyProvider),
|
||||
@ -34,19 +42,24 @@ class UserPlaylists extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
const PlaylistCreateDialog(),
|
||||
PlaylistCard(likedTracksPlaylist),
|
||||
...playlistsQuery.data!
|
||||
.map((playlist) => PlaylistCard(playlist))
|
||||
.toList(),
|
||||
],
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
const PlaylistCreateDialog(),
|
||||
PlaylistCard(likedTracksPlaylist),
|
||||
...playlistsQuery.data!
|
||||
.map((playlist) => PlaylistCard(playlist))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -11,10 +11,11 @@ class ShimmerArtistProfile extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shimmerColor =
|
||||
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
|
||||
Colors.white;
|
||||
final shimmerBackgroundColor = Theme.of(context)
|
||||
.extension<ShimmerColorTheme>()!
|
||||
.shimmerBackgroundColor!;
|
||||
.extension<ShimmerColorTheme>()
|
||||
?.shimmerBackgroundColor;
|
||||
|
||||
final avatarWidth = useBreakpointValue(
|
||||
sm: MediaQuery.of(context).size.width * 0.80,
|
||||
|
@ -9,10 +9,12 @@ class ShimmerCategories extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shimmerColor =
|
||||
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
|
||||
Colors.white;
|
||||
final shimmerBackgroundColor = Theme.of(context)
|
||||
.extension<ShimmerColorTheme>()!
|
||||
.shimmerBackgroundColor!;
|
||||
.extension<ShimmerColorTheme>()
|
||||
?.shimmerBackgroundColor ??
|
||||
Colors.grey;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
@ -12,10 +12,12 @@ class ShimmerLyrics extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shimmerColor =
|
||||
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
|
||||
Colors.white;
|
||||
final shimmerBackgroundColor = Theme.of(context)
|
||||
.extension<ShimmerColorTheme>()!
|
||||
.shimmerBackgroundColor!;
|
||||
.extension<ShimmerColorTheme>()
|
||||
?.shimmerBackgroundColor ??
|
||||
Colors.grey;
|
||||
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
|
@ -9,10 +9,12 @@ class ShimmerPlaybuttonCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shimmerColor =
|
||||
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
|
||||
Colors.white;
|
||||
final shimmerBackgroundColor = Theme.of(context)
|
||||
.extension<ShimmerColorTheme>()!
|
||||
.shimmerBackgroundColor!;
|
||||
.extension<ShimmerColorTheme>()
|
||||
?.shimmerBackgroundColor ??
|
||||
Colors.grey;
|
||||
|
||||
final card = Stack(
|
||||
children: [
|
||||
|
@ -13,10 +13,12 @@ class ShimmerTrackTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shimmerColor =
|
||||
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||
Theme.of(context).extension<ShimmerColorTheme>()?.shimmerColor ??
|
||||
Colors.white;
|
||||
final shimmerBackgroundColor = Theme.of(context)
|
||||
.extension<ShimmerColorTheme>()!
|
||||
.shimmerBackgroundColor!;
|
||||
.extension<ShimmerColorTheme>()
|
||||
?.shimmerBackgroundColor ??
|
||||
Colors.grey;
|
||||
|
||||
final single = Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Login/TokenLoginForms.dart';
|
||||
import 'package:spotube/components/Shared/Hyperlink.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
@ -13,46 +14,56 @@ class LoginTutorial extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authProvider);
|
||||
final key = GlobalKey<State<IntroductionScreen>>();
|
||||
|
||||
return Scaffold(
|
||||
return PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
leading: TextButton(
|
||||
child: const Text("Exit"),
|
||||
leading: PlatformTextButton(
|
||||
child: const PlatformText("Exit"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: IntroductionScreen(
|
||||
next: const Text("Next"),
|
||||
back: const Text("Previous"),
|
||||
key: key,
|
||||
overrideBack: PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Center(child: PlatformText("Previous")),
|
||||
onPressed: () {
|
||||
(key.currentState as IntroductionScreenState).previous();
|
||||
},
|
||||
),
|
||||
overrideNext: PlatformFilledButton(
|
||||
child: const Center(child: PlatformText("Next")),
|
||||
onPressed: () {
|
||||
(key.currentState as IntroductionScreenState).next();
|
||||
},
|
||||
),
|
||||
showBackButton: true,
|
||||
overrideDone: TextButton(
|
||||
overrideDone: PlatformFilledButton(
|
||||
onPressed: auth.isLoggedIn
|
||||
? () {
|
||||
ServiceUtils.navigate(context, "/");
|
||||
}
|
||||
: null,
|
||||
child: const Text("Done"),
|
||||
child: const Center(child: PlatformText("Done")),
|
||||
),
|
||||
pages: [
|
||||
PageViewModel(
|
||||
title: "Step 1",
|
||||
image: Image.asset("assets/tutorial/step-1.png"),
|
||||
bodyWidget: Wrap(
|
||||
children: [
|
||||
Text(
|
||||
children: const [
|
||||
PlatformText(
|
||||
"First, Go to ",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
Hyperlink(
|
||||
"accounts.spotify.com ",
|
||||
"https://accounts.spotify.com",
|
||||
style: Theme.of(context).textTheme.bodyText1!,
|
||||
),
|
||||
Text(
|
||||
PlatformText(
|
||||
"and Login/Sign up if you're not logged in",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -60,10 +71,9 @@ class LoginTutorial extends ConsumerWidget {
|
||||
PageViewModel(
|
||||
title: "Step 2",
|
||||
image: Image.asset("assets/tutorial/step-2.png"),
|
||||
bodyWidget: Text(
|
||||
bodyWidget: const PlatformText(
|
||||
"1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection",
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
PageViewModel(
|
||||
@ -71,10 +81,9 @@ class LoginTutorial extends ConsumerWidget {
|
||||
image: Image.asset(
|
||||
"assets/tutorial/step-3.png",
|
||||
),
|
||||
bodyWidget: Text(
|
||||
bodyWidget: const PlatformText(
|
||||
"Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
@ -91,13 +100,12 @@ class LoginTutorial extends ConsumerWidget {
|
||||
PageViewModel(
|
||||
title: "Step 5",
|
||||
bodyWidget: Column(
|
||||
children: [
|
||||
Text(
|
||||
children: const [
|
||||
PlatformText(
|
||||
"Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const TokenLoginForm(),
|
||||
SizedBox(height: 10),
|
||||
TokenLoginForm(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Login/TokenLoginForms.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
@ -14,12 +15,17 @@ class TokenLogin extends HookConsumerWidget {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||
child: PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(leading: const PlatformBackButton()),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||
margin: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: PlatformTheme.of(context).secondaryBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
@ -27,11 +33,11 @@ class TokenLogin extends HookConsumerWidget {
|
||||
width: MediaQuery.of(context).size.width *
|
||||
(breakpoint <= Breakpoints.md ? .5 : .3),
|
||||
),
|
||||
Text("Add your spotify credentials to get started",
|
||||
PlatformText("Add your spotify credentials to get started",
|
||||
style: breakpoint <= Breakpoints.md
|
||||
? textTheme.headline5
|
||||
: textTheme.headline4),
|
||||
Text(
|
||||
PlatformText(
|
||||
"Don't worry, any of your credentials won't be collected or shared with anyone",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
@ -44,9 +50,9 @@ class TokenLogin extends HookConsumerWidget {
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
const Text("Don't know how to do this?"),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
const PlatformText("Don't know how to do this?"),
|
||||
PlatformTextButton(
|
||||
child: const PlatformText(
|
||||
"Follow along the Step by Step guide",
|
||||
),
|
||||
onPressed: () => GoRouter.of(context).push(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
@ -24,31 +25,27 @@ class TokenLoginForm extends HookConsumerWidget {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
PlatformTextField(
|
||||
controller: directCodeController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Spotify \"sp_dc\" Cookie",
|
||||
label: Text("sp_dc Cookie"),
|
||||
),
|
||||
placeholder: "Spotify \"sp_dc\" Cookie",
|
||||
label: "sp_dc Cookie",
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
PlatformTextField(
|
||||
controller: keyCodeController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Spotify \"sp_key\" Cookie",
|
||||
label: Text("sp_key Cookie"),
|
||||
),
|
||||
placeholder: "Spotify \"sp_key\" Cookie",
|
||||
label: "sp_key Cookie",
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
PlatformFilledButton(
|
||||
onPressed: () async {
|
||||
if (keyCodeController.text.isEmpty ||
|
||||
directCodeController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Please fill in all fields"),
|
||||
content: PlatformText("Please fill in all fields"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
@ -67,7 +64,7 @@ class TokenLoginForm extends HookConsumerWidget {
|
||||
onDone?.call();
|
||||
}
|
||||
},
|
||||
child: const Text("Submit"),
|
||||
child: const PlatformText("Submit"),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
@ -23,7 +24,7 @@ class WebViewLogin extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
return PlatformScaffold(
|
||||
body: SafeArea(
|
||||
child: InAppWebView(
|
||||
initialOptions: InAppWebViewGroupOptions(
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
|
||||
|
||||
class LyricDelayAdjustDialog extends HookConsumerWidget {
|
||||
@ -15,16 +17,20 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
|
||||
double getValue() =>
|
||||
double.tryParse(controller.text.replaceAll("ms", "")) ?? 0;
|
||||
|
||||
return AlertDialog(
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: const Center(child: Text("Adjust Lyrics Delay")),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
child: const Text("Cancel"),
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
],
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const Text("Done"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
@ -35,39 +41,39 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
|
||||
},
|
||||
)
|
||||
],
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_rounded),
|
||||
onPressed: () {
|
||||
controller.text = "${getValue() - 25}ms";
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
hintText: "Delay in milliseconds",
|
||||
),
|
||||
onSubmitted: (_) {
|
||||
Navigator.of(context).pop(
|
||||
Duration(
|
||||
milliseconds: getValue().toInt(),
|
||||
),
|
||||
);
|
||||
content: SizedBox(
|
||||
height: 100,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.remove_rounded),
|
||||
onPressed: () {
|
||||
controller.text = "${getValue() - 25}ms";
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
onPressed: () {
|
||||
controller.text = "${getValue() + 25}ms";
|
||||
},
|
||||
),
|
||||
],
|
||||
Flexible(
|
||||
child: PlatformTextField(
|
||||
keyboardType: TextInputType.number,
|
||||
controller: controller,
|
||||
placeholder: "Delay in milliseconds",
|
||||
onSubmitted: (_) {
|
||||
Navigator.of(context).pop(
|
||||
Duration(
|
||||
milliseconds: getValue().toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
onPressed: () {
|
||||
controller.text = "${getValue() + 25}ms";
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -3,12 +3,15 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Lyrics/GeniusLyrics.dart';
|
||||
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class Lyrics extends HookConsumerWidget {
|
||||
@ -26,6 +29,7 @@ class Lyrics extends HookConsumerWidget {
|
||||
[playback.track?.album?.images],
|
||||
);
|
||||
final palette = usePaletteColor(albumArt, ref);
|
||||
final index = useState(0);
|
||||
|
||||
useCustomStatusBarColor(
|
||||
palette.color,
|
||||
@ -33,38 +37,52 @@ class Lyrics extends HookConsumerWidget {
|
||||
noSetBGColor: true,
|
||||
);
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: "Synced Lyrics"),
|
||||
Tab(text: "Lyrics (genius.com)"),
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: Container(
|
||||
color: palette.color.withOpacity(.7),
|
||||
child: SafeArea(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
SyncedLyrics(palette: palette),
|
||||
GeniusLyrics(palette: palette),
|
||||
],
|
||||
),
|
||||
final body = [
|
||||
SyncedLyrics(palette: palette),
|
||||
GeniusLyrics(palette: palette),
|
||||
][index.value];
|
||||
|
||||
return PlatformScaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: !kIsMacOS
|
||||
? PageWindowTitleBar(
|
||||
toolbarOpacity: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
center: PlatformTabBar(
|
||||
isNavigational:
|
||||
PlatformProperty.only(linux: true, other: false),
|
||||
selectedIndex: index.value,
|
||||
onSelectedIndexChanged: (value) => index.value = value,
|
||||
backgroundColor:
|
||||
PlatformTheme.of(context).scaffoldBackgroundColor,
|
||||
tabs: [
|
||||
PlatformTab(
|
||||
label: "Synced",
|
||||
icon: const SizedBox.shrink(),
|
||||
color: PlatformTextTheme.of(context).caption?.color,
|
||||
),
|
||||
PlatformTab(
|
||||
label: "Genius",
|
||||
icon: const SizedBox.shrink(),
|
||||
color: PlatformTextTheme.of(context).caption?.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: UniversalImage.imageProvider(albumArt),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: Container(
|
||||
color: palette.color.withOpacity(.7),
|
||||
child: SafeArea(child: body),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
|
||||
import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
|
||||
@ -73,6 +74,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
height: breakpoint >= Breakpoints.md ? 50 : 30,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
@ -85,18 +87,24 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
tooltip: "Lyrics Delay",
|
||||
icon: const Icon(Icons.av_timer_rounded),
|
||||
onPressed: () async {
|
||||
final delay = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => const LyricDelayAdjustDialog(),
|
||||
);
|
||||
if (delay != null) {
|
||||
ref.read(lyricDelayState.notifier).state = delay;
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PlatformFilledButton(
|
||||
child: const Icon(
|
||||
Icons.av_timer_rounded,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () async {
|
||||
final delay = await showPlatformAlertDialog(
|
||||
context,
|
||||
builder: (context) =>
|
||||
const LyricDelayAdjustDialog(),
|
||||
);
|
||||
if (delay != null) {
|
||||
ref.read(lyricDelayState.notifier).state = delay;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,11 +1,16 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:macos_ui/macos_ui.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as FluentUI;
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Player/PlayerActions.dart';
|
||||
import 'package:spotube/components/Player/PlayerOverlay.dart';
|
||||
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
||||
import 'package:spotube/components/Player/PlayerControls.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/hooks/usePlatformProperty.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -46,10 +51,51 @@ class Player extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
final backgroundColor = usePlatformProperty<Color?>(
|
||||
(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(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
@ -90,7 +136,7 @@ class Player extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Slider.adaptive(
|
||||
child: PlatformSlider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: volume.value,
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Player/PlayerQueue.dart';
|
||||
@ -48,7 +49,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
return Row(
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
children: [
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.queue_music_rounded),
|
||||
tooltip: 'Queue',
|
||||
onPressed: playback.playlist != null
|
||||
@ -73,7 +74,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
}
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
icon: const Icon(Icons.alt_route_rounded),
|
||||
tooltip: "Alternative Track Sources",
|
||||
onPressed: playback.track != null
|
||||
@ -108,7 +109,7 @@ class PlayerActions extends HookConsumerWidget {
|
||||
),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: 'Download track',
|
||||
icon: Icon(
|
||||
isDownloaded
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/hooks/playback.dart';
|
||||
import 'package:spotube/models/Intents.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
@ -94,10 +95,9 @@ class PlayerControls extends HookConsumerWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
PlatformTooltip(
|
||||
message: "Slide to seek forward or backward",
|
||||
child: Slider.adaptive(
|
||||
focusNode: FocusNode(),
|
||||
child: PlatformSlider(
|
||||
// cannot divide by zero
|
||||
// there's an edge case for value being bigger
|
||||
// than total duration. Keeping it resolved
|
||||
@ -122,10 +122,10 @@ class PlayerControls extends HookConsumerWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
"$currentMinutes:$currentSeconds",
|
||||
),
|
||||
Text("$totalMinutes:$totalSeconds"),
|
||||
PlatformText("$totalMinutes:$totalSeconds"),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -138,7 +138,7 @@ class PlayerControls extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: playback.isLoop
|
||||
? "Repeat playlist"
|
||||
: playback.isShuffled
|
||||
@ -156,14 +156,16 @@ class PlayerControls extends HookConsumerWidget {
|
||||
? null
|
||||
: playback.cyclePlaybackMode,
|
||||
),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: "Previous track",
|
||||
icon: const Icon(Icons.skip_previous_rounded),
|
||||
color: iconColor,
|
||||
icon: Icon(
|
||||
Icons.skip_previous_rounded,
|
||||
color: iconColor,
|
||||
),
|
||||
onPressed: () {
|
||||
onPrevious();
|
||||
}),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: playback.isPlaying
|
||||
? "Pause playback"
|
||||
: "Resume playback",
|
||||
@ -171,29 +173,33 @@ class PlayerControls extends HookConsumerWidget {
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(),
|
||||
child: PlatformCircularProgressIndicator(),
|
||||
)
|
||||
: Icon(
|
||||
playback.isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
color: iconColor,
|
||||
),
|
||||
color: iconColor,
|
||||
onPressed: Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: "Next track",
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
icon: Icon(
|
||||
Icons.skip_next_rounded,
|
||||
color: iconColor,
|
||||
),
|
||||
onPressed: () => onNext(),
|
||||
color: iconColor,
|
||||
),
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: "Stop playback",
|
||||
icon: const Icon(Icons.stop_rounded),
|
||||
color: iconColor,
|
||||
icon: Icon(
|
||||
Icons.stop_rounded,
|
||||
color: iconColor,
|
||||
),
|
||||
onPressed: playback.track != null
|
||||
? () async {
|
||||
try {
|
||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
|
||||
import 'package:spotube/hooks/playback.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
@ -61,6 +62,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: canShow ? 1 : 0,
|
||||
child: Material(
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
type: MaterialType.transparency,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@ -80,8 +82,10 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_previous_rounded),
|
||||
color: paletteColor.bodyTextColor,
|
||||
icon: Icon(
|
||||
Icons.skip_previous_rounded,
|
||||
color: paletteColor.bodyTextColor,
|
||||
),
|
||||
onPressed: () {
|
||||
onPrevious();
|
||||
}),
|
||||
@ -92,8 +96,8 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
ref.read(playbackProvider).isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
color: paletteColor.bodyTextColor,
|
||||
),
|
||||
color: paletteColor.bodyTextColor,
|
||||
onPressed: Actions.handler<PlayPauseIntent>(
|
||||
context,
|
||||
PlayPauseIntent(ref),
|
||||
@ -102,9 +106,11 @@ class PlayerOverlay extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
icon: Icon(
|
||||
Icons.skip_next_rounded,
|
||||
color: paletteColor.bodyTextColor,
|
||||
),
|
||||
onPressed: () => onNext(),
|
||||
color: paletteColor.bodyTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:spotube/components/Shared/NotFound.dart';
|
||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||
@ -47,9 +48,6 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
var titleStyle = Theme.of(context).textTheme.headline4?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 12.0,
|
||||
@ -61,9 +59,8 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
top: 5.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.navigationRailTheme
|
||||
.backgroundColor
|
||||
color: PlatformTheme.of(context)
|
||||
.scaffoldBackgroundColor
|
||||
?.withOpacity(0.5),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
@ -78,16 +75,13 @@ class PlayerQueue extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
Text("Queue", style: titleStyle),
|
||||
PlatformText.subheading("Queue"),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
child: PlatformText(
|
||||
playback.playlist?.name ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
style: PlatformTextTheme.of(context).body,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
@ -36,13 +37,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
||||
),
|
||||
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md))
|
||||
Flexible(
|
||||
child: Text(
|
||||
child: PlatformText(
|
||||
playback.track?.name ?? "Not playing",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.copyWith(fontWeight: FontWeight.bold, color: color),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
||||
),
|
||||
),
|
||||
|
||||
@ -52,13 +50,10 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
playback.track?.name ?? "Not playing",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText1
|
||||
?.copyWith(fontWeight: FontWeight.bold, color: color),
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
||||
),
|
||||
TypeConversionUtils.artists_X_ClickableArtists(
|
||||
playback.track?.artists ?? [],
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Player/PlayerActions.dart';
|
||||
import 'package:spotube/components/Player/PlayerControls.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
@ -57,7 +58,19 @@ class PlayerView extends HookConsumerWidget {
|
||||
noSetBGColor: true,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
return PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: paletteColor.titleTextColor,
|
||||
toolbarOpacity: 0,
|
||||
leading: PlatformBackButton(
|
||||
color: PlatformProperty.only(
|
||||
macos: Colors.transparent,
|
||||
other: paletteColor.titleTextColor,
|
||||
).resolve(platform!),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
@ -68,15 +81,11 @@ class PlayerView extends HookConsumerWidget {
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
child: Material(
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
color: paletteColor.color.withOpacity(.5),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: const BackButton(),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: paletteColor.titleTextColor,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
@ -162,7 +171,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
floatingQueue: false,
|
||||
extraActions: [
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
tooltip: "Open Lyrics",
|
||||
icon: const Icon(Icons.lyrics_rounded),
|
||||
onPressed: () {
|
||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
@ -40,42 +41,46 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
color: Theme.of(context)
|
||||
.navigationRailTheme
|
||||
.backgroundColor
|
||||
?.withOpacity(0.5),
|
||||
color: PlatformTheme.of(context)
|
||||
.scaffoldBackgroundColor!
|
||||
.withOpacity(.3),
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
appBar: PlatformAppBar(
|
||||
centerTitle: true,
|
||||
title: const Text('Alternative Tracks Sources'),
|
||||
title: PlatformText.subheading(
|
||||
'Alternative Tracks Sources',
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
toolbarOpacity: 0,
|
||||
),
|
||||
body: Padding(
|
||||
body: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ListView.builder(
|
||||
itemCount: playback.siblingYtVideos.length,
|
||||
itemBuilder: (context, index) {
|
||||
final video = playback.siblingYtVideos[index];
|
||||
return ListTile(
|
||||
title: Text(video.title),
|
||||
leading: UniversalImage(
|
||||
path: video.thumbnails.lowResUrl,
|
||||
height: 60,
|
||||
width: 60,
|
||||
return PlatformListTile(
|
||||
title: PlatformText(video.title),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: UniversalImage(
|
||||
path: video.thumbnails.lowResUrl,
|
||||
height: 60,
|
||||
width: 60,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
horizontalTitleGap: 10,
|
||||
trailing: Text(
|
||||
trailing: PlatformText(
|
||||
PrimitiveUtils.toReadableDuration(
|
||||
video.duration ?? Duration.zero,
|
||||
),
|
||||
),
|
||||
subtitle: Text(video.author),
|
||||
subtitle: PlatformText(video.author),
|
||||
enabled: playback.status != PlaybackStatus.loading,
|
||||
selected: video.id == playback.track!.ytTrack.id,
|
||||
selectedTileColor: Theme.of(context).popupMenuTheme.color,
|
||||
|
@ -2,6 +2,8 @@ import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
@ -12,10 +14,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
|
||||
return TextButton(
|
||||
return PlatformTextButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
showPlatformAlertDialog(
|
||||
context,
|
||||
builder: (context) {
|
||||
return HookBuilder(builder: (context) {
|
||||
final playlistName = useTextEditingController();
|
||||
@ -23,14 +25,11 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
final public = useState(false);
|
||||
final collaborative = useState(false);
|
||||
|
||||
return AlertDialog(
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: const Text("Create a Playlist"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
ElevatedButton(
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const Text("Create"),
|
||||
onPressed: () async {
|
||||
if (playlistName.text.isEmpty) return;
|
||||
@ -52,38 +51,41 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
||||
},
|
||||
)
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
],
|
||||
content: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
TextField(
|
||||
PlatformTextField(
|
||||
controller: playlistName,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Name of the playlist",
|
||||
label: Text("Playlist Name"),
|
||||
),
|
||||
placeholder: "Name of the playlist",
|
||||
label: "Playlist Name",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
PlatformTextField(
|
||||
controller: description,
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Description...",
|
||||
),
|
||||
placeholder: "Description...",
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: 5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
PlatformCheckbox(
|
||||
value: public.value,
|
||||
title: const Text("Public"),
|
||||
label: const PlatformText("Public"),
|
||||
onChanged: (val) => public.value = val ?? false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CheckboxListTile(
|
||||
PlatformCheckbox(
|
||||
value: collaborative.value,
|
||||
title: const Text("Collaborative"),
|
||||
label: const PlatformText("Collaborative"),
|
||||
onChanged: (val) => collaborative.value = val ?? false,
|
||||
),
|
||||
],
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
@ -19,8 +20,8 @@ class PlaylistGenreView extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
appBar: PageWindowTitleBar(
|
||||
leading: const PlatformBackButton(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
@ -47,7 +48,7 @@ class PlaylistGenreView extends ConsumerWidget {
|
||||
return const Center(child: Text("Error occurred"));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
return const PlatformCircularProgressIndicator();
|
||||
}
|
||||
return Center(
|
||||
child: Wrap(
|
||||
|
@ -3,12 +3,14 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||
import 'package:spotube/components/Shared/Waypoint.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
@ -17,6 +19,7 @@ import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
@ -56,10 +59,6 @@ class Search extends HookConsumerWidget {
|
||||
job: searchQueryJob(SearchType.artist.key),
|
||||
externalData: getVariables());
|
||||
|
||||
if (auth.isAnonymous) {
|
||||
return const AnonymousFallback();
|
||||
}
|
||||
|
||||
void onSearch() {
|
||||
for (final query in [
|
||||
searchTrack,
|
||||
@ -75,300 +74,297 @@ class Search extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Material(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
ref.read(searchTermStateProvider.notifier).state = value;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
suffix: ElevatedButton(
|
||||
onPressed: onSearch,
|
||||
child: const Icon(Icons.search_rounded),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 7,
|
||||
),
|
||||
hintStyle: const TextStyle(height: 2),
|
||||
hintText: "Search...",
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
onSearch();
|
||||
},
|
||||
),
|
||||
),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
List<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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: PlatformScaffold(
|
||||
appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null,
|
||||
body: auth.isAnonymous
|
||||
? const AnonymousFallback()
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: PlatformTextField(
|
||||
onChanged: (value) {
|
||||
ref.read(searchTermStateProvider.notifier).state =
|
||||
value;
|
||||
},
|
||||
prefixIcon: Icons.search_rounded,
|
||||
prefixIconColor: PlatformProperty.only(
|
||||
ios:
|
||||
PlatformTheme.of(context).textTheme?.caption?.color,
|
||||
other: null,
|
||||
).resolve(platform!),
|
||||
placeholder: "Search...",
|
||||
onSubmitted: (value) {
|
||||
onSearch();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
List<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_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Shared/Hyperlink.dart';
|
||||
import 'package:spotube/hooks/usePackageInfo.dart';
|
||||
|
||||
@ -29,9 +30,12 @@ class About extends HookWidget {
|
||||
version: "2.5.0",
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
return PlatformListTile(
|
||||
leading: const Icon(Icons.info_outline_rounded),
|
||||
title: const Text("About Spotube"),
|
||||
title: PlatformText(
|
||||
"About Spotube",
|
||||
style: PlatformTextTheme.of(context).body,
|
||||
),
|
||||
onTap: () {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
@ -44,7 +48,7 @@ class About extends HookWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Text("Author: "),
|
||||
PlatformText("Author: "),
|
||||
Hyperlink(
|
||||
"Kingkor Roy Tirtho",
|
||||
"https://github.com/KRTirtho",
|
||||
@ -59,12 +63,12 @@ class About extends HookWidget {
|
||||
"💚 Sponsor/Donate 💚",
|
||||
"https://opencollective.com/spotube",
|
||||
),
|
||||
Text(" • "),
|
||||
PlatformText(" • "),
|
||||
Hyperlink(
|
||||
"BSD-4-Clause LICENSE",
|
||||
"https://github.com/KRTirtho/spotube/blob/master/LICENSE",
|
||||
),
|
||||
Text(" • "),
|
||||
PlatformText(" • "),
|
||||
Hyperlink(
|
||||
"Bug Report",
|
||||
"https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=",
|
||||
@ -72,7 +76,8 @@ class About extends HookWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Center(child: Text("© Spotube 2022. All rights reserved"))
|
||||
const Center(
|
||||
child: PlatformText("© Spotube 2022. All rights reserved"))
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
final highContrast = MaterialColor(
|
||||
@ -65,16 +67,11 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
|
||||
},
|
||||
).key);
|
||||
|
||||
return AlertDialog(
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: Text("Pick ${schemeType.name} color scheme"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const Text("Save"),
|
||||
onPressed: () {
|
||||
switch (schemeType) {
|
||||
@ -90,6 +87,15 @@ class ColorSchemePickerDialog extends HookConsumerWidget {
|
||||
},
|
||||
)
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
content: SizedBox(
|
||||
height: 200,
|
||||
width: 400,
|
||||
|
@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Settings/About.dart';
|
||||
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
|
||||
import 'package:spotube/components/Shared/AdaptiveListTile.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/main.dart';
|
||||
import 'package:spotube/models/SpotifyMarkets.dart';
|
||||
import 'package:spotube/models/SpotubeTrack.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
@ -26,9 +28,7 @@ class Settings extends HookConsumerWidget {
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
|
||||
final pickColorScheme = useCallback((ColorSchemeType schemeType) {
|
||||
return () => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return () => showPlatformAlertDialog(context, builder: (context) {
|
||||
return ColorSchemePickerDialog(
|
||||
schemeType: schemeType,
|
||||
);
|
||||
@ -48,12 +48,10 @@ class Settings extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
child: PlatformScaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
center: Text(
|
||||
"Settings",
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
center: PlatformText.headline("Settings"),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -63,10 +61,11 @@ class Settings extends HookConsumerWidget {
|
||||
constraints: const BoxConstraints(maxWidth: 1366),
|
||||
child: ListView(
|
||||
children: [
|
||||
const Text(
|
||||
PlatformText(
|
||||
" Account",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (auth.isAnonymous)
|
||||
AdaptiveListTile(
|
||||
@ -88,7 +87,7 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: (context, update) => ElevatedButton(
|
||||
trailing: (context, update) => PlatformFilledButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push("/login");
|
||||
},
|
||||
@ -99,15 +98,16 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text("Connect with Spotify".toUpperCase()),
|
||||
child: PlatformText(
|
||||
"Connect with Spotify".toUpperCase()),
|
||||
),
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
Builder(builder: (context) {
|
||||
Auth auth = ref.watch(authProvider);
|
||||
return ListTile(
|
||||
return PlatformListTile(
|
||||
leading: const Icon(Icons.logout_rounded),
|
||||
title: const SizedBox(
|
||||
title: SizedBox(
|
||||
height: 50,
|
||||
width: 180,
|
||||
child: Align(
|
||||
@ -115,10 +115,11 @@ class Settings extends HookConsumerWidget {
|
||||
child: AutoSizeText(
|
||||
"Log out of this account",
|
||||
maxLines: 1,
|
||||
style: PlatformTextTheme.of(context).body,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: ElevatedButton(
|
||||
trailing: PlatformFilledButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Colors.red),
|
||||
@ -129,39 +130,41 @@ class Settings extends HookConsumerWidget {
|
||||
auth.logout();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: const Text("Logout"),
|
||||
child: const PlatformText("Logout"),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const Text(
|
||||
PlatformText(
|
||||
" Appearance",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.dashboard_rounded),
|
||||
title: const Text("Layout Mode"),
|
||||
subtitle: const Text(
|
||||
title: const PlatformText("Layout Mode"),
|
||||
subtitle: const PlatformText(
|
||||
"Override responsive layout mode settings",
|
||||
),
|
||||
trailing: (context, update) => DropdownButton<LayoutMode>(
|
||||
trailing: (context, update) =>
|
||||
PlatformDropDownMenu<LayoutMode>(
|
||||
value: preferences.layoutMode,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
items: [
|
||||
PlatformDropDownMenuItem(
|
||||
value: LayoutMode.adaptive,
|
||||
child: Text(
|
||||
child: const PlatformText(
|
||||
"Adaptive",
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: LayoutMode.compact,
|
||||
child: Text(
|
||||
child: const PlatformText(
|
||||
"Compact",
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: LayoutMode.extended,
|
||||
child: Text("Extended"),
|
||||
child: const PlatformText("Extended"),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@ -174,25 +177,22 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.dark_mode_outlined),
|
||||
title: const Text("Theme"),
|
||||
trailing: (context, update) => DropdownButton<ThemeMode>(
|
||||
title: const PlatformText("Theme"),
|
||||
trailing: (context, update) =>
|
||||
PlatformDropDownMenu<ThemeMode>(
|
||||
value: preferences.themeMode,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
items: [
|
||||
PlatformDropDownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Text(
|
||||
"Dark",
|
||||
),
|
||||
child: const PlatformText("Dark"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Text(
|
||||
"Light",
|
||||
),
|
||||
child: const PlatformText("Light"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Text("System"),
|
||||
child: const PlatformText("System"),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@ -203,9 +203,45 @@ class Settings extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.ad_units_rounded),
|
||||
title: const PlatformText("Mimic Platform"),
|
||||
trailing: (context, update) =>
|
||||
PlatformDropDownMenu<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),
|
||||
title: const Text("Accent Color Scheme"),
|
||||
title: const PlatformText("Accent Color Scheme"),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
@ -217,9 +253,9 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
onTap: pickColorScheme(ColorSchemeType.accent),
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.format_color_fill_rounded),
|
||||
title: const Text("Background Color Scheme"),
|
||||
title: const PlatformText("Background Color Scheme"),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 5,
|
||||
@ -231,38 +267,38 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
onTap: pickColorScheme(ColorSchemeType.background),
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.album_rounded),
|
||||
title: const Text("Rotating Album Art"),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
title: const PlatformText("Rotating Album Art"),
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.rotatingAlbumArt,
|
||||
onChanged: (state) {
|
||||
preferences.setRotatingAlbumArt(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
PlatformText(
|
||||
" Playback",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.multitrack_audio_rounded),
|
||||
title: const Text("Audio Quality"),
|
||||
title: const PlatformText("Audio Quality"),
|
||||
trailing: (context, update) =>
|
||||
DropdownButton<AudioQuality>(
|
||||
PlatformDropDownMenu<AudioQuality>(
|
||||
value: preferences.audioQuality,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
items: [
|
||||
PlatformDropDownMenuItem(
|
||||
value: AudioQuality.high,
|
||||
child: Text(
|
||||
child: const PlatformText(
|
||||
"High",
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: AudioQuality.low,
|
||||
child: Text("Low"),
|
||||
child: const PlatformText("Low"),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@ -274,60 +310,54 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
if (kIsMobile)
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.download_for_offline_rounded),
|
||||
title: const Text(
|
||||
title: const PlatformText(
|
||||
"Pre download and play",
|
||||
),
|
||||
subtitle: const Text(
|
||||
subtitle: const PlatformText(
|
||||
"Instead of streaming audio, download bytes and play instead (Recommended for higher bandwidth users)",
|
||||
),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.androidBytesPlay,
|
||||
onChanged: (state) {
|
||||
preferences.setAndroidBytesPlay(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.fast_forward_rounded),
|
||||
title: const Text(
|
||||
title: const PlatformText(
|
||||
"Skip non-music segments (SponsorBlock)",
|
||||
),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.skipSponsorSegments,
|
||||
onChanged: (state) {
|
||||
preferences.setSkipSponsorSegments(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
PlatformText(
|
||||
" Search",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.shopping_bag_rounded),
|
||||
title: Text(
|
||||
"Market Place",
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
subtitle: Text(
|
||||
title: const PlatformText("Market Place"),
|
||||
subtitle: PlatformText.caption(
|
||||
"Recommendation Country",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
trailing: (context, update) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: DropdownButton(
|
||||
isExpanded: true,
|
||||
constraints: const BoxConstraints(maxWidth: 350),
|
||||
child: PlatformDropDownMenu(
|
||||
value: preferences.recommendationMarket,
|
||||
items: spotifyMarkets
|
||||
.map(
|
||||
(country) => (DropdownMenuItem(
|
||||
(country) => (PlatformDropDownMenuItem(
|
||||
value: country.first,
|
||||
child: Text(country.last),
|
||||
child: PlatformText(country.last),
|
||||
)),
|
||||
)
|
||||
.toList(),
|
||||
@ -354,22 +384,19 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: const Text("(Case sensitive)"),
|
||||
subtitle: const PlatformText("(Case sensitive)"),
|
||||
breakOn: Breakpoints.lg,
|
||||
trailing: (context, update) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 450),
|
||||
child: TextField(
|
||||
child: PlatformTextField(
|
||||
controller: ytSearchFormatController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
suffix: ElevatedButton(
|
||||
child: const Icon(Icons.save_rounded),
|
||||
onPressed: () {
|
||||
preferences.setYtSearchFormat(
|
||||
ytSearchFormatController.value.text,
|
||||
);
|
||||
},
|
||||
),
|
||||
suffix: PlatformFilledButton(
|
||||
child: const Icon(Icons.save_rounded),
|
||||
onPressed: () {
|
||||
preferences.setYtSearchFormat(
|
||||
ytSearchFormatController.value.text,
|
||||
);
|
||||
},
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
preferences.setYtSearchFormat(value);
|
||||
@ -380,7 +407,7 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.low_priority_rounded),
|
||||
title: const SizedBox(
|
||||
title: SizedBox(
|
||||
height: 50,
|
||||
width: 180,
|
||||
child: Align(
|
||||
@ -388,28 +415,29 @@ class Settings extends HookConsumerWidget {
|
||||
child: AutoSizeText(
|
||||
"Track Match Algorithm",
|
||||
maxLines: 1,
|
||||
style: PlatformTextTheme.of(context).body,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: (context, update) =>
|
||||
DropdownButton<SpotubeTrackMatchAlgorithm>(
|
||||
PlatformDropDownMenu<SpotubeTrackMatchAlgorithm>(
|
||||
value: preferences.trackMatchAlgorithm,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
items: [
|
||||
PlatformDropDownMenuItem(
|
||||
value: SpotubeTrackMatchAlgorithm.authenticPopular,
|
||||
child: Text(
|
||||
child: const PlatformText(
|
||||
"Popular from Author",
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: SpotubeTrackMatchAlgorithm.popular,
|
||||
child: Text(
|
||||
child: const PlatformText(
|
||||
"Accurately Popular",
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
PlatformDropDownMenuItem(
|
||||
value: SpotubeTrackMatchAlgorithm.youtube,
|
||||
child: Text("YouTube's Top choice"),
|
||||
child: const PlatformText("YouTube's Top choice"),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
@ -420,36 +448,38 @@ class Settings extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
PlatformText(
|
||||
" Downloads",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.file_download_outlined),
|
||||
title: const Text("Download Location"),
|
||||
subtitle: Text(preferences.downloadLocation),
|
||||
trailing: ElevatedButton(
|
||||
title: const PlatformText("Download Location"),
|
||||
subtitle: PlatformText(preferences.downloadLocation),
|
||||
trailing: PlatformFilledButton(
|
||||
onPressed: pickDownloadLocation,
|
||||
child: const Icon(Icons.folder_rounded),
|
||||
),
|
||||
onTap: pickDownloadLocation,
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.lyrics_rounded),
|
||||
title: const Text("Download lyrics along with the Track"),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
title: const PlatformText(
|
||||
"Download lyrics along with the Track"),
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.saveTrackLyrics,
|
||||
onChanged: (state) {
|
||||
preferences.setSaveTrackLyrics(state);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
PlatformText(
|
||||
" About",
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||
style: PlatformTextTheme.of(context)
|
||||
.headline
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(
|
||||
@ -471,13 +501,14 @@ class Settings extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: (context, update) => ElevatedButton.icon(
|
||||
icon: const Icon(Icons.favorite_outline_rounded),
|
||||
label: const Text("Please Sponsor/Donate"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[100],
|
||||
foregroundColor: Colors.pinkAccent,
|
||||
padding: const EdgeInsets.all(15),
|
||||
trailing: (context, update) => PlatformFilledButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStatePropertyAll(Colors.red[100]),
|
||||
foregroundColor:
|
||||
const MaterialStatePropertyAll(Colors.pinkAccent),
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.all(15)),
|
||||
),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
@ -485,13 +516,20 @@ class Settings extends HookConsumerWidget {
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.favorite_outline_rounded),
|
||||
SizedBox(width: 5),
|
||||
PlatformText("Please Sponsor/Donate"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
PlatformListTile(
|
||||
leading: const Icon(Icons.update_rounded),
|
||||
title: const Text("Check for Update"),
|
||||
trailing: Switch.adaptive(
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
title: const PlatformText("Check for Update"),
|
||||
trailing: PlatformSwitch(
|
||||
value: preferences.checkUpdate,
|
||||
onChanged: (checked) =>
|
||||
preferences.setCheckUpdate(checked),
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
|
||||
class AdaptiveListTile extends HookWidget {
|
||||
@ -24,7 +26,7 @@ class AdaptiveListTile extends HookWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
return ListTile(
|
||||
return PlatformListTile(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing:
|
||||
@ -33,11 +35,13 @@ class AdaptiveListTile extends HookWidget {
|
||||
onTap: breakpoint.isLessThan(breakOn)
|
||||
? () {
|
||||
onTap?.call();
|
||||
showDialog(
|
||||
context: context,
|
||||
showPlatformAlertDialog(
|
||||
context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(builder: (context, update) {
|
||||
return AlertDialog(
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: title != null
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@ -49,7 +53,7 @@ class AdaptiveListTile extends HookWidget {
|
||||
Flexible(child: title!),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
: Container(),
|
||||
content: trailing?.call(context, update),
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:popover/popover.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
|
||||
@ -19,28 +20,30 @@ class Action extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isExpanded != true) {
|
||||
return Tooltip(
|
||||
message: text.toStringShallow().split(",").last.replaceAll(
|
||||
"\"",
|
||||
"",
|
||||
),
|
||||
child: IconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
return PlatformIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
tooltip: text is Text
|
||||
? (text as Text).data
|
||||
: text.toStringShallow().split(",").last.replaceAll(
|
||||
"\"",
|
||||
"",
|
||||
),
|
||||
);
|
||||
}
|
||||
return TextButton.icon(
|
||||
return PlatformTextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
padding: const EdgeInsets.all(20),
|
||||
),
|
||||
icon: icon,
|
||||
label: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: text,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 10),
|
||||
text,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -59,7 +62,7 @@ class AdaptiveActions extends HookWidget {
|
||||
final breakpoint = useBreakpoints();
|
||||
|
||||
if (breakpoint.isLessThan(breakOn)) {
|
||||
return IconButton(
|
||||
return PlatformIconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: () {
|
||||
showPopover(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
|
||||
class AnchorButton<T> extends HookWidget {
|
||||
final String text;
|
||||
@ -28,7 +29,7 @@ class AnchorButton<T> extends HookWidget {
|
||||
onTap: onTap,
|
||||
child: MouseRegion(
|
||||
cursor: MaterialStateMouseCursor.clickable,
|
||||
child: Text(
|
||||
child: PlatformText(
|
||||
text,
|
||||
style: style.copyWith(
|
||||
decoration:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
@ -19,10 +20,10 @@ class AnonymousFallback extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text("You're not logged in"),
|
||||
const PlatformText("You're not logged in"),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
child: const Text("Login with Spotify"),
|
||||
PlatformFilledButton(
|
||||
child: const PlatformText("Login with Spotify"),
|
||||
onPressed: () => ServiceUtils.navigate(context, "/settings"),
|
||||
)
|
||||
],
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
|
||||
class DownloadConfirmationDialog extends StatelessWidget {
|
||||
@ -6,22 +8,25 @@ class DownloadConfirmationDialog extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(15),
|
||||
title: Row(
|
||||
children: const [
|
||||
Text("Are you sure?"),
|
||||
SizedBox(width: 10),
|
||||
UniversalImage(
|
||||
path:
|
||||
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
||||
height: 40,
|
||||
width: 40,
|
||||
)
|
||||
],
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Row(
|
||||
children: const [
|
||||
Text("Are you sure?"),
|
||||
SizedBox(width: 10),
|
||||
UniversalImage(
|
||||
path:
|
||||
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
||||
height: 40,
|
||||
width: 40,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -56,19 +61,22 @@ class DownloadConfirmationDialog extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
style: const ButtonStyle(
|
||||
foregroundColor: MaterialStatePropertyAll(Colors.white),
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.red),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text("Accept"),
|
||||
)
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Text("Decline"),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text("Accept"),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
@ -32,7 +33,7 @@ class HeartButton extends ConsumerWidget {
|
||||
|
||||
if (!auth.isLoggedIn) return Container();
|
||||
|
||||
return IconButton(
|
||||
return PlatformIconButton(
|
||||
tooltip: tooltip,
|
||||
icon: Icon(
|
||||
icon ??
|
||||
@ -121,7 +122,7 @@ class TrackHeartButton extends HookConsumerWidget {
|
||||
);
|
||||
final toggler = useTrackToggleLike(track, ref);
|
||||
if (toggler.item3.isLoading || !toggler.item3.hasData) {
|
||||
return const CircularProgressIndicator();
|
||||
return const PlatformCircularProgressIndicator();
|
||||
}
|
||||
|
||||
return HeartButton(
|
||||
@ -181,7 +182,8 @@ class PlaylistHeartButton extends HookConsumerWidget {
|
||||
titleImage,
|
||||
).dominantColor;
|
||||
|
||||
if (me.isLoading || !me.hasData) return const CircularProgressIndicator();
|
||||
if (me.isLoading || !me.hasData)
|
||||
return const PlatformCircularProgressIndicator();
|
||||
|
||||
return HeartButton(
|
||||
isLiked: isLikedQuery.data ?? false,
|
||||
@ -235,7 +237,8 @@ class AlbumHeartButton extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
|
||||
if (me.isLoading || !me.hasData) return const CircularProgressIndicator();
|
||||
if (me.isLoading || !me.hasData)
|
||||
return const PlatformCircularProgressIndicator();
|
||||
|
||||
return HeartButton(
|
||||
isLiked: isLiked,
|
||||
|
@ -1,130 +1,35 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class TitleBarActionButtons extends StatelessWidget {
|
||||
final Color? color;
|
||||
const TitleBarActionButtons({
|
||||
Key? key,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
class PageWindowTitleBar extends PlatformAppBar {
|
||||
PageWindowTitleBar({
|
||||
super.backgroundColor,
|
||||
List<Widget>? actions,
|
||||
super.actionsIconTheme,
|
||||
super.automaticallyImplyLeading = false,
|
||||
super.centerTitle,
|
||||
super.foregroundColor,
|
||||
super.key,
|
||||
super.leading,
|
||||
super.leadingWidth,
|
||||
Widget? center,
|
||||
super.titleSpacing,
|
||||
super.titleTextStyle,
|
||||
super.titleWidth,
|
||||
super.toolbarOpacity,
|
||||
super.toolbarTextStyle,
|
||||
}) : super(
|
||||
actions: [
|
||||
...?actions,
|
||||
if (!kIsMacOS && !kIsMobile) const PlatformWindowButtons(),
|
||||
],
|
||||
title: center,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButtonTheme(
|
||||
data: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
shape: MaterialStateProperty.all(const RoundedRectangleBorder()),
|
||||
overlayColor: MaterialStateProperty.all(Colors.black12),
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
minimumSize: MaterialStateProperty.all(const Size(50, 40)),
|
||||
maximumSize: MaterialStateProperty.all(const Size(50, 40)),
|
||||
),
|
||||
),
|
||||
child: IconTheme(
|
||||
data: const IconThemeData(size: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appWindow.minimize();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).iconTheme.color),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.minimize_rounded,
|
||||
color: color,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
appWindow.maximizeOrRestore();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).iconTheme.color),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.crop_square_rounded,
|
||||
color: color,
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appWindow.close();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
color ?? Theme.of(context).iconTheme.color),
|
||||
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close_rounded,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PageWindowTitleBar extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
final Widget? leading;
|
||||
final Widget? center;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final Size? _preferredSize;
|
||||
const PageWindowTitleBar({
|
||||
Key? key,
|
||||
Size? preferredSize,
|
||||
this.leading,
|
||||
this.center,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
}) : _preferredSize = preferredSize,
|
||||
super(key: key);
|
||||
|
||||
static Size get staticPreferredSize => Size.fromHeight(
|
||||
(kIsDesktop ? appWindow.titleBarHeight : 35),
|
||||
);
|
||||
|
||||
@override
|
||||
Size get preferredSize => _preferredSize ?? staticPreferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (kIsMobile) {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(300),
|
||||
child: Container(
|
||||
color: backgroundColor,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) leading!,
|
||||
Expanded(child: Center(child: center)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return WindowTitleBarBox(
|
||||
child: Container(
|
||||
color: backgroundColor,
|
||||
child: Row(
|
||||
children: [
|
||||
if (kIsMacOS)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.045,
|
||||
),
|
||||
if (leading != null) leading!,
|
||||
Expanded(child: MoveWindow(child: Center(child: center))),
|
||||
if (!kIsMacOS && !kIsMobile)
|
||||
TitleBarActionButtons(color: foregroundColor)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return MoveWindow(child: super.build(context));
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
||||
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
||||
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||
import 'package:spotube/hooks/usePlatformProperty.dart';
|
||||
|
||||
class PlaybuttonCard extends StatelessWidget {
|
||||
class PlaybuttonCard extends HookWidget {
|
||||
final void Function()? onTap;
|
||||
final void Function()? onPlaybuttonPressed;
|
||||
final String? description;
|
||||
@ -26,26 +29,65 @@ class PlaybuttonCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = PlatformTheme.of(context).secondaryBackgroundColor;
|
||||
|
||||
final boxShadow = usePlatformProperty<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(
|
||||
margin: margin,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
splashFactory: splash,
|
||||
highlightColor: Colors.black12,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: HoverBuilder(builder: (context, isHovering) {
|
||||
return Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
[TargetPlatform.windows, TargetPlatform.linux]
|
||||
.contains(platform)
|
||||
? 5
|
||||
: 8,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 5,
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
if (boxShadow != null) boxShadow,
|
||||
],
|
||||
border: [TargetPlatform.windows, TargetPlatform.macOS]
|
||||
.contains(platform)
|
||||
? Border.all(
|
||||
color: PlatformTheme.of(context).borderColor ??
|
||||
Colors.transparent,
|
||||
width: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@ -53,13 +95,23 @@ class PlaybuttonCard extends StatelessWidget {
|
||||
// thumbnail of the playlist
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: UniversalImage(
|
||||
path: imageUrl,
|
||||
width: 200,
|
||||
placeholder: (context, url) =>
|
||||
Image.asset("assets/placeholder.png"),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(
|
||||
platform == TargetPlatform.windows ? 5 : 0,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
[TargetPlatform.windows, TargetPlatform.linux]
|
||||
.contains(platform)
|
||||
? 5
|
||||
: 8,
|
||||
),
|
||||
child: UniversalImage(
|
||||
path: imageUrl,
|
||||
width: 200,
|
||||
placeholder: (context, url) =>
|
||||
Image.asset("assets/placeholder.png"),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.directional(
|
||||
@ -67,27 +119,32 @@ class PlaybuttonCard extends StatelessWidget {
|
||||
bottom: 10,
|
||||
end: 5,
|
||||
child: Builder(builder: (context) {
|
||||
return ElevatedButton(
|
||||
onPressed: onPlaybuttonPressed,
|
||||
style: ButtonStyle(
|
||||
shape: MaterialStateProperty.all(
|
||||
const CircleBorder(),
|
||||
),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.all(16),
|
||||
),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: iconBgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: PlatformIconButton(
|
||||
onPressed: onPlaybuttonPressed,
|
||||
backgroundColor:
|
||||
PlatformTheme.of(context).primaryColor,
|
||||
hoverColor: PlatformTheme.of(context)
|
||||
.primaryColor
|
||||
?.withOpacity(0.5),
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
height: 23,
|
||||
width: 23,
|
||||
child:
|
||||
PlatformCircularProgressIndicator(),
|
||||
)
|
||||
: Icon(
|
||||
isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 23,
|
||||
width: 23,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: Icon(
|
||||
isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
@ -117,13 +174,7 @@ class PlaybuttonCard extends StatelessWidget {
|
||||
height: 30,
|
||||
child: SpotubeMarqueeText(
|
||||
text: description!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
?.color,
|
||||
),
|
||||
style: PlatformTextTheme.of(context).caption,
|
||||
isHovering: isHovering,
|
||||
),
|
||||
),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
|
||||
final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
||||
|
||||
@ -13,7 +15,8 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final groupValue = ref.watch(replaceDownloadedFileState);
|
||||
|
||||
return AlertDialog(
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: Text("Track ${track.name} Already Exists"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -47,20 +50,23 @@ class ReplaceDownloadedFileDialog extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("No"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const Text("Yes"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
)
|
||||
],
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const Text("No"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
|
||||
class SortTracksDropdown extends StatelessWidget {
|
||||
@ -12,43 +13,41 @@ class SortTracksDropdown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<SortBy>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: SortBy.none,
|
||||
enabled: value != SortBy.none,
|
||||
child: const Text("None"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.ascending,
|
||||
enabled: value != SortBy.ascending,
|
||||
child: const Text("Sort by A-Z"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.descending,
|
||||
enabled: value != SortBy.descending,
|
||||
child: const Text("Sort by Z-A"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.dateAdded,
|
||||
enabled: value != SortBy.dateAdded,
|
||||
child: const Text("Sort by Date"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
child: const Text("Sort by Artist"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.album,
|
||||
enabled: value != SortBy.album,
|
||||
child: const Text("Sort by Album"),
|
||||
),
|
||||
];
|
||||
},
|
||||
return PlatformPopupMenuButton<SortBy>(
|
||||
items: [
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.none,
|
||||
enabled: value != SortBy.none,
|
||||
child: const Text("None"),
|
||||
),
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.ascending,
|
||||
enabled: value != SortBy.ascending,
|
||||
child: const Text("Sort by A-Z"),
|
||||
),
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.descending,
|
||||
enabled: value != SortBy.descending,
|
||||
child: const Text("Sort by Z-A"),
|
||||
),
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.dateAdded,
|
||||
enabled: value != SortBy.dateAdded,
|
||||
child: const Text("Sort by Date"),
|
||||
),
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
child: const Text("Sort by Artist"),
|
||||
),
|
||||
PlatformPopupMenuItem(
|
||||
value: SortBy.album,
|
||||
enabled: value != SortBy.album,
|
||||
child: const Text("Sort by Album"),
|
||||
),
|
||||
],
|
||||
onSelected: onChanged,
|
||||
icon: const Icon(Icons.sort_rounded),
|
||||
child: const Icon(Icons.sort_rounded),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +26,12 @@ class SpotubeMarqueeText extends HookWidget {
|
||||
return AutoSizeText(
|
||||
text,
|
||||
minFontSize: 13,
|
||||
style: style,
|
||||
style: DefaultTextStyle.of(context).style.merge(style),
|
||||
maxLines: 1,
|
||||
overflowReplacement: Marquee(
|
||||
key: uKey.value,
|
||||
text: text,
|
||||
style: style,
|
||||
style: DefaultTextStyle.of(context).style.merge(style),
|
||||
scrollAxis: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
blankSpace: 40.0,
|
||||
|
@ -2,6 +2,7 @@ import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
@ -59,7 +60,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
|
||||
final List<Widget> buttons = [
|
||||
if (showShare)
|
||||
IconButton(
|
||||
PlatformIconButton(
|
||||
icon: Icon(
|
||||
Icons.share_rounded,
|
||||
color: color?.titleTextColor,
|
||||
@ -71,13 +72,9 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
// play playlist
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: ElevatedButton(
|
||||
child: PlatformFilledButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.all(Theme.of(context).primaryColor),
|
||||
shape: MaterialStateProperty.all(
|
||||
const CircleBorder(),
|
||||
),
|
||||
shape: MaterialStateProperty.all(const CircleBorder()),
|
||||
),
|
||||
onPressed: tracksSnapshot.data != null ? onPlay : null,
|
||||
child: Icon(
|
||||
@ -112,14 +109,12 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
}, [collapsed.value]);
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
child: PlatformScaffold(
|
||||
appBar: kIsDesktop
|
||||
? PageWindowTitleBar(
|
||||
backgroundColor: color?.color,
|
||||
foregroundColor: color?.titleTextColor,
|
||||
leading: Row(
|
||||
children: [BackButton(color: color?.titleTextColor)],
|
||||
),
|
||||
leading: PlatformBackButton(color: color?.titleTextColor),
|
||||
)
|
||||
: null,
|
||||
body: CustomScrollView(
|
||||
@ -131,16 +126,19 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
pinned: true,
|
||||
expandedHeight: 400,
|
||||
automaticallyImplyLeading: kIsMobile,
|
||||
leading: kIsMobile
|
||||
? PlatformBackButton(color: color?.titleTextColor)
|
||||
: null,
|
||||
iconTheme: IconThemeData(color: color?.titleTextColor),
|
||||
primary: true,
|
||||
backgroundColor: color?.color,
|
||||
title: collapsed.value
|
||||
? Text(
|
||||
? PlatformText.headline(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headline4?.copyWith(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
flexibleSpace: LayoutBuilder(builder: (context, constrains) {
|
||||
@ -158,6 +156,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
type: MaterialType.transparency,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -191,25 +190,19 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
PlatformText.headline(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headline4
|
||||
?.copyWith(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: color?.titleTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (description != null)
|
||||
Text(
|
||||
PlatformText(
|
||||
description!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
?.copyWith(
|
||||
color: color?.bodyTextColor,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: color?.bodyTextColor,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
@ -235,7 +228,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
||||
} else if (tracksSnapshot.hasError &&
|
||||
tracksSnapshot.isError) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Text("Error ${tracksSnapshot.error}"));
|
||||
child: PlatformText("Error ${tracksSnapshot.error}"));
|
||||
}
|
||||
|
||||
final tracks = tracksSnapshot.data!;
|
||||
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart' hide Action;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart' hide Image;
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/components/Shared/AdaptivePopupMenuButton.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/LinkText.dart';
|
||||
@ -73,7 +75,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
SnackBar(
|
||||
width: 300,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
content: Text(
|
||||
content: PlatformText(
|
||||
"Copied $data to clipboard",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@ -83,81 +85,78 @@ class TrackTile extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> actionAddToPlaylist() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: spotify.playlists.me.all().then((playlists) async {
|
||||
final me = await spotify.me.get();
|
||||
return playlists.where((playlist) =>
|
||||
playlist.owner?.id != null &&
|
||||
playlist.owner!.id == me.id);
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
return HookBuilder(builder: (context) {
|
||||
final playlistsCheck = useState(<String, bool>{});
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
"Add `${track.value.name}` to following Playlists"),
|
||||
titleTextStyle:
|
||||
Theme.of(context).textTheme.bodyText1?.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Cancel"),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
ElevatedButton(
|
||||
child: const Text("Add"),
|
||||
onPressed: () async {
|
||||
final selectedPlaylists = playlistsCheck
|
||||
.value.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((entry) => entry.key);
|
||||
showPlatformAlertDialog(context, builder: (context) {
|
||||
return FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: spotify.playlists.me.all().then((playlists) async {
|
||||
final me = await spotify.me.get();
|
||||
return playlists.where((playlist) =>
|
||||
playlist.owner?.id != null && playlist.owner!.id == me.id);
|
||||
}),
|
||||
builder: (context, snapshot) {
|
||||
return HookBuilder(builder: (context) {
|
||||
final playlistsCheck = useState(<String, bool>{});
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: PlatformText(
|
||||
"Add `${track.value.name}` to following Playlists",
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
secondaryActions: [
|
||||
PlatformFilledButton(
|
||||
isSecondary: true,
|
||||
child: const PlatformText("Cancel"),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const PlatformText("Add"),
|
||||
onPressed: () async {
|
||||
final selectedPlaylists = playlistsCheck.value.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((entry) => entry.key);
|
||||
|
||||
await Future.wait(
|
||||
selectedPlaylists.map(
|
||||
(playlistId) => spotify.playlists
|
||||
.addTrack(track.value.uri!, playlistId),
|
||||
),
|
||||
).then((_) => Navigator.pop(context));
|
||||
},
|
||||
)
|
||||
],
|
||||
content: SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: !snapshot.hasData
|
||||
? const Center(
|
||||
child: CircularProgressIndicator.adaptive())
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist =
|
||||
snapshot.data!.elementAt(index);
|
||||
return CheckboxListTile(
|
||||
title: Text(playlist.name!),
|
||||
controlAffinity:
|
||||
ListTileControlAffinity.leading,
|
||||
value: playlistsCheck.value[playlist.id] ??
|
||||
false,
|
||||
onChanged: (val) {
|
||||
playlistsCheck.value = {
|
||||
...playlistsCheck.value,
|
||||
playlist.id!: val == true
|
||||
};
|
||||
},
|
||||
);
|
||||
await Future.wait(
|
||||
selectedPlaylists.map(
|
||||
(playlistId) => spotify.playlists
|
||||
.addTrack(track.value.uri!, playlistId),
|
||||
),
|
||||
).then((_) => Navigator.pop(context));
|
||||
},
|
||||
)
|
||||
],
|
||||
content: SizedBox(
|
||||
height: 300,
|
||||
width: 300,
|
||||
child: !snapshot.hasData
|
||||
? const Center(
|
||||
child: PlatformCircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = snapshot.data!.elementAt(index);
|
||||
return PlatformCheckbox(
|
||||
label: PlatformText(playlist.name!),
|
||||
value:
|
||||
playlistsCheck.value[playlist.id] ?? false,
|
||||
onChanged: (val) {
|
||||
playlistsCheck.value = {
|
||||
...playlistsCheck.value,
|
||||
playlist.id!: val == true
|
||||
};
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final String thumbnailUrl = TypeConversionUtils.image_X_UrlString(
|
||||
@ -178,10 +177,11 @@ class TrackTile extends HookConsumerWidget {
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
textStyle: PlatformTheme.of(context).textTheme!.body!,
|
||||
child: Row(
|
||||
children: [
|
||||
if (showCheck)
|
||||
Checkbox(
|
||||
PlatformCheckbox(
|
||||
value: isChecked,
|
||||
onChanged: (s) => onCheckChange?.call(s),
|
||||
)
|
||||
@ -190,7 +190,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
height: 20,
|
||||
width: 25,
|
||||
child: Center(
|
||||
child: Text((track.key + 1).toString()),
|
||||
child: PlatformText((track.key + 1).toString()),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@ -214,23 +214,29 @@ class TrackTile extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
playback.track?.id != null &&
|
||||
playback.track?.id == track.value.id
|
||||
? Icons.pause_circle_rounded
|
||||
: Icons.play_circle_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
onPressed: () => onTrackPlayButtonPressed?.call(
|
||||
track.value,
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0).copyWith(left: 0),
|
||||
child: PlatformIconButton(
|
||||
icon: Icon(
|
||||
playback.track?.id != null &&
|
||||
playback.track?.id == track.value.id
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
backgroundColor: PlatformTheme.of(context).primaryColor,
|
||||
hoverColor:
|
||||
PlatformTheme.of(context).primaryColor?.withOpacity(0.5),
|
||||
onPressed: () => onTrackPlayButtonPressed?.call(
|
||||
track.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
track.value.name ?? "",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -239,7 +245,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
isReallyLocal
|
||||
? Text(
|
||||
? PlatformText(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.value.artists ?? []),
|
||||
)
|
||||
@ -255,7 +261,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum)
|
||||
Expanded(
|
||||
child: isReallyLocal
|
||||
? Text(track.value.album?.name ?? "")
|
||||
? PlatformText(track.value.album?.name ?? "")
|
||||
: LinkText(
|
||||
track.value.album!.name!,
|
||||
"/album/${track.value.album?.id}",
|
||||
@ -265,7 +271,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
),
|
||||
if (!breakpoint.isSm) ...[
|
||||
const SizedBox(width: 10),
|
||||
Text(duration),
|
||||
PlatformText(duration),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
if (!isReallyLocal)
|
||||
@ -279,7 +285,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
color: Colors.pink,
|
||||
)
|
||||
: const Icon(Icons.favorite_border_rounded),
|
||||
text: const Text("Save as favorite"),
|
||||
text: const PlatformText("Save as favorite"),
|
||||
onPressed: () {
|
||||
toggler.item2.mutate(Tuple2(spotify, toggler.item1));
|
||||
},
|
||||
@ -287,18 +293,18 @@ class TrackTile extends HookConsumerWidget {
|
||||
if (auth.isLoggedIn)
|
||||
Action(
|
||||
icon: const Icon(Icons.add_box_rounded),
|
||||
text: const Text("Add To playlist"),
|
||||
text: const PlatformText("Add To playlist"),
|
||||
onPressed: actionAddToPlaylist,
|
||||
),
|
||||
if (userPlaylist && auth.isLoggedIn)
|
||||
Action(
|
||||
icon: const Icon(Icons.remove_circle_outline_rounded),
|
||||
text: const Text("Remove from playlist"),
|
||||
text: const PlatformText("Remove from playlist"),
|
||||
onPressed: actionRemoveFromPlaylist,
|
||||
),
|
||||
Action(
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
text: const Text("Share"),
|
||||
text: const PlatformText("Share"),
|
||||
onPressed: () {
|
||||
actionShare(track.value);
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart';
|
||||
@ -67,7 +68,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
if (heading != null) heading!,
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
PlatformCheckbox(
|
||||
value: selected.value.length == sortedTracks.length,
|
||||
onChanged: (checked) {
|
||||
if (!showCheck.value) showCheck.value = true;
|
||||
@ -81,7 +82,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
child: PlatformText(
|
||||
"#",
|
||||
textAlign: TextAlign.center,
|
||||
style: tableHeadStyle,
|
||||
@ -90,7 +91,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
"Title",
|
||||
style: tableHeadStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -104,7 +105,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
PlatformText(
|
||||
"Album",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: tableHeadStyle,
|
||||
@ -115,7 +116,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
],
|
||||
if (!breakpoint.isSm) ...[
|
||||
const SizedBox(width: 10),
|
||||
Text("Time", style: tableHeadStyle),
|
||||
PlatformText("Time", style: tableHeadStyle),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
SortTracksDropdown(
|
||||
@ -126,32 +127,29 @@ class TracksTableView extends HookConsumerWidget {
|
||||
.state = value;
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: selected.value.isNotEmpty,
|
||||
value: "download",
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.file_download_outlined),
|
||||
Text(
|
||||
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
|
||||
),
|
||||
],
|
||||
),
|
||||
PlatformPopupMenuButton(
|
||||
items: [
|
||||
PlatformPopupMenuItem(
|
||||
enabled: selected.value.isNotEmpty,
|
||||
value: "download",
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.file_download_outlined),
|
||||
PlatformText(
|
||||
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
onSelected: (action) async {
|
||||
switch (action) {
|
||||
case "download":
|
||||
{
|
||||
final isConfirmed = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const DownloadConfirmationDialog();
|
||||
});
|
||||
final isConfirmed = await showPlatformAlertDialog(
|
||||
context, builder: (context) {
|
||||
return const DownloadConfirmationDialog();
|
||||
});
|
||||
if (isConfirmed != true) return;
|
||||
for (final selectedTrack in selectedTracks) {
|
||||
downloader.addToQueue(selectedTrack);
|
||||
@ -163,6 +161,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
default:
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/components/Home/Sidebar.dart';
|
||||
import 'package:spotube/components/Shared/AnchorButton.dart';
|
||||
import 'package:spotube/hooks/usePackageInfo.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
@ -51,41 +53,40 @@ void useUpdateChecker(WidgetRef ref) {
|
||||
final latestVersion = value.last;
|
||||
if (currentVersion == null || latestVersion == null) return;
|
||||
if (latestVersion <= currentVersion) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
const url =
|
||||
"https://spotube.netlify.app/other-downloads/stable-downloads";
|
||||
return AlertDialog(
|
||||
title: const Text("Spotube has an update"),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
child: const Text("Download Now"),
|
||||
onPressed: () => download(url),
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
showPlatformAlertDialog(context, builder: (context) {
|
||||
const url =
|
||||
"https://spotube.netlify.app/other-downloads/stable-downloads";
|
||||
return PlatformAlertDialog(
|
||||
macosAppIcon: Sidebar.brandLogo(),
|
||||
title: const PlatformText("Spotube has an update"),
|
||||
primaryActions: [
|
||||
PlatformFilledButton(
|
||||
child: const Text("Download Now"),
|
||||
onPressed: () => download(url),
|
||||
),
|
||||
],
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text("Spotube v${value.last} has been released"),
|
||||
Row(
|
||||
children: [
|
||||
Text("Spotube v${value.last} has been released"),
|
||||
Row(
|
||||
children: [
|
||||
const Text("Read the latest "),
|
||||
AnchorButton(
|
||||
"release notes",
|
||||
style: const TextStyle(color: Colors.blue),
|
||||
onTap: () => launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
],
|
||||
const PlatformText("Read the latest "),
|
||||
AnchorButton(
|
||||
"release notes",
|
||||
style: const TextStyle(color: Colors.blue),
|
||||
onTap: () => launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
return null;
|
||||
}, [packageInfo, isCheckUpdateEnabled]);
|
||||
|
@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
||||
import 'package:spotube/entities/CacheTrack.dart';
|
||||
@ -106,8 +107,8 @@ void main() async {
|
||||
logger.v(
|
||||
"[onFileExists] download confirmation for ${track.name}",
|
||||
);
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
return showPlatformAlertDialog<bool>(
|
||||
context,
|
||||
builder: (_) =>
|
||||
ReplaceDownloadedFileDialog(track: track),
|
||||
).then((s) => s ?? false);
|
||||
@ -140,6 +141,11 @@ class Spotube extends StatefulHookConsumerWidget {
|
||||
|
||||
@override
|
||||
SpotubeState createState() => SpotubeState();
|
||||
|
||||
/// ↓↓ ADDED
|
||||
/// InheritedWidget style accessor to our State object.
|
||||
static SpotubeState of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<SpotubeState>()!;
|
||||
}
|
||||
|
||||
class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
||||
@ -153,6 +159,11 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
SharedPreferences.getInstance().then(((value) => localStorage = value));
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
setState(() {
|
||||
appPlatform = Theme.of(context).platform;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -180,6 +191,13 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
||||
prevSize = appWindow.size;
|
||||
}
|
||||
|
||||
TargetPlatform appPlatform = TargetPlatform.android;
|
||||
|
||||
void changePlatform(TargetPlatform targetPlatform) {
|
||||
appPlatform = targetPlatform;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeMode =
|
||||
@ -198,57 +216,82 @@ class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: router,
|
||||
platform = appPlatform;
|
||||
|
||||
return PlatformApp.router(
|
||||
routeInformationParser: router.routeInformationParser,
|
||||
routerDelegate: router.routerDelegate,
|
||||
routeInformationProvider: router.routeInformationProvider,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Spotube',
|
||||
theme: lightTheme(
|
||||
androidTheme: lightTheme(
|
||||
accentMaterialColor: accentMaterialColor,
|
||||
backgroundMaterialColor: backgroundMaterialColor,
|
||||
),
|
||||
darkTheme: darkTheme(
|
||||
androidDarkTheme: darkTheme(
|
||||
accentMaterialColor: accentMaterialColor,
|
||||
backgroundMaterialColor: backgroundMaterialColor,
|
||||
),
|
||||
linuxTheme: linuxTheme,
|
||||
linuxDarkTheme: linuxDarkTheme,
|
||||
iosTheme: themeMode == ThemeMode.dark ? iosDarkTheme : iosTheme,
|
||||
windowsTheme: windowsTheme,
|
||||
windowsDarkTheme: windowsDarkTheme,
|
||||
macosTheme: macosTheme,
|
||||
macosDarkTheme: macosDarkTheme,
|
||||
themeMode: themeMode,
|
||||
shortcuts: {
|
||||
...WidgetsApp.defaultShortcuts,
|
||||
const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref),
|
||||
const SingleActivator(LogicalKeyboardKey.comma, control: true):
|
||||
windowButtonConfig: kIsDesktop
|
||||
? PlatformWindowButtonConfig(
|
||||
isMaximized: () => appWindow.isMaximized,
|
||||
onClose: appWindow.close,
|
||||
onRestore: appWindow.restore,
|
||||
onMaximize: appWindow.maximize,
|
||||
onMinimize: appWindow.minimize,
|
||||
)
|
||||
: null,
|
||||
shortcuts: PlatformProperty.all({
|
||||
...WidgetsApp.defaultShortcuts.map((key, value) {
|
||||
return MapEntry(
|
||||
LogicalKeySet.fromSet(key.triggers?.toSet() ?? {}),
|
||||
value,
|
||||
);
|
||||
}),
|
||||
LogicalKeySet(LogicalKeyboardKey.space): PlayPauseIntent(ref),
|
||||
LogicalKeySet(LogicalKeyboardKey.comma, LogicalKeyboardKey.control):
|
||||
NavigationIntent(router, "/settings"),
|
||||
const SingleActivator(
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.keyB,
|
||||
control: true,
|
||||
shift: true,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
): HomeTabIntent(ref, tab: HomeTabs.browse),
|
||||
const SingleActivator(
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.keyS,
|
||||
control: true,
|
||||
shift: true,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
): HomeTabIntent(ref, tab: HomeTabs.search),
|
||||
const SingleActivator(
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.keyL,
|
||||
control: true,
|
||||
shift: true,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
): HomeTabIntent(ref, tab: HomeTabs.library),
|
||||
const SingleActivator(
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.keyY,
|
||||
control: true,
|
||||
shift: true,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
): HomeTabIntent(ref, tab: HomeTabs.lyrics),
|
||||
const SingleActivator(
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.keyW,
|
||||
control: true,
|
||||
shift: true,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.shift,
|
||||
): CloseAppIntent(),
|
||||
},
|
||||
actions: {
|
||||
}),
|
||||
actions: PlatformProperty.all({
|
||||
...WidgetsApp.defaultActions,
|
||||
PlayPauseIntent: PlayPauseAction(),
|
||||
NavigationIntent: NavigationAction(),
|
||||
HomeTabIntent: HomeTabAction(),
|
||||
CloseAppIntent: CloseAppAction(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ ThemeData darkTheme({
|
||||
),
|
||||
dialogTheme: DialogTheme(backgroundColor: backgroundMaterialColor[900]),
|
||||
cardColor: backgroundMaterialColor[800],
|
||||
canvasColor: backgroundMaterialColor[900],
|
||||
canvasColor: backgroundMaterialColor[800],
|
||||
listTileTheme: const ListTileThemeData(horizontalTitleGap: 0),
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith((states) {
|
||||
@ -89,5 +89,8 @@ ThemeData darkTheme({
|
||||
unselectedLabelStyle:
|
||||
const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: backgroundMaterialColor[900],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import 'package:adwaita/adwaita.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:macos_ui/macos_ui.dart';
|
||||
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' as FluentUI;
|
||||
|
||||
final materialWhite = MaterialColor(Colors.white.value, {
|
||||
50: Colors.white,
|
||||
@ -115,3 +119,49 @@ ThemeData lightTheme({
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final windowsTheme = FluentUI.ThemeData.light().copyWith(
|
||||
buttonTheme: FluentUI.ButtonThemeData(
|
||||
iconButtonStyle: FluentUI.ButtonStyle(
|
||||
iconSize: FluentUI.ButtonState.all(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
final windowsDarkTheme = FluentUI.ThemeData.dark().copyWith(
|
||||
buttonTheme: FluentUI.ButtonThemeData(
|
||||
iconButtonStyle: FluentUI.ButtonStyle(
|
||||
iconSize: FluentUI.ButtonState.all(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
final macosTheme = MacosThemeData.light().copyWith(
|
||||
pushButtonTheme: const PushButtonThemeData(
|
||||
secondaryColor: Colors.white,
|
||||
),
|
||||
iconTheme: const MacosIconThemeData(size: 14),
|
||||
typography: MacosTypography(color: Colors.grey[900]!),
|
||||
);
|
||||
final macosDarkTheme = MacosThemeData.dark().copyWith(
|
||||
pushButtonTheme: const PushButtonThemeData(
|
||||
secondaryColor: Colors.white,
|
||||
),
|
||||
iconTheme: const MacosIconThemeData(size: 14),
|
||||
typography: MacosTypography(color: MacosColors.textColor),
|
||||
);
|
||||
const iosTheme = CupertinoThemeData(brightness: Brightness.light);
|
||||
const iosDarkTheme = CupertinoThemeData(
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
|
||||
final linuxTheme = AdwaitaThemeData.light().copyWith(
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: Colors.grey[900],
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
);
|
||||
final linuxDarkTheme = AdwaitaThemeData.dark().copyWith(
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: Colors.grey[50],
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import audio_session
|
||||
import audioplayers_darwin
|
||||
import bitsdojo_window_macos
|
||||
import connectivity_plus_macos
|
||||
import macos_ui
|
||||
import metadata_god
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
@ -23,6 +24,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
|
||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||
MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin"))
|
||||
MetadataGodPlugin.register(with: registry.registrar(forPlugin: "MetadataGodPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
|
105
pubspec.lock
105
pubspec.lock
@ -8,6 +8,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "50.0.0"
|
||||
adwaita:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: adwaita
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -254,7 +261,7 @@ packages:
|
||||
name: build_resolvers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
version: "2.1.0"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -493,6 +500,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
fluent_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluent_ui
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -561,6 +575,11 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -582,6 +601,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.42.0"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -627,6 +653,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
gsettings:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gsettings
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.5"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -690,6 +723,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
introduction_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -718,6 +758,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
libadwaita:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: libadwaita
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.5"
|
||||
libadwaita_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: libadwaita_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.4"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -739,6 +793,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
macos_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: macos_ui
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.5"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -867,6 +928,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -972,6 +1047,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
platform_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../platform_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -993,6 +1075,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.6+3"
|
||||
popover_gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: popover_gtk
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.6+3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1021,6 +1110,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1035,6 +1131,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.3"
|
||||
scroll_pos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scroll_pos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
scroll_to_index:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -62,6 +62,12 @@ dependencies:
|
||||
flutter_inappwebview: ^5.4.3+7
|
||||
tuple: ^2.0.1
|
||||
uuid: ^3.0.6
|
||||
platform_ui:
|
||||
path: ../platform_ui
|
||||
fluent_ui: ^4.0.3
|
||||
macos_ui: ^1.7.5
|
||||
libadwaita: ^1.2.5
|
||||
adwaita: ^0.5.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user