Merge pull request #323 from KRTirtho/experimental/platform_ui

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

2
.vscode/launch.json vendored
View File

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

View File

@ -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,
),
);
return SizedBox(
height: 240,
width: 200,
child: InkWell(
onTap: () {
ServiceUtils.navigate(context, "/artist/${artist.id}");
},
borderRadius: BorderRadius.circular(10),
child: HoverBuilder(builder: (context, isHovering) {
return Ink(
width: 200,
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
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}");
},
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: PlatformTheme.of(context).secondaryBackgroundColor,
borderRadius: BorderRadius.circular(
platform == TargetPlatform.windows ? 5 : 8,
),
boxShadow: [
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,
),
),

View File

@ -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
child: PlatformIconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_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()),
);
}

View File

@ -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(

View File

@ -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) {

View File

@ -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(
return PlatformScaffold(
body: Sidebar(
selectedIndex: index.value,
onSelectedIndexChanged: (selectedIndex) {
index.value = selectedIndex;
GoRouter.of(context).go(_path[selectedIndex]!);
onSelectedIndexChanged: (i) {
index.value = i;
GoRouter.of(context).go(rootPaths[index.value]!);
},
),
Expanded(child: child),
],
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]!);
},
),
],

View File

@ -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,66 +127,56 @@ class Sidebar extends HookConsumerWidget {
),
if (!kIsDesktop && !extended.value)
Center(
child: IconButton(
child: PlatformIconButton(
icon: const Icon(Icons.menu_rounded),
onPressed: toggleExtended,
),
),
(extended.value)
? Row(
? Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
_buildSmallLogo(),
brandLogo(),
const SizedBox(
width: 10,
),
Text(
"Spotube",
style: Theme.of(context).textTheme.headline4,
),
IconButton(
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,
: brandLogo(),
],
),
windowsFooterItems: [
FluentUI.PaneItemAction(
icon: const FluentUI.Icon(FluentUI.FluentIcons.settings),
onTap: () => goToSettings(context),
),
animationType: BadgeAnimationType.fade,
child: icon,
)
: icon,
label: Text(
e.title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
],
footer: SidebarFooter(extended: extended.value),
),
);
},
).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onSelectedIndexChanged,
extended: extended.value,
),
),
SizedBox(
width: extended.value ? 256 : 80,
}
}
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(
@ -185,7 +199,7 @@ class Sidebar extends HookConsumerWidget {
return;
}, [auth.isLoggedIn, me.hasData]);
if (extended.value) {
if (extended) {
return Padding(
padding: const EdgeInsets.all(16).copyWith(left: 0),
child: Row(
@ -193,20 +207,17 @@ class Sidebar extends HookConsumerWidget {
children: [
if (auth.isLoggedIn && data == null)
const Center(
child: CircularProgressIndicator(),
child: PlatformCircularProgressIndicator(),
)
else if (data != null)
Flexible(
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(
avatarImg),
onBackgroundImageError:
(exception, stackTrace) =>
UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
height: 16,
@ -222,27 +233,27 @@ class Sidebar extends HookConsumerWidget {
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
style: PlatformTheme.of(context)
.textTheme
?.body
?.copyWith(fontWeight: FontWeight.bold),
),
),
],
),
),
IconButton(
PlatformIconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => goToSettings(context)),
onPressed: () => Sidebar.goToSettings(context)),
],
));
} else {
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () => goToSettings(context),
onTap: () => Sidebar.goToSettings(context),
child: CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(avatarImg),
backgroundImage: UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
@ -255,10 +266,6 @@ class Sidebar extends HookConsumerWidget {
}
},
),
)
],
),
),
);
}
}

View File

@ -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);

View File

@ -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,7 +29,12 @@ class UserAlbums extends HookConsumerWidget {
}
return SingleChildScrollView(
child: Padding(
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
@ -34,6 +46,7 @@ class UserAlbums extends HookConsumerWidget {
.toList(),
),
),
),
);
}
}

View File

@ -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,7 +35,11 @@ class UserArtists extends HookConsumerWidget {
? false
: (artistQuery.pages.last?.items?.length ?? 0) == 15;
return GridView.builder(
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,
@ -48,6 +59,7 @@ class UserArtists extends HookConsumerWidget {
}
return ArtistCard(artists[index]);
},
),
);
}
}

View File

@ -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>[],

View File

@ -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,
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: [
Tab(text: "Playlist"),
Tab(text: "Downloads"),
Tab(text: "Local"),
Tab(text: "Artists"),
Tab(text: "Album"),
PlatformTab(label: 'Playlists', icon: const SizedBox.shrink()),
PlatformTab(label: 'Tracks', icon: const SizedBox.shrink()),
PlatformTab(label: 'Downloads', icon: const SizedBox.shrink()),
PlatformTab(label: 'Artists', icon: const SizedBox.shrink()),
PlatformTab(label: 'Albums', icon: const SizedBox.shrink()),
],
),
),
body: const TabBarView(children: [
AnonymousFallback(child: UserPlaylists()),
UserDownloads(),
UserLocalTracks(),
AnonymousFallback(child: UserArtists()),
AnonymousFallback(child: UserAlbums()),
]),
),
body: body,
),
);
}

View File

@ -9,6 +9,7 @@ import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:path_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);

View File

@ -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,7 +42,11 @@ class UserPlaylists extends HookConsumerWidget {
}
return SingleChildScrollView(
child: Padding(
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
@ -49,6 +61,7 @@ class UserPlaylists extends HookConsumerWidget {
],
),
),
),
);
}
}

View File

@ -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,

View File

@ -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),

View File

@ -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();

View File

@ -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: [

View File

@ -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),

View File

@ -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(),
],
),
),

View File

@ -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(

View File

@ -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"),
)
],
),

View File

@ -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(

View File

@ -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,23 +41,22 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
},
)
],
content: Row(
content: SizedBox(
height: 100,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
PlatformIconButton(
icon: const Icon(Icons.remove_rounded),
onPressed: () {
controller.text = "${getValue() - 25}ms";
},
),
Flexible(
child: TextField(
child: PlatformTextField(
keyboardType: TextInputType.number,
controller: controller,
decoration: const InputDecoration(
isDense: true,
hintText: "Delay in milliseconds",
),
placeholder: "Delay in milliseconds",
onSubmitted: (_) {
Navigator.of(context).pop(
Duration(
@ -61,7 +66,7 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
},
),
),
IconButton(
PlatformIconButton(
icon: const Icon(Icons.add_rounded),
onPressed: () {
controller.text = "${getValue() + 25}ms";
@ -69,6 +74,7 @@ class LyricDelayAdjustDialog extends HookConsumerWidget {
),
],
),
),
);
}
}

View File

@ -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,17 +37,39 @@ class Lyrics extends HookConsumerWidget {
noSetBGColor: true,
);
return DefaultTabController(
length: 2,
child: Scaffold(
final body = [
SyncedLyrics(palette: palette),
GeniusLyrics(palette: palette),
][index.value];
return PlatformScaffold(
extendBodyBehindAppBar: true,
appBar: const TabBar(
isScrollable: 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: [
Tab(text: "Synced Lyrics"),
Tab(text: "Lyrics (genius.com)"),
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(
@ -56,15 +82,7 @@ class Lyrics extends HookConsumerWidget {
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),
],
),
),
),
child: SafeArea(child: body),
),
),
),

View File

@ -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,13 +87,18 @@ class SyncedLyrics extends HookConsumerWidget {
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
tooltip: "Lyrics Delay",
icon: const Icon(Icons.av_timer_rounded),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: PlatformFilledButton(
child: const Icon(
Icons.av_timer_rounded,
size: 16,
),
onPressed: () async {
final delay = await showDialog(
context: context,
builder: (context) => const LyricDelayAdjustDialog(),
final delay = await showPlatformAlertDialog(
context,
builder: (context) =>
const LyricDelayAdjustDialog(),
);
if (delay != null) {
ref.read(lyricDelayState.notifier).state = delay;
@ -100,6 +107,7 @@ class SyncedLyrics extends HookConsumerWidget {
),
),
),
),
],
),
),

View File

@ -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,

View File

@ -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

View File

@ -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),
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,
),
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
),
IconButton(
PlatformIconButton(
tooltip: "Next track",
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext(),
icon: Icon(
Icons.skip_next_rounded,
color: iconColor,
),
IconButton(
onPressed: () => onNext(),
),
PlatformIconButton(
tooltip: "Stop playback",
icon: const Icon(Icons.stop_rounded),
icon: Icon(
Icons.stop_rounded,
color: iconColor,
),
onPressed: playback.track != null
? () async {
try {

View File

@ -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),
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,
),
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
@ -102,10 +106,12 @@ class PlayerOverlay extends HookConsumerWidget {
},
),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext(),
icon: Icon(
Icons.skip_next_rounded,
color: paletteColor.bodyTextColor,
),
onPressed: () => onNext(),
),
],
),
],

View File

@ -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),

View File

@ -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 ?? [],

View File

@ -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: () {

View File

@ -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(
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,

View File

@ -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,
),
],

View File

@ -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(

View File

@ -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,33 +74,29 @@ class Search extends HookConsumerWidget {
}
return SafeArea(
child: Material(
color: Theme.of(context).backgroundColor,
child: Column(
child: PlatformScaffold(
appBar: kIsDesktop && !kIsMacOS ? PageWindowTitleBar() : null,
body: auth.isAnonymous
? const AnonymousFallback()
: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
color: Theme.of(context).backgroundColor,
child: TextField(
child: PlatformTextField(
onChanged: (value) {
ref.read(searchTermStateProvider.notifier).state = 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...",
),
prefixIcon: Icons.search_rounded,
prefixIconColor: PlatformProperty.only(
ios:
PlatformTheme.of(context).textTheme?.caption?.color,
other: null,
).resolve(platform!),
placeholder: "Search...",
onSubmitted: (value) {
onSearch();
},
@ -144,16 +139,13 @@ class Search extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Text(
"Songs",
style: Theme.of(context).textTheme.headline5,
),
PlatformText.headline("Songs"),
if (searchTrack.isLoading &&
!searchTrack.isFetchingNextPage)
const CircularProgressIndicator()
const PlatformCircularProgressIndicator()
else if (searchTrack.hasError)
Text(
searchTrack.error?[searchTrack.pageParams.last])
PlatformText(searchTrack
.error?[searchTrack.pageParams.last])
else
...tracks.asMap().entries.map((track) {
String duration =
@ -162,8 +154,10 @@ class Search extends HookConsumerWidget {
playback,
track: track,
duration: duration,
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async {
isActive:
playback.track?.id == track.value.id,
onTrackPlayButtonPressed:
(currentTrack) async {
var isPlaylistPlaying =
playback.playlist?.id != null &&
playback.playlist?.id ==
@ -184,39 +178,38 @@ class Search extends HookConsumerWidget {
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.track?.id) {
currentTrack.id !=
playback.track?.id) {
playback.play(currentTrack);
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
if (searchTrack.hasNextPage &&
tracks.isNotEmpty)
Center(
child: TextButton(
child: PlatformTextButton(
onPressed: searchTrack.isFetchingNextPage
? null
: () => searchTrack.fetchNextPage(),
child: searchTrack.isFetchingNextPage
? const CircularProgressIndicator()
: const Text("Load more"),
? const PlatformCircularProgressIndicator()
: const PlatformText("Load more"),
),
),
if (playlists.isNotEmpty)
Text(
"Playlists",
style: Theme.of(context).textTheme.headline5,
),
PlatformText.headline("Playlists"),
const SizedBox(height: 10),
if (searchPlaylist.isLoading &&
!searchPlaylist.isFetchingNextPage)
const CircularProgressIndicator()
const PlatformCircularProgressIndicator()
else if (searchPlaylist.hasError)
Text(searchPlaylist
PlatformText(searchPlaylist
.error?[searchPlaylist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
@ -236,10 +229,12 @@ class Search extends HookConsumerWidget {
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist.hasNextPage) {
searchPlaylist
.hasNextPage) {
return Waypoint(
onEnter: () {
searchPlaylist.fetchNextPage();
searchPlaylist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
@ -256,21 +251,18 @@ class Search extends HookConsumerWidget {
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Text(
"Artists",
style: Theme.of(context).textTheme.headline5,
),
PlatformText.headline("Artists"),
const SizedBox(height: 10),
if (searchArtist.isLoading &&
!searchArtist.isFetchingNextPage)
const CircularProgressIndicator()
const PlatformCircularProgressIndicator()
else if (searchArtist.hasError)
Text(searchArtist
PlatformText(searchArtist
.error?[searchArtist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
@ -289,7 +281,8 @@ class Search extends HookConsumerWidget {
searchArtist.hasNextPage) {
return Waypoint(
onEnter: () {
searchArtist.fetchNextPage();
searchArtist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
@ -297,7 +290,8 @@ class Search extends HookConsumerWidget {
);
}
return Container(
margin: const EdgeInsets.symmetric(
margin: const EdgeInsets
.symmetric(
horizontal: 15),
child: ArtistCard(artist),
);
@ -310,21 +304,22 @@ class Search extends HookConsumerWidget {
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Text(
PlatformText(
"Albums",
style: Theme.of(context).textTheme.headline5,
style:
Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
if (searchAlbum.isLoading &&
!searchAlbum.isFetchingNextPage)
const CircularProgressIndicator()
const PlatformCircularProgressIndicator()
else if (searchAlbum.hasError)
Text(
searchAlbum.error?[searchAlbum.pageParams.last])
PlatformText(searchAlbum
.error?[searchAlbum.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
@ -344,7 +339,8 @@ class Search extends HookConsumerWidget {
onEnter: () {
searchAlbum.fetchNextPage();
},
child: const ShimmerPlaybuttonCard(
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}

View File

@ -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"))
]);
},
);

View File

@ -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,

View File

@ -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,15 +384,13 @@ 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(
suffix: PlatformFilledButton(
child: const Icon(Icons.save_rounded),
onPressed: () {
preferences.setYtSearchFormat(
@ -370,7 +398,6 @@ class Settings extends HookConsumerWidget {
);
},
),
),
onSubmitted: (value) {
preferences.setYtSearchFormat(value);
update?.call(() {});
@ -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),

View File

@ -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),
);
});

View File

@ -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(
return PlatformIconButton(
icon: icon,
onPressed: onPressed,
tooltip: text is Text
? (text as Text).data
: text.toStringShallow().split(",").last.replaceAll(
"\"",
"",
),
child: IconButton(
icon: icon,
onPressed: onPressed,
),
);
}
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(

View File

@ -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:

View File

@ -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"),
)
],

View File

@ -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,9 +8,11 @@ class DownloadConfirmationDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(15),
title: Row(
return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: Padding(
padding: const EdgeInsets.all(15),
child: Row(
children: const [
Text("Are you sure?"),
SizedBox(width: 10),
@ -20,8 +24,9 @@ class DownloadConfirmationDialog extends StatelessWidget {
)
],
),
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"),
)
],
);
}

View File

@ -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,

View File

@ -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));
}
}

View File

@ -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,8 +95,17 @@ class PlaybuttonCard extends StatelessWidget {
// thumbnail of the playlist
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
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,
@ -62,31 +113,37 @@ class PlaybuttonCard extends StatelessWidget {
Image.asset("assets/placeholder.png"),
),
),
),
Positioned.directional(
textDirection: TextDirection.ltr,
bottom: 10,
end: 5,
child: Builder(builder: (context) {
return ElevatedButton(
return Container(
decoration: BoxDecoration(
color: iconBgColor,
shape: BoxShape.circle,
),
child: PlatformIconButton(
onPressed: onPlaybuttonPressed,
style: ButtonStyle(
shape: MaterialStateProperty.all(
const CircleBorder(),
),
padding: MaterialStateProperty.all(
const EdgeInsets.all(16),
),
),
child: isLoading
backgroundColor:
PlatformTheme.of(context).primaryColor,
hoverColor: PlatformTheme.of(context)
.primaryColor
?.withOpacity(0.5),
icon: isLoading
? const SizedBox(
height: 23,
width: 23,
child: CircularProgressIndicator(),
child:
PlatformCircularProgressIndicator(),
)
: Icon(
isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
color: Colors.white,
),
),
);
}),
@ -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,
),
),

View File

@ -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);
},
),
],
);
}
}

View File

@ -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(
return PlatformPopupMenuButton<SortBy>(
items: [
PlatformPopupMenuItem(
value: SortBy.none,
enabled: value != SortBy.none,
child: const Text("None"),
),
PopupMenuItem(
PlatformPopupMenuItem(
value: SortBy.ascending,
enabled: value != SortBy.ascending,
child: const Text("Sort by A-Z"),
),
PopupMenuItem(
PlatformPopupMenuItem(
value: SortBy.descending,
enabled: value != SortBy.descending,
child: const Text("Sort by Z-A"),
),
PopupMenuItem(
PlatformPopupMenuItem(
value: SortBy.dateAdded,
enabled: value != SortBy.dateAdded,
child: const Text("Sort by Date"),
),
PopupMenuItem(
PlatformPopupMenuItem(
value: SortBy.artist,
enabled: value != SortBy.artist,
child: const Text("Sort by Artist"),
),
PopupMenuItem(
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),
);
}
}

View File

@ -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,

View File

@ -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,13 +126,16 @@ 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(
style: TextStyle(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
@ -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,23 +190,17 @@ class TrackCollectionView<T> extends HookConsumerWidget {
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
PlatformText.headline(
title,
style: Theme.of(context)
.textTheme
.headline4
?.copyWith(
style: TextStyle(
color: color?.titleTextColor,
fontWeight: FontWeight.w600,
),
),
if (description != null)
Text(
PlatformText(
description!,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
style: TextStyle(
color: color?.bodyTextColor,
),
maxLines: 2,
@ -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!;

View File

@ -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,37 +85,37 @@ class TrackTile extends HookConsumerWidget {
}
Future<void> actionAddToPlaylist() async {
showDialog(
context: context,
builder: (context) {
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);
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(
return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: PlatformText(
"Add `${track.value.name}` to following Playlists",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
actions: [
TextButton(
child: const Text("Cancel"),
),
secondaryActions: [
PlatformFilledButton(
isSecondary: true,
child: const PlatformText("Cancel"),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
child: const Text("Add"),
],
primaryActions: [
PlatformFilledButton(
child: const PlatformText("Add"),
onPressed: () async {
final selectedPlaylists = playlistsCheck
.value.entries
final selectedPlaylists = playlistsCheck.value.entries
.where((entry) => entry.value)
.map((entry) => entry.key);
@ -131,19 +133,16 @@ class TrackTile extends HookConsumerWidget {
width: 300,
child: !snapshot.hasData
? const Center(
child: CircularProgressIndicator.adaptive())
child: PlatformCircularProgressIndicator())
: 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,
final playlist = snapshot.data!.elementAt(index);
return PlatformCheckbox(
label: PlatformText(playlist.name!),
value:
playlistsCheck.value[playlist.id] ?? false,
onChanged: (val) {
playlistsCheck.value = {
...playlistsCheck.value,
@ -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(
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_circle_rounded
: Icons.play_circle_rounded,
color: Theme.of(context).primaryColor,
? 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);
},

View File

@ -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,30 +127,27 @@ class TracksTableView extends HookConsumerWidget {
.state = value;
},
),
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
PlatformPopupMenuButton(
items: [
PlatformPopupMenuItem(
enabled: selected.value.isNotEmpty,
value: "download",
child: Row(
children: [
const Icon(Icons.file_download_outlined),
Text(
PlatformText(
"Download ${selectedTracks.isNotEmpty ? "(${selectedTracks.length})" : ""}",
),
],
),
),
];
},
],
onSelected: (action) async {
switch (action) {
case "download":
{
final isConfirmed = await showDialog(
context: context,
builder: (context) {
final isConfirmed = await showPlatformAlertDialog(
context, builder: (context) {
return const DownloadConfirmationDialog();
});
if (isConfirmed != true) return;
@ -163,6 +161,7 @@ class TracksTableView extends HookConsumerWidget {
default:
}
},
child: const Icon(Icons.more_vert),
),
],
),

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:platform_ui/platform_ui.dart';
T usePlatformProperty<T>(
PlatformProperty<T> Function(BuildContext context) getProperties) {
final context = useContext();
return getProperties(context).resolve(platform ?? Theme.of(context).platform);
}

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_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,15 +53,14 @@ void useUpdateChecker(WidgetRef ref) {
final latestVersion = value.last;
if (currentVersion == null || latestVersion == null) return;
if (latestVersion <= currentVersion) return;
showDialog(
context: context,
builder: (context) {
showPlatformAlertDialog(context, builder: (context) {
const url =
"https://spotube.netlify.app/other-downloads/stable-downloads";
return AlertDialog(
title: const Text("Spotube has an update"),
actions: [
ElevatedButton(
return PlatformAlertDialog(
macosAppIcon: Sidebar.brandLogo(),
title: const PlatformText("Spotube has an update"),
primaryActions: [
PlatformFilledButton(
child: const Text("Download Now"),
onPressed: () => download(url),
),
@ -71,7 +72,7 @@ void useUpdateChecker(WidgetRef ref) {
Text("Spotube v${value.last} has been released"),
Row(
children: [
const Text("Read the latest "),
const PlatformText("Read the latest "),
AnchorButton(
"release notes",
style: const TextStyle(color: Colors.blue),

View File

@ -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(),
},
}),
);
}
}

View File

@ -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],
),
);
}

View File

@ -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,
),
);

View File

@ -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"))

View File

@ -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:

View File

@ -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: