feat: titlebar complete compatibility, platform specific login, library tabbar in titlebar

This commit is contained in:
Kingkor Roy Tirtho 2022-11-12 13:30:21 +06:00
parent e659e3c56f
commit b3c27d1fca
16 changed files with 449 additions and 455 deletions

View File

@ -6,6 +6,7 @@ import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Category/CategoryCard.dart'; import 'package:spotube/components/Category/CategoryCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/components/Shared/Waypoint.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -42,6 +43,7 @@ class Genres extends HookConsumerWidget {
]; ];
return PlatformScaffold( return PlatformScaffold(
appBar: PageWindowTitleBar(),
body: ListView.builder( body: ListView.builder(
itemCount: categories.length, itemCount: categories.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {

View File

@ -64,40 +64,14 @@ class Shell extends HookConsumerWidget {
return null; return null;
}, [backgroundColor]); }, [backgroundColor]);
final allowedPath =
rootPaths.values.contains(GoRouter.of(context).location);
final titleBar = PageWindowTitleBar(
backgroundColor:
platform == TargetPlatform.android ? Colors.transparent : null,
);
final preferredSize = allowedPath ? titleBar.preferredSize : Size.zero;
var appBar = kIsDesktop
? PreferredSize(
preferredSize: preferredSize,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
height: allowedPath ? titleBar.preferredSize.height : 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: allowedPath ? 1 : 0,
child: titleBar,
),
),
)
: null;
return PlatformScaffold( return PlatformScaffold(
appBar: platform == TargetPlatform.windows ? appBar : null,
extendBodyBehindAppBar: false,
body: Sidebar( body: Sidebar(
selectedIndex: index.value, selectedIndex: index.value,
onSelectedIndexChanged: (i) { onSelectedIndexChanged: (i) {
index.value = i; index.value = i;
GoRouter.of(context).go(rootPaths[index.value]!); GoRouter.of(context).go(rootPaths[index.value]!);
}, },
child: PlatformScaffold( child: child,
appBar: platform != TargetPlatform.windows ? appBar : null,
body: child,
),
), ),
extendBody: true, extendBody: true,
bottomNavigationBar: Column( bottomNavigationBar: Column(

View File

@ -133,18 +133,21 @@ class Sidebar extends HookConsumerWidget {
), ),
), ),
(extended.value) (extended.value)
? Row( ? Padding(
children: [ padding: const EdgeInsets.all(8.0),
brandLogo(), child: Row(
const SizedBox( children: [
width: 10, brandLogo(),
), const SizedBox(
PlatformText.headline("Spotube"), width: 10,
PlatformIconButton( ),
icon: const Icon(Icons.menu_rounded), PlatformText.headline("Spotube"),
onPressed: toggleExtended, PlatformIconButton(
), icon: const Icon(Icons.menu_rounded),
], onPressed: toggleExtended,
),
],
),
) )
: brandLogo(), : brandLogo(),
], ],

View File

@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -13,6 +15,10 @@ class UserAlbums extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final albumsQuery = useQuery( final albumsQuery = useQuery(
job: currentUserAlbumsQueryJob, job: currentUserAlbumsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),

View File

@ -5,7 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/components/Shared/Waypoint.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -14,6 +16,10 @@ class UserArtists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final artistQuery = useInfiniteQuery( final artistQuery = useInfiniteQuery(
job: currentUserFollowingArtistsQueryJob, job: currentUserFollowingArtistsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),

View File

@ -1,47 +1,48 @@
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Library/UserAlbums.dart'; import 'package:spotube/components/Library/UserAlbums.dart';
import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserDownloads.dart'; import 'package:spotube/components/Library/UserDownloads.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
class UserLibrary extends ConsumerWidget { class UserLibrary extends HookConsumerWidget {
const UserLibrary({Key? key}) : super(key: key); const UserLibrary({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return DefaultTabController( final index = useState(0);
length: 5,
child: SafeArea( final body = [
child: PlatformTabView( const UserPlaylists(),
const UserLocalTracks(),
const UserDownloads(),
const UserArtists(),
const UserAlbums(),
][index.value];
return PlatformScaffold(
appBar: PageWindowTitleBar(
titleWidth: 415,
centerTitle: true,
center: PlatformTabBar(
androidIsScrollable: true, androidIsScrollable: true,
placement: PlatformProperty.all(PlatformTabbarPlacement.top), selectedIndex: index.value,
body: { onSelectedIndexChanged: (value) => index.value = value,
PlatformTab( isNavigational:
label: "Playlist", PlatformProperty.byPlatformGroup(mobile: false, desktop: true),
icon: Container(), tabs: [
): const AnonymousFallback(child: UserPlaylists()), PlatformTab(label: 'Playlists', icon: const SizedBox.shrink()),
PlatformTab( PlatformTab(label: 'Tracks', icon: const SizedBox.shrink()),
label: "Downloads", PlatformTab(label: 'Downloads', icon: const SizedBox.shrink()),
icon: Container(), PlatformTab(label: 'Artists', icon: const SizedBox.shrink()),
): const UserDownloads(), PlatformTab(label: 'Albums', icon: const SizedBox.shrink()),
PlatformTab( ],
label: "Local",
icon: Container(),
): const UserLocalTracks(),
PlatformTab(
label: "Artists",
icon: Container(),
): const AnonymousFallback(child: UserArtists()),
PlatformTab(
label: "Album",
icon: Container(),
): const AnonymousFallback(child: UserAlbums()),
},
), ),
), ),
body: body,
); );
} }
} }

View File

@ -6,6 +6,8 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart'; import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -14,6 +16,11 @@ class UserPlaylists extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider);
if (auth.isAnonymous) {
return const AnonymousFallback();
}
final playlistsQuery = useQuery( final playlistsQuery = useQuery(
job: currentUserPlaylistsQueryJob, job: currentUserPlaylistsQueryJob,
externalData: ref.watch(spotifyProvider), externalData: ref.watch(spotifyProvider),

View File

@ -14,46 +14,56 @@ class LoginTutorial extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authProvider); final auth = ref.watch(authProvider);
final key = GlobalKey<State<IntroductionScreen>>();
return Scaffold( return PlatformScaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
leading: PlatformTextButton( leading: PlatformTextButton(
child: const Text("Exit"), child: const PlatformText("Exit"),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
), ),
body: IntroductionScreen( body: IntroductionScreen(
next: const Text("Next"), key: key,
back: const Text("Previous"), overrideBack: PlatformFilledButton(
isSecondary: true,
child: const Center(child: PlatformText("Previous")),
onPressed: () {
(key.currentState as IntroductionScreenState).previous();
},
),
overrideNext: PlatformFilledButton(
child: const Center(child: PlatformText("Next")),
onPressed: () {
(key.currentState as IntroductionScreenState).next();
},
),
showBackButton: true, showBackButton: true,
overrideDone: PlatformTextButton( overrideDone: PlatformFilledButton(
onPressed: auth.isLoggedIn onPressed: auth.isLoggedIn
? () { ? () {
ServiceUtils.navigate(context, "/"); ServiceUtils.navigate(context, "/");
} }
: null, : null,
child: const Text("Done"), child: const Center(child: PlatformText("Done")),
), ),
pages: [ pages: [
PageViewModel( PageViewModel(
title: "Step 1", title: "Step 1",
image: Image.asset("assets/tutorial/step-1.png"), image: Image.asset("assets/tutorial/step-1.png"),
bodyWidget: Wrap( bodyWidget: Wrap(
children: [ children: const [
Text( PlatformText(
"First, Go to ", "First, Go to ",
style: Theme.of(context).textTheme.bodyText1,
), ),
Hyperlink( Hyperlink(
"accounts.spotify.com ", "accounts.spotify.com ",
"https://accounts.spotify.com", "https://accounts.spotify.com",
style: Theme.of(context).textTheme.bodyText1!,
), ),
Text( PlatformText(
"and Login/Sign up if you're not logged in", "and Login/Sign up if you're not logged in",
style: Theme.of(context).textTheme.bodyText1,
), ),
], ],
), ),
@ -61,10 +71,9 @@ class LoginTutorial extends ConsumerWidget {
PageViewModel( PageViewModel(
title: "Step 2", title: "Step 2",
image: Image.asset("assets/tutorial/step-2.png"), image: Image.asset("assets/tutorial/step-2.png"),
bodyWidget: Text( bodyWidget: const PlatformText(
"1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection", "1. Once you're logged in, press F12 or Mouse Right Click > Inspect to Open the Browser devtools.\n2. Then go the \"Application\" Tab (Chrome, Edge, Brave etc..) or \"Storage\" Tab (Firefox, Palemoon etc..)\n3. Go to the \"Cookies\" section then the \"https://accounts.spotify.com\" subsection",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
PageViewModel( PageViewModel(
@ -72,10 +81,9 @@ class LoginTutorial extends ConsumerWidget {
image: Image.asset( image: Image.asset(
"assets/tutorial/step-3.png", "assets/tutorial/step-3.png",
), ),
bodyWidget: Text( bodyWidget: const PlatformText(
"Copy the values of \"sp_dc\" and \"sp_key\" Cookies", "Copy the values of \"sp_dc\" and \"sp_key\" Cookies",
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.bodyText1,
), ),
), ),
if (auth.isLoggedIn) if (auth.isLoggedIn)
@ -92,13 +100,12 @@ class LoginTutorial extends ConsumerWidget {
PageViewModel( PageViewModel(
title: "Step 5", title: "Step 5",
bodyWidget: Column( bodyWidget: Column(
children: [ children: const [
Text( PlatformText(
"Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields", "Paste the copied \"sp_dc\" and \"sp_key\" values in the respective fields",
style: Theme.of(context).textTheme.bodyText1,
), ),
const SizedBox(height: 10), SizedBox(height: 10),
const TokenLoginForm(), TokenLoginForm(),
], ],
), ),
), ),

View File

@ -15,12 +15,17 @@ class TokenLogin extends HookConsumerWidget {
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return SafeArea( return SafeArea(
child: Scaffold( child: PlatformScaffold(
appBar: PageWindowTitleBar(leading: PlatformBackButton()), appBar: PageWindowTitleBar(leading: const PlatformBackButton()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 10), margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: PlatformTheme.of(context).secondaryBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
child: Column( child: Column(
children: [ children: [
Image.asset( Image.asset(
@ -28,11 +33,11 @@ class TokenLogin extends HookConsumerWidget {
width: MediaQuery.of(context).size.width * width: MediaQuery.of(context).size.width *
(breakpoint <= Breakpoints.md ? .5 : .3), (breakpoint <= Breakpoints.md ? .5 : .3),
), ),
Text("Add your spotify credentials to get started", PlatformText("Add your spotify credentials to get started",
style: breakpoint <= Breakpoints.md style: breakpoint <= Breakpoints.md
? textTheme.headline5 ? textTheme.headline5
: textTheme.headline4), : textTheme.headline4),
Text( PlatformText(
"Don't worry, any of your credentials won't be collected or shared with anyone", "Don't worry, any of your credentials won't be collected or shared with anyone",
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
), ),
@ -45,9 +50,9 @@ class TokenLogin extends HookConsumerWidget {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
const Text("Don't know how to do this?"), const PlatformText("Don't know how to do this?"),
PlatformTextButton( PlatformTextButton(
child: const Text( child: const PlatformText(
"Follow along the Step by Step guide", "Follow along the Step by Step guide",
), ),
onPressed: () => GoRouter.of(context).push( onPressed: () => GoRouter.of(context).push(

View File

@ -25,21 +25,17 @@ class TokenLoginForm extends HookConsumerWidget {
), ),
child: Column( child: Column(
children: [ children: [
TextField( PlatformTextField(
controller: directCodeController, controller: directCodeController,
decoration: const InputDecoration( placeholder: "Spotify \"sp_dc\" Cookie",
hintText: "Spotify \"sp_dc\" Cookie", label: "sp_dc Cookie",
label: Text("sp_dc Cookie"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TextField( PlatformTextField(
controller: keyCodeController, controller: keyCodeController,
decoration: const InputDecoration( placeholder: "Spotify \"sp_key\" Cookie",
hintText: "Spotify \"sp_key\" Cookie", label: "sp_key Cookie",
label: Text("sp_key Cookie"),
),
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -49,7 +45,7 @@ class TokenLoginForm extends HookConsumerWidget {
directCodeController.text.isEmpty) { directCodeController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please fill in all fields"), content: PlatformText("Please fill in all fields"),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
@ -68,7 +64,7 @@ class TokenLoginForm extends HookConsumerWidget {
onDone?.call(); onDone?.call();
} }
}, },
child: const Text("Submit"), child: const PlatformText("Submit"),
) )
], ],
), ),

View File

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

View File

@ -3,38 +3,20 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/components/Lyrics/GeniusLyrics.dart'; import 'package:spotube/components/Lyrics/GeniusLyrics.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart'; import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class Lyrics extends HookConsumerWidget { class Lyrics extends HookConsumerWidget {
const Lyrics({Key? key}) : super(key: key); const Lyrics({Key? key}) : super(key: key);
Widget buildContainer(Widget child, String albumArt, PaletteColor palette) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(albumArt),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: child,
),
),
);
}
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
@ -47,6 +29,7 @@ class Lyrics extends HookConsumerWidget {
[playback.track?.album?.images], [playback.track?.album?.images],
); );
final palette = usePaletteColor(albumArt, ref); final palette = usePaletteColor(albumArt, ref);
final index = useState(0);
useCustomStatusBarColor( useCustomStatusBarColor(
palette.color, palette.color,
@ -54,52 +37,42 @@ class Lyrics extends HookConsumerWidget {
noSetBGColor: true, noSetBGColor: true,
); );
return SafeArea( final body = [
child: PlatformTabView( SyncedLyrics(palette: palette),
body: { GeniusLyrics(palette: palette),
PlatformTab( ][index.value];
label: "Synced Lyrics",
icon: Container(),
): buildContainer(SyncedLyrics(palette: palette), albumArt, palette),
PlatformTab(
label: "Lyrics (genius.com)",
icon: Container(),
): buildContainer(GeniusLyrics(palette: palette), albumArt, palette),
},
),
);
return SafeArea( return PlatformScaffold(
child: Scaffold( extendBodyBehindAppBar: true,
extendBodyBehindAppBar: true, appBar: !kIsMacOS
appBar: const TabBar( ? PageWindowTitleBar(
isScrollable: true, backgroundColor: Colors.transparent,
tabs: [ toolbarOpacity: 0,
Tab(text: "Synced Lyrics"), center: PlatformTabBar(
Tab(text: "Lyrics (genius.com)"), isNavigational:
], PlatformProperty.only(linux: true, other: false),
), selectedIndex: index.value,
body: Container( onSelectedIndexChanged: (value) => index.value = value,
clipBehavior: Clip.hardEdge, tabs: [
decoration: BoxDecoration( PlatformTab(label: "Synced", icon: const SizedBox.shrink()),
image: DecorationImage( PlatformTab(label: "Genius", icon: const SizedBox.shrink()),
image: UniversalImage.imageProvider(albumArt), ],
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: SafeArea(
child: TabBarView(
children: [
SyncedLyrics(palette: palette),
GeniusLyrics(palette: palette),
],
),
), ),
), )
: null,
body: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(albumArt),
fit: BoxFit.cover,
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: SafeArea(child: body),
), ),
), ),
), ),

View File

@ -58,7 +58,14 @@ class PlayerView extends HookConsumerWidget {
noSetBGColor: true, noSetBGColor: true,
); );
return Scaffold( return PlatformScaffold(
appBar: PageWindowTitleBar(
backgroundColor: Colors.transparent,
foregroundColor: paletteColor.titleTextColor,
toolbarOpacity: 0,
automaticallyImplyLeading: true,
),
extendBodyBehindAppBar: true,
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
@ -74,11 +81,6 @@ class PlayerView extends HookConsumerWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
PageWindowTitleBar(
leading: const PlatformBackButton(),
backgroundColor: Colors.transparent,
foregroundColor: paletteColor.titleTextColor,
),
Padding( Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Column( child: Column(

View File

@ -10,6 +10,7 @@ import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart'; import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TrackTile.dart'; import 'package:spotube/components/Shared/TrackTile.dart';
import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/components/Shared/Waypoint.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
@ -18,6 +19,7 @@ import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -57,10 +59,6 @@ class Search extends HookConsumerWidget {
job: searchQueryJob(SearchType.artist.key), job: searchQueryJob(SearchType.artist.key),
externalData: getVariables()); externalData: getVariables());
if (auth.isAnonymous) {
return const AnonymousFallback();
}
void onSearch() { void onSearch() {
for (final query in [ for (final query in [
searchTrack, searchTrack,
@ -75,281 +73,288 @@ class Search extends HookConsumerWidget {
} }
} }
return SafeArea( return PlatformScaffold(
child: Material( appBar: !kIsMacOS ? PageWindowTitleBar() : null,
color: PlatformTheme.of(context).scaffoldBackgroundColor, body: auth.isAnonymous
textStyle: PlatformTheme.of(context).textTheme!.body!, ? const AnonymousFallback()
child: Column( : Column(
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, horizontal: 20,
vertical: 10, vertical: 10,
), ),
child: PlatformTextField( child: PlatformTextField(
onChanged: (value) { onChanged: (value) {
ref.read(searchTermStateProvider.notifier).state = value; ref.read(searchTermStateProvider.notifier).state = value;
}, },
prefixIcon: Icons.search_rounded, prefixIcon: Icons.search_rounded,
placeholder: "Search...", placeholder: "Search...",
onSubmitted: (value) { onSubmitted: (value) {
onSearch(); onSearch();
}, },
), ),
), ),
HookBuilder( HookBuilder(
builder: (context) { builder: (context) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
List<AlbumSimple> albums = []; List<AlbumSimple> albums = [];
List<Artist> artists = []; List<Artist> artists = [];
List<Track> tracks = []; List<Track> tracks = [];
List<PlaylistSimple> playlists = []; List<PlaylistSimple> playlists = [];
final pages = [ final pages = [
...searchTrack.pages, ...searchTrack.pages,
...searchAlbum.pages, ...searchAlbum.pages,
...searchPlaylist.pages, ...searchPlaylist.pages,
...searchArtist.pages, ...searchArtist.pages,
].expand<Page>((page) => page ?? []).toList(); ].expand<Page>((page) => page ?? []).toList();
for (MapEntry<int, Page> page in pages.asMap().entries) { for (MapEntry<int, Page> page in pages.asMap().entries) {
for (var item in page.value.items ?? []) { for (var item in page.value.items ?? []) {
if (item is AlbumSimple) { if (item is AlbumSimple) {
albums.add(item); albums.add(item);
} else if (item is PlaylistSimple) { } else if (item is PlaylistSimple) {
playlists.add(item); playlists.add(item);
} else if (item is Artist) { } else if (item is Artist) {
artists.add(item); artists.add(item);
} else if (item is Track) { } else if (item is Track) {
tracks.add(item); tracks.add(item);
}
}
} }
} return Expanded(
} child: SingleChildScrollView(
return Expanded( child: Padding(
child: SingleChildScrollView( padding: const EdgeInsets.symmetric(
child: Padding( vertical: 8,
padding: const EdgeInsets.symmetric( horizontal: 20,
vertical: 8, ),
horizontal: 20, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, if (tracks.isNotEmpty)
children: [ PlatformText.headline("Songs"),
if (tracks.isNotEmpty) PlatformText.headline("Songs"), if (searchTrack.isLoading &&
if (searchTrack.isLoading && !searchTrack.isFetchingNextPage)
!searchTrack.isFetchingNextPage) const PlatformCircularProgressIndicator()
const PlatformCircularProgressIndicator() else if (searchTrack.hasError)
else if (searchTrack.hasError) PlatformText(searchTrack
PlatformText( .error?[searchTrack.pageParams.last])
searchTrack.error?[searchTrack.pageParams.last]) else
else ...tracks.asMap().entries.map((track) {
...tracks.asMap().entries.map((track) { String duration =
String duration = "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; return TrackTile(
return TrackTile( playback,
playback, track: track,
track: track, duration: duration,
duration: duration, isActive:
isActive: playback.track?.id == track.value.id, playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async { onTrackPlayButtonPressed:
var isPlaylistPlaying = (currentTrack) async {
playback.playlist?.id != null && var isPlaylistPlaying =
playback.playlist?.id == playback.playlist?.id != null &&
currentTrack.id; playback.playlist?.id ==
if (!isPlaylistPlaying) { currentTrack.id;
playback.playPlaylist( if (!isPlaylistPlaying) {
CurrentPlaylist( playback.playPlaylist(
tracks: [currentTrack], CurrentPlaylist(
id: currentTrack.id!, tracks: [currentTrack],
name: currentTrack.name!, id: currentTrack.id!,
thumbnail: TypeConversionUtils name: currentTrack.name!,
.image_X_UrlString( thumbnail: TypeConversionUtils
currentTrack.album?.images, .image_X_UrlString(
placeholder: currentTrack.album?.images,
ImagePlaceholder.albumArt, placeholder:
), ImagePlaceholder.albumArt,
), ),
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.track?.id) {
playback.play(currentTrack);
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: PlatformTextButton(
onPressed: searchTrack.isFetchingNextPage
? null
: () => searchTrack.fetchNextPage(),
child: searchTrack.isFetchingNextPage
? const PlatformCircularProgressIndicator()
: const PlatformText("Load more"),
),
),
if (playlists.isNotEmpty)
PlatformText.headline("Playlists"),
const SizedBox(height: 10),
if (searchPlaylist.isLoading &&
!searchPlaylist.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchPlaylist.hasError)
PlatformText(searchPlaylist
.error?[searchPlaylist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist.hasNextPage) {
return Waypoint(
onEnter: () {
searchPlaylist.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}
return PlaylistCard(playlist);
},
),
],
),
),
),
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
PlatformText.headline("Artists"),
const SizedBox(height: 10),
if (searchArtist.isLoading &&
!searchArtist.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchArtist.hasError)
PlatformText(searchArtist
.error?[searchArtist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController,
child: Row(
children: [
...artists.mapIndexed(
(i, artist) {
if (i == artists.length - 1 &&
searchArtist.hasNextPage) {
return Waypoint(
onEnter: () {
searchArtist.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 15),
child: ArtistCard(artist),
);
},
),
],
),
),
),
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
PlatformText(
"Albums",
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
if (searchAlbum.isLoading &&
!searchAlbum.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchAlbum.hasError)
PlatformText(
searchAlbum.error?[searchAlbum.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: [
...albums.mapIndexed((i, album) {
if (i == albums.length - 1 &&
searchAlbum.hasNextPage) {
return Waypoint(
onEnter: () {
searchAlbum.fetchNextPage();
},
child: const ShimmerPlaybuttonCard(
count: 1),
);
}
return AlbumCard(
TypeConversionUtils
.simpleAlbum_X_Album(
album,
), ),
); );
}), } else if (isPlaylistPlaying &&
], currentTrack.id != null &&
currentTrack.id !=
playback.track?.id) {
playback.play(currentTrack);
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: PlatformTextButton(
onPressed: searchTrack.isFetchingNextPage
? null
: () => searchTrack.fetchNextPage(),
child: searchTrack.isFetchingNextPage
? const PlatformCircularProgressIndicator()
: const PlatformText("Load more"),
), ),
), ),
), if (playlists.isNotEmpty)
), PlatformText.headline("Playlists"),
], const SizedBox(height: 10),
if (searchPlaylist.isLoading &&
!searchPlaylist.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchPlaylist.hasError)
PlatformText(searchPlaylist
.error?[searchPlaylist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist.hasNextPage) {
return Waypoint(
onEnter: () {
searchPlaylist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}
return PlaylistCard(playlist);
},
),
],
),
),
),
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
PlatformText.headline("Artists"),
const SizedBox(height: 10),
if (searchArtist.isLoading &&
!searchArtist.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchArtist.hasError)
PlatformText(searchArtist
.error?[searchArtist.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController,
child: Row(
children: [
...artists.mapIndexed(
(i, artist) {
if (i == artists.length - 1 &&
searchArtist.hasNextPage) {
return Waypoint(
onEnter: () {
searchArtist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}
return Container(
margin:
const EdgeInsets.symmetric(
horizontal: 15),
child: ArtistCard(artist),
);
},
),
],
),
),
),
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
PlatformText(
"Albums",
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
if (searchAlbum.isLoading &&
!searchAlbum.isFetchingNextPage)
const PlatformCircularProgressIndicator()
else if (searchAlbum.hasError)
PlatformText(searchAlbum
.error?[searchAlbum.pageParams.last])
else
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: [
...albums.mapIndexed((i, album) {
if (i == albums.length - 1 &&
searchAlbum.hasNextPage) {
return Waypoint(
onEnter: () {
searchAlbum.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
}
return AlbumCard(
TypeConversionUtils
.simpleAlbum_X_Album(
album,
),
);
}),
],
),
),
),
),
],
),
),
), ),
), );
), },
); )
}, ],
) ),
],
),
),
); );
} }
} }

View File

@ -20,10 +20,10 @@ class AnonymousFallback extends ConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text("You're not logged in"), const PlatformText("You're not logged in"),
const SizedBox(height: 10), const SizedBox(height: 10),
PlatformFilledButton( PlatformFilledButton(
child: const Text("Login with Spotify"), child: const PlatformText("Login with Spotify"),
onPressed: () => ServiceUtils.navigate(context, "/settings"), onPressed: () => ServiceUtils.navigate(context, "/settings"),
) )
], ],

View File

@ -1,3 +1,4 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:platform_ui/platform_ui.dart'; import 'package:platform_ui/platform_ui.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -26,4 +27,9 @@ class PageWindowTitleBar extends PlatformAppBar {
], ],
title: center, title: center,
); );
@override
Widget build(BuildContext context) {
return MoveWindow(child: super.build(context));
}
} }