feat(lyrics): tabs for both synced and static lyrics #182

refactor: remove code-style warnings
This commit is contained in:
Kingkor Roy Tirtho 2022-10-24 17:59:58 +06:00
parent 0b79a1181c
commit 6b6907af3f
27 changed files with 311 additions and 622 deletions

View File

@ -1,4 +1,3 @@
import 'package:fl_query/fl_query.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -12,7 +11,6 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
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';
@ -56,16 +54,11 @@ class AlbumView extends HookConsumerWidget {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final SpotifyApi spotify = ref.watch(spotifyProvider); final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider);
final tracksSnapshot = useQuery( final tracksSnapshot = useQuery(
job: albumTracksQueryJob(album.id!), job: albumTracksQueryJob(album.id!),
externalData: spotify, externalData: spotify,
); );
final albumSavedSnapshot = useQuery(
job: albumIsSavedForCurrentUserQueryJob(album.id!),
externalData: spotify,
);
final albumArt = useMemoized( final albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString( () => TypeConversionUtils.image_X_UrlString(

View File

@ -1,197 +0,0 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Image, Player, Search;
import 'package:spotube/components/Category/CategoryCard.dart';
import 'package:spotube/components/Home/Sidebar.dart';
import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
import 'package:spotube/components/Search/Search.dart';
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/components/Shared/Waypoint.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useUpdateChecker.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Downloader.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';
final selectedIndexState = StateProvider((ref) => 0);
class Home extends HookConsumerWidget {
Home({Key? key}) : super(key: key);
final logger = getLogger(Home);
@override
Widget build(BuildContext context, ref) {
final double titleBarWidth = useBreakpointValue(
sm: 0.0,
md: 80.0,
lg: 256.0,
xl: 256.0,
xxl: 256.0,
);
final extended = ref.watch(sidebarExtendedStateProvider);
final selectedIndex = ref.watch(selectedIndexState);
onSelectedIndexChanged(int index) =>
ref.read(selectedIndexState.notifier).state = index;
final downloader = ref.watch(downloaderProvider);
final isMounted = useIsMounted();
useEffect(() {
downloader.onFileExists = (track) async {
if (!isMounted()) return false;
return await showDialog<bool>(
context: context,
builder: (context) => ReplaceDownloadedFileDialog(
track: track,
),
) ??
false;
};
return null;
}, [downloader]);
// checks for latest version of the application
useUpdateChecker(ref);
final titleBarContents = Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Row(
children: [
Expanded(
child: Row(
children: [
Container(
constraints: BoxConstraints(
maxWidth: extended == null
? titleBarWidth
: (extended ? 256 : 80),
),
color: Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(),
),
Expanded(child: MoveWindow()),
if (!kIsMacOS && !kIsMobile) const TitleBarActionButtons(),
],
),
)
],
),
);
final backgroundColor = Theme.of(context).backgroundColor;
useEffect(() {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: backgroundColor, // status bar color
statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179
? Brightness.dark
: Brightness.light,
),
);
return null;
}, [backgroundColor]);
return Scaffold(
bottomNavigationBar: SpotubeNavigationBar(
selectedIndex: selectedIndex,
onSelectedIndexChanged: onSelectedIndexChanged,
),
body: Column(
children: [
if (selectedIndex != 3)
kIsMobile
? titleBarContents
: WindowTitleBarBox(child: titleBarContents),
Expanded(
child: Row(
children: [
Sidebar(
selectedIndex: selectedIndex,
onSelectedIndexChanged: onSelectedIndexChanged,
),
// contents of the spotify
if (selectedIndex == 0)
Expanded(
child: Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
top: 8.0,
left: 8.0,
),
child: HookBuilder(builder: (context) {
final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch(
userPreferencesProvider
.select((s) => s.recommendationMarket),
);
final categoriesQuery = useInfiniteQuery(
job: categoriesQueryJob,
externalData: {
"spotify": spotify,
"recommendationMarket": recommendationMarket,
},
);
final categories = [
useMemoized(
() => Category()
..id = "user-featured-playlists"
..name = "Featured",
[],
),
...categoriesQuery.pages
.expand<Category?>(
(page) => page?.items ?? const Iterable.empty(),
)
.toList()
];
return ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
if (category == null) return Container();
if (index == categories.length - 1) {
return Waypoint(
onEnter: () {
if (categoriesQuery.hasNextPage) {
categoriesQuery.fetchNextPage();
}
},
child: const ShimmerCategories(),
);
}
return CategoryCard(category);
},
);
}),
),
),
if (selectedIndex == 1) const Search(),
if (selectedIndex == 2) const UserLibrary(),
if (selectedIndex == 3) const SyncedLyrics(),
],
),
),
// player itself
Player(),
],
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -193,7 +192,7 @@ class UserLocalTracks extends HookConsumerWidget {
SortTracksDropdown( SortTracksDropdown(
value: sortBy.value, value: sortBy.value,
onChanged: (value) { onChanged: (value) {
if (value != null) sortBy.value = value; sortBy.value = value;
}, },
), ),
const SizedBox(width: 10), const SizedBox(width: 10),

View File

@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/utils/service_utils.dart';
class TokenLogin extends HookConsumerWidget { class TokenLogin extends HookConsumerWidget {
const TokenLogin({Key? key}) : super(key: key); const TokenLogin({Key? key}) : super(key: key);

View File

@ -0,0 +1,114 @@
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:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class GeniusLyrics extends HookConsumerWidget {
final PaletteColor palette;
const GeniusLyrics({
required this.palette,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
final geniusLyricsQuery = useQuery(
job: geniusLyricsQueryJob,
externalData: Tuple2(
playback.track,
ref.watch(userPreferencesProvider).geniusAccessToken,
),
);
final breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme;
useEffect(() {
if (playback.track != null) {
geniusLyricsQuery.setExternalData(Tuple2(
playback.track,
ref.read(userPreferencesProvider).geniusAccessToken,
));
geniusLyricsQuery.refetch();
}
return null;
}, [playback.track]);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
playback.track?.name ?? "",
style: breakpoint >= Breakpoints.md
? textTheme.headline3
: textTheme.headline4?.copyWith(
fontSize: 25,
color: palette.titleTextColor,
),
),
),
Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
style: (breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6)
?.copyWith(color: palette.bodyTextColor),
),
),
Expanded(
child: SingleChildScrollView(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Builder(
builder: (context) {
if (geniusLyricsQuery.isLoading) {
return const ShimmerLyrics();
} else if (geniusLyricsQuery.hasError) {
return Text(
"Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}",
style: textTheme.bodyText1?.copyWith(
color: palette.bodyTextColor,
),
);
}
final lyrics = geniusLyricsQuery.data;
return Text(
lyrics == null && playback.track == null
? "No Track being played currently"
: lyrics ?? "",
style: textTheme.headline6?.copyWith(
color: palette.bodyTextColor,
),
);
},
),
),
),
),
),
const Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Powered by genius.com"),
),
)
],
);
}
}

View File

@ -1,93 +1,73 @@
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotube/components/Lyrics/GeniusLyrics.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
import 'package:spotube/hooks/useBreakpoints.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/provider/Playback.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class Lyrics extends HookConsumerWidget { class Lyrics extends HookConsumerWidget {
final Color? titleBarForegroundColor; const Lyrics({Key? key}) : super(key: key);
const Lyrics({
required this.titleBarForegroundColor,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final geniusLyricsQuery = useQuery( String albumArt = useMemoized(
job: geniusLyricsQueryJob, () => TypeConversionUtils.image_X_UrlString(
externalData: Tuple2( playback.track?.album?.images,
playback.track, index: (playback.track?.album?.images?.length ?? 1) - 1,
ref.watch(userPreferencesProvider).geniusAccessToken, placeholder: ImagePlaceholder.albumArt,
), ),
[playback.track?.album?.images],
); );
final breakpoint = useBreakpoints(); final palette = usePaletteColor(albumArt, ref);
final textTheme = Theme.of(context).textTheme;
return Column( useCustomStatusBarColor(
crossAxisAlignment: CrossAxisAlignment.stretch, palette.color,
children: [ true,
Center( noSetBGColor: true,
child: Text( );
playback.track?.name ?? "",
style: breakpoint >= Breakpoints.md return DefaultTabController(
? textTheme.headline3 length: 2,
: textTheme.headline4?.copyWith(fontSize: 25), child: Scaffold(
), extendBodyBehindAppBar: true,
appBar: const TabBar(
isScrollable: true,
tabs: [
Tab(text: "Synced Lyrics"),
Tab(text: "Lyrics (genius.com)"),
],
), ),
Center( body: Container(
child: Text( clipBehavior: Clip.hardEdge,
TypeConversionUtils.artists_X_String<Artist>( decoration: BoxDecoration(
playback.track?.artists ?? []), image: DecorationImage(
style: breakpoint >= Breakpoints.md image: UniversalImage.imageProvider(albumArt),
? textTheme.headline5 fit: BoxFit.cover,
: textTheme.headline6, ),
), ),
), child: BackdropFilter(
Expanded( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: SingleChildScrollView( child: Container(
child: Center( color: palette.color.withOpacity(.7),
child: Padding( child: SafeArea(
padding: const EdgeInsets.all(8.0), child: TabBarView(
child: Builder( children: [
builder: (context) { SyncedLyrics(palette: palette),
if (geniusLyricsQuery.isLoading) { GeniusLyrics(palette: palette),
return const ShimmerLyrics(); ],
} else if (geniusLyricsQuery.hasError) {
return Text(
"Sorry, no Lyrics were found for `${playback.track?.name}` :'(\n${geniusLyricsQuery.error.toString()}",
);
}
final lyrics = geniusLyricsQuery.data;
return Text(
lyrics == null && playback.track == null
? "No Track being played currently"
: lyrics ?? "",
style: textTheme.headline6
?.copyWith(color: textTheme.headline1?.color),
);
},
), ),
), ),
), ),
), ),
), ),
const Align( ),
alignment: Alignment.bottomRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("Powered by genius.com"),
),
)
],
); );
} }
} }

View File

@ -1,19 +1,14 @@
import 'dart:ui';
import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:fl_query_hooks/fl_query_hooks.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useAutoScrollController.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart'; import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
@ -27,7 +22,11 @@ final lyricDelayState = StateProvider<Duration>(
); );
class SyncedLyrics extends HookConsumerWidget { class SyncedLyrics extends HookConsumerWidget {
const SyncedLyrics({Key? key}) : super(key: key); final PaletteColor palette;
const SyncedLyrics({
required this.palette,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -40,7 +39,6 @@ class SyncedLyrics extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final failed = useState(false);
final lyricValue = timedLyricsQuery.data; final lyricValue = timedLyricsQuery.data;
final lyricsMap = useMemoized( final lyricsMap = useMemoized(
() => () =>
@ -61,197 +59,111 @@ class SyncedLyrics extends HookConsumerWidget {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(lyricDelayState.notifier).state = Duration.zero; ref.read(lyricDelayState.notifier).state = Duration.zero;
}); });
failed.value = false;
return null; return null;
}, [playback.track]); }, [playback.track]);
useEffect(() {
if (lyricValue != null && lyricValue.rating <= 2) {
Future.delayed(const Duration(seconds: 5), () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
failed.value = true;
Navigator.pop(context);
},
),
],
content: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Text(
"The found lyrics might not be properly synced. Do you want to default to static (genius.com) lyrics?",
),
SizedBox(height: 10),
Text(
"Hint: Wait for a moment to see if the lyric actually sync. Sometimes it may sync.",
),
],
),
);
},
);
});
}
return null;
}, [lyricValue]);
// when synced lyrics not found, fallback to GeniusLyrics
String albumArt = useMemoized(
() => TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
),
[playback.track?.album?.images],
);
final palette = usePaletteColor(albumArt, ref);
final headlineTextStyle = (breakpoint >= Breakpoints.md final headlineTextStyle = (breakpoint >= Breakpoints.md
? textTheme.headline3 ? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25)) : textTheme.headline4?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor); ?.copyWith(color: palette.titleTextColor);
useCustomStatusBarColor( return Column(
palette.color, children: [
true, SizedBox(
noSetBGColor: true, height: breakpoint >= Breakpoints.md ? 50 : 30,
); child: Material(
type: MaterialType.transparency,
return Container( child: Stack(
clipBehavior: Clip.hardEdge, children: [
decoration: BoxDecoration( Center(
image: DecorationImage( child: SpotubeMarqueeText(
image: UniversalImage.imageProvider(albumArt), text: playback.track?.name ?? "Not Playing",
fit: BoxFit.cover, style: headlineTextStyle,
), isHovering: true,
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: palette.color.withOpacity(.7),
child: SafeArea(
child: failed.value
? Lyrics(titleBarForegroundColor: palette.bodyTextColor)
: Column(
children: [
SizedBox(
height: breakpoint >= Breakpoints.md ? 50 : 30,
child: Material(
type: MaterialType.transparency,
child: Stack(
children: [
Center(
child: SpotubeMarqueeText(
text: playback.track?.name ?? "Not Playing",
style: headlineTextStyle,
isHovering: true,
),
),
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
tooltip: "Lyrics Delay",
icon: const Icon(Icons.av_timer_rounded),
onPressed: () async {
final delay = await showDialog(
context: context,
builder: (context) =>
const LyricDelayAdjustDialog(),
);
if (delay != null) {
ref
.read(lyricDelayState.notifier)
.state = delay;
}
},
),
),
),
],
),
),
),
Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6,
),
),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
Expanded(
child: ListView.builder(
controller: controller,
itemCount: lyricValue.lyrics.length,
itemBuilder: (context, index) {
final lyricSlice = lyricValue.lyrics[index];
final isActive =
lyricSlice.time.inSeconds == currentTime;
if (isActive) {
controller.scrollToIndex(
index,
preferPosition: AutoScrollPosition.middle,
);
}
return AutoScrollTag(
key: ValueKey(index),
index: index,
controller: controller,
child: lyricSlice.text.isEmpty
? Container()
: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle(
duration: const Duration(
milliseconds: 250),
style: TextStyle(
color: isActive
? Colors.white
: palette.bodyTextColor,
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
fontSize: isActive ? 30 : 26,
),
child: Text(
lyricSlice.text,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
),
);
},
),
),
if (playback.track != null &&
(lyricValue == null ||
lyricValue.lyrics.isEmpty == true))
const Expanded(child: ShimmerLyrics()),
],
), ),
),
Positioned.fill(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
tooltip: "Lyrics Delay",
icon: const Icon(Icons.av_timer_rounded),
onPressed: () async {
final delay = await showDialog(
context: context,
builder: (context) => const LyricDelayAdjustDialog(),
);
if (delay != null) {
ref.read(lyricDelayState.notifier).state = delay;
}
},
),
),
),
],
),
), ),
), ),
), Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6,
),
),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
Expanded(
child: ListView.builder(
controller: controller,
itemCount: lyricValue.lyrics.length,
itemBuilder: (context, index) {
final lyricSlice = lyricValue.lyrics[index];
final isActive = lyricSlice.time.inSeconds == currentTime;
if (isActive) {
controller.scrollToIndex(
index,
preferPosition: AutoScrollPosition.middle,
);
}
return AutoScrollTag(
key: ValueKey(index),
index: index,
controller: controller,
child: lyricSlice.text.isEmpty
? Container()
: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
color: isActive
? Colors.white
: palette.bodyTextColor,
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
fontSize: isActive ? 30 : 26,
),
child: Text(
lyricSlice.text,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
),
);
},
),
),
if (playback.track != null &&
(lyricValue == null || lyricValue.lyrics.isEmpty == true))
const Expanded(child: ShimmerLyrics()),
],
); );
} }
} }

View File

@ -13,14 +13,6 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
return TextButton( return TextButton(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.add_box_rounded, size: 50),
Text("Create Playlist", style: TextStyle(fontSize: 22)),
],
),
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
@ -106,6 +98,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
padding: MaterialStateProperty.all( padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 15, vertical: 100)), const EdgeInsets.symmetric(horizontal: 15, vertical: 100)),
), ),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.add_box_rounded, size: 50),
Text("Create Playlist", style: TextStyle(fontSize: 22)),
],
),
); );
} }
} }

View File

@ -30,7 +30,7 @@ class About extends HookWidget {
); );
return ListTile( return ListTile(
leading: Icon(Icons.info_outline_rounded), leading: const Icon(Icons.info_outline_rounded),
title: const Text("About Spotube"), title: const Text("About Spotube"),
onTap: () { onTap: () {
showAboutDialog( showAboutDialog(

View File

@ -475,8 +475,8 @@ class Settings extends HookConsumerWidget {
icon: const Icon(Icons.favorite_outline_rounded), icon: const Icon(Icons.favorite_outline_rounded),
label: const Text("Please Sponsor/Donate"), label: const Text("Please Sponsor/Donate"),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Colors.red[100], backgroundColor: Colors.red[100],
onPrimary: Colors.pinkAccent, foregroundColor: Colors.pinkAccent,
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
), ),
onPressed: () { onPressed: () {

View File

@ -23,6 +23,9 @@ class AnchorButton<T> extends HookWidget {
var tap = useState(false); var tap = useState(false);
return GestureDetector( return GestureDetector(
onTapDown: (event) => tap.value = true,
onTapUp: (event) => tap.value = false,
onTap: onTap,
child: MouseRegion( child: MouseRegion(
cursor: MaterialStateMouseCursor.clickable, cursor: MaterialStateMouseCursor.clickable,
child: Text( child: Text(
@ -37,9 +40,6 @@ class AnchorButton<T> extends HookWidget {
onEnter: (event) => hover.value = true, onEnter: (event) => hover.value = true,
onExit: (event) => hover.value = false, onExit: (event) => hover.value = false,
), ),
onTapDown: (event) => tap.value = true,
onTapUp: (event) => tap.value = false,
onTap: onTap,
); );
} }
} }

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ColoredTabBar extends ColoredBox implements PreferredSizeWidget { class ColoredTabBar extends ColoredBox implements PreferredSizeWidget {
@override
// ignore: overridden_fields
final TabBar child; final TabBar child;
const ColoredTabBar({ const ColoredTabBar({

View File

@ -62,12 +62,12 @@ class DownloadConfirmationDialog extends StatelessWidget {
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
), ),
ElevatedButton( ElevatedButton(
child: const Text("Accept"),
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Colors.red, foregroundColor: Colors.white,
onPrimary: Colors.white, backgroundColor: Colors.red,
), ),
child: const Text("Accept"),
) )
], ],
); );

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/AnchorButton.dart'; import 'package:spotube/components/Shared/AnchorButton.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class Hyperlink extends StatelessWidget { class Hyperlink extends StatelessWidget {

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';

View File

@ -1,4 +1,3 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';

View File

@ -139,10 +139,10 @@ class Spotube extends StatefulHookConsumerWidget {
const Spotube({Key? key}) : super(key: key); const Spotube({Key? key}) : super(key: key);
@override @override
_SpotubeState createState() => _SpotubeState(); SpotubeState createState() => SpotubeState();
} }
class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver { class SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
final logger = getLogger(Spotube); final logger = getLogger(Spotube);
SharedPreferences? localStorage; SharedPreferences? localStorage;

View File

@ -8,7 +8,7 @@ import 'package:spotube/components/Home/Shell.dart';
import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/components/Login/LoginTutorial.dart'; import 'package:spotube/components/Login/LoginTutorial.dart';
import 'package:spotube/components/Login/TokenLogin.dart'; import 'package:spotube/components/Login/TokenLogin.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart';
import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Search/Search.dart';
@ -44,8 +44,7 @@ final router = GoRouter(
GoRoute( GoRoute(
path: "/lyrics", path: "/lyrics",
name: "Lyrics", name: "Lyrics",
pageBuilder: (context, state) => pageBuilder: (context, state) => const SpotubePage(child: Lyrics()),
const SpotubePage(child: SyncedLyrics()),
), ),
GoRoute( GoRoute(
path: "/settings", path: "/settings",

View File

@ -4,8 +4,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/models/GoRouteDeclarations.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -80,19 +80,18 @@ class HomeTabIntent extends Intent {
class HomeTabAction extends Action<HomeTabIntent> { class HomeTabAction extends Action<HomeTabIntent> {
@override @override
invoke(intent) { invoke(intent) {
final notifier = intent.ref.read(selectedIndexState.notifier);
switch (intent.tab) { switch (intent.tab) {
case HomeTabs.browse: case HomeTabs.browse:
notifier.state = 0; router.go("/");
break; break;
case HomeTabs.search: case HomeTabs.search:
notifier.state = 1; router.go("/search");
break; break;
case HomeTabs.library: case HomeTabs.library:
notifier.state = 2; router.go("/library");
break; break;
case HomeTabs.lyrics: case HomeTabs.lyrics:
notifier.state = 3; router.go("/lyrics");
break; break;
} }
return null; return null;

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:dbus/dbus.dart'; import 'package:dbus/dbus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';

View File

@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:spotube/models/LyricsModels.dart'; import 'package:spotube/models/LyricsModels.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';

View File

@ -260,7 +260,7 @@ class _MprisMediaPlayer2Player extends DBusObject {
/// Gets value of property org.mpris.MediaPlayer2.Player.Rate /// Gets value of property org.mpris.MediaPlayer2.Player.Rate
Future<DBusMethodResponse> getRate() async { Future<DBusMethodResponse> getRate() async {
return DBusMethodSuccessResponse([DBusDouble(1)]); return DBusMethodSuccessResponse([const DBusDouble(1)]);
} }
/// Sets property org.mpris.MediaPlayer2.Player.Rate /// Sets property org.mpris.MediaPlayer2.Player.Rate
@ -442,9 +442,12 @@ class _MprisMediaPlayer2Player extends DBusObject {
} }
/// Emits signal org.mpris.MediaPlayer2.Player.Seeked /// Emits signal org.mpris.MediaPlayer2.Player.Seeked
Future<void> emitSeeked(int Position) async { Future<void> emitSeeked(int position) async {
await emitSignal( await emitSignal(
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]); 'org.mpris.MediaPlayer2.Player',
'Seeked',
[DBusInt64(position)],
);
} }
Future<void> updateProperties(Playback playback) async { Future<void> updateProperties(Playback playback) async {

View File

@ -6,7 +6,7 @@
Duration parseDuration(String input) { Duration parseDuration(String input) {
final parts = input.split(':'); final parts = input.split(':');
if (parts.length != 3) throw FormatException('Invalid time format'); if (parts.length != 3) throw const FormatException('Invalid time format');
int days; int days;
int hours; int hours;
@ -18,7 +18,7 @@ Duration parseDuration(String input) {
{ {
final p = parts[2].split('.'); final p = parts[2].split('.');
if (p.length != 2) throw FormatException('Invalid time format'); if (p.length != 2) throw const FormatException('Invalid time format');
final p2 = int.parse(p[1]); final p2 = int.parse(p[1]);
microseconds = p2 % 1000; microseconds = p2 % 1000;
@ -38,12 +38,13 @@ Duration parseDuration(String input) {
// TODO verify that there are no negative parts // TODO verify that there are no negative parts
return Duration( return Duration(
days: days, days: days,
hours: hours, hours: hours,
minutes: minutes, minutes: minutes,
seconds: seconds, seconds: seconds,
milliseconds: milliseconds, milliseconds: milliseconds,
microseconds: microseconds); microseconds: microseconds,
);
} }
Duration? tryParseDuration(String input) { Duration? tryParseDuration(String input) {

View File

@ -1,24 +1,19 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/widgets.dart' hide Element; import 'package:flutter/widgets.dart' hide Element;
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:spotube/models/LyricsModels.dart'; import 'package:spotube/models/LyricsModels.dart';
import 'package:spotube/models/SpotifySpotubeCredentials.dart'; import 'package:spotube/models/SpotifySpotubeCredentials.dart';
import 'package:spotube/models/SpotubeTrack.dart'; import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/models/generated_secrets.dart'; import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:html/parser.dart' as parser; import 'package:html/parser.dart' as parser;
import 'package:url_launcher/url_launcher.dart';
abstract class ServiceUtils { abstract class ServiceUtils {
static final logger = getLogger("ServiceUtils"); static final logger = getLogger("ServiceUtils");
@ -179,109 +174,6 @@ abstract class ServiceUtils {
} }
} }
@Deprecated("Use getAccessToken instead")
static Future<String?> connectIpc(String authUri, String redirectUri) async {
try {
logger.i("[connectIpc][Launching]: $authUri");
await launchUrl(
Uri.parse(authUri),
mode: LaunchMode.externalApplication,
);
HttpServer server = await HttpServer.bind(
InternetAddress.loopbackIPv4,
4304,
shared: true,
);
logger.i("[connectIpc] Server started");
await for (HttpRequest request in server) {
if (request.uri.path == "/auth/spotify/callback" &&
request.method == "GET") {
String? code = request.uri.queryParameters["code"];
if (code != null) {
request.response
..statusCode = HttpStatus.ok
..write("Authentication successful. Now Go back to Spotube")
..close();
return "$redirectUri?code=$code";
} else {
request.response
..statusCode = HttpStatus.forbidden
..write("Authorization failed start over!")
..close();
throw Exception("No code provided");
}
}
}
} catch (e, stack) {
logger.e("connectIpc", e, stack);
rethrow;
}
return null;
}
static const authRedirectUri = "http://localhost:4304/auth/spotify/callback";
/// Use [getAccessToken] instead
/// This method will be removed in the next major release
@Deprecated("Use getAccessToken instead")
static Future<void> oauthLogin(Auth auth,
{required String clientId, required String clientSecret}) async {
try {
String? accessToken;
String? refreshToken;
DateTime? expiration;
final credentials = SpotifyApiCredentials(clientId, clientSecret);
final grant = SpotifyApi.authorizationCodeGrant(credentials);
final authUri = grant.getAuthorizationUrl(
Uri.parse(authRedirectUri),
);
final responseUri = await connectIpc(authUri.toString(), authRedirectUri);
SharedPreferences localStorage = await SharedPreferences.getInstance();
if (responseUri != null) {
final SpotifyApi spotify =
SpotifyApi.fromAuthCodeGrant(grant, responseUri);
final credentials = await spotify.getCredentials();
if (credentials.accessToken != null) {
accessToken = credentials.accessToken;
await localStorage.setString(
LocalStorageKeys.accessToken, credentials.accessToken!);
}
if (credentials.refreshToken != null) {
refreshToken = credentials.refreshToken;
await localStorage.setString(
LocalStorageKeys.refreshToken, credentials.refreshToken!);
}
if (credentials.expiration != null) {
expiration = credentials.expiration;
await localStorage.setString(LocalStorageKeys.expiration,
credentials.expiration?.toString() ?? "");
}
}
await localStorage.setString(LocalStorageKeys.clientId, clientId);
await localStorage.setString(
LocalStorageKeys.clientSecret,
clientSecret,
);
// auth.setAuthState(
// clientId: clientId,
// clientSecret: clientSecret,
// accessToken: accessToken,
// refreshToken: refreshToken,
// expiration: expiration,
// );
} catch (e, stack) {
logger.e("oauthLogin", e, stack);
rethrow;
}
}
static const baseUri = "https://www.rentanadviser.com/subtitles"; static const baseUri = "https://www.rentanadviser.com/subtitles";
static Future<SubtitleSimple?> getTimedLyrics(SpotubeTrack track) async { static Future<SubtitleSimple?> getTimedLyrics(SpotubeTrack track) async {

View File

@ -1300,7 +1300,7 @@ packages:
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
uuid: uuid:
dependency: transitive dependency: "direct main"
description: description:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@ -61,6 +61,7 @@ dependencies:
fl_query_hooks: ^0.3.1 fl_query_hooks: ^0.3.1
flutter_inappwebview: ^5.4.3+7 flutter_inappwebview: ^5.4.3+7
tuple: ^2.0.1 tuple: ^2.0.1
uuid: ^3.0.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -13,7 +13,7 @@ import 'package:spotube/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(Spotube()); await tester.pumpWidget(const Spotube());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);