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:flutter/material.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/type_conversion_utils.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
@ -56,16 +54,11 @@ class AlbumView extends HookConsumerWidget {
Playback playback = ref.watch(playbackProvider);
final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider);
final tracksSnapshot = useQuery(
job: albumTracksQueryJob(album.id!),
externalData: spotify,
);
final albumSavedSnapshot = useQuery(
job: albumIsSavedForCurrentUserQueryJob(album.id!),
externalData: spotify,
);
final albumArt = useMemoized(
() => 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 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@ -193,7 +192,7 @@ class UserLocalTracks extends HookConsumerWidget {
SortTracksDropdown(
value: sortBy.value,
onChanged: (value) {
if (value != null) sortBy.value = value;
sortBy.value = value;
},
),
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/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/utils/service_utils.dart';
class TokenLogin extends HookConsumerWidget {
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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/components/Lyrics/GeniusLyrics.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.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/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:tuple/tuple.dart';
class Lyrics extends HookConsumerWidget {
final Color? titleBarForegroundColor;
const Lyrics({
required this.titleBarForegroundColor,
Key? key,
}) : super(key: key);
const Lyrics({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,
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 breakpoint = useBreakpoints();
final textTheme = Theme.of(context).textTheme;
final palette = usePaletteColor(albumArt, ref);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
playback.track?.name ?? "",
style: breakpoint >= Breakpoints.md
? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25),
),
useCustomStatusBarColor(
palette.color,
true,
noSetBGColor: true,
);
return DefaultTabController(
length: 2,
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: const TabBar(
isScrollable: true,
tabs: [
Tab(text: "Synced Lyrics"),
Tab(text: "Lyrics (genius.com)"),
],
),
Center(
child: Text(
TypeConversionUtils.artists_X_String<Artist>(
playback.track?.artists ?? []),
style: breakpoint >= Breakpoints.md
? textTheme.headline5
: textTheme.headline6,
body: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
image: DecorationImage(
image: UniversalImage.imageProvider(albumArt),
fit: BoxFit.cover,
),
),
),
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()}",
);
}
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),
);
},
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),
],
),
),
),
),
),
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: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/components/Lyrics/LyricDelayAdjustDialog.dart';
import 'package:spotube/components/Lyrics/Lyrics.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/useBreakpoints.dart';
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/hooks/useSyncedLyrics.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
@ -27,7 +22,11 @@ final lyricDelayState = StateProvider<Duration>(
);
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
Widget build(BuildContext context, ref) {
@ -40,7 +39,6 @@ class SyncedLyrics extends HookConsumerWidget {
final breakpoint = useBreakpoints();
final controller = useAutoScrollController();
final failed = useState(false);
final lyricValue = timedLyricsQuery.data;
final lyricsMap = useMemoized(
() =>
@ -61,197 +59,111 @@ class SyncedLyrics extends HookConsumerWidget {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(lyricDelayState.notifier).state = Duration.zero;
});
failed.value = false;
return null;
}, [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
? textTheme.headline3
: textTheme.headline4?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
useCustomStatusBarColor(
palette.color,
true,
noSetBGColor: true,
);
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: 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()),
],
return 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()),
],
);
}
}

View File

@ -13,14 +13,6 @@ class PlaylistCreateDialog extends HookConsumerWidget {
final spotify = ref.watch(spotifyProvider);
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: () {
showDialog(
context: context,
@ -106,6 +98,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
padding: MaterialStateProperty.all(
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(
leading: Icon(Icons.info_outline_rounded),
leading: const Icon(Icons.info_outline_rounded),
title: const Text("About Spotube"),
onTap: () {
showAboutDialog(

View File

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

View File

@ -23,6 +23,9 @@ class AnchorButton<T> extends HookWidget {
var tap = useState(false);
return GestureDetector(
onTapDown: (event) => tap.value = true,
onTapUp: (event) => tap.value = false,
onTap: onTap,
child: MouseRegion(
cursor: MaterialStateMouseCursor.clickable,
child: Text(
@ -37,9 +40,6 @@ class AnchorButton<T> extends HookWidget {
onEnter: (event) => hover.value = true,
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';
class ColoredTabBar extends ColoredBox implements PreferredSizeWidget {
@override
// ignore: overridden_fields
final TabBar child;
const ColoredTabBar({

View File

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

View File

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

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.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_hooks/flutter_hooks.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);
@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);
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/Login/LoginTutorial.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/Playlist/PlaylistView.dart';
import 'package:spotube/components/Search/Search.dart';
@ -44,8 +44,7 @@ final router = GoRouter(
GoRoute(
path: "/lyrics",
name: "Lyrics",
pageBuilder: (context, state) =>
const SpotubePage(child: SyncedLyrics()),
pageBuilder: (context, state) => const SpotubePage(child: Lyrics()),
),
GoRoute(
path: "/settings",

View File

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

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:dbus/dbus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:spotube/models/LyricsModels.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
Future<DBusMethodResponse> getRate() async {
return DBusMethodSuccessResponse([DBusDouble(1)]);
return DBusMethodSuccessResponse([const DBusDouble(1)]);
}
/// Sets property org.mpris.MediaPlayer2.Player.Rate
@ -442,9 +442,12 @@ class _MprisMediaPlayer2Player extends DBusObject {
}
/// Emits signal org.mpris.MediaPlayer2.Player.Seeked
Future<void> emitSeeked(int Position) async {
Future<void> emitSeeked(int position) async {
await emitSignal(
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]);
'org.mpris.MediaPlayer2.Player',
'Seeked',
[DBusInt64(position)],
);
}
Future<void> updateProperties(Playback playback) async {

View File

@ -6,7 +6,7 @@
Duration parseDuration(String input) {
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 hours;
@ -18,7 +18,7 @@ Duration parseDuration(String input) {
{
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]);
microseconds = p2 % 1000;
@ -38,12 +38,13 @@ Duration parseDuration(String input) {
// TODO verify that there are no negative parts
return Duration(
days: days,
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
microseconds: microseconds);
days: days,
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
microseconds: microseconds,
);
}
Duration? tryParseDuration(String input) {

View File

@ -1,24 +1,19 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/widgets.dart' hide Element;
import 'package:go_router/go_router.dart';
import 'package:html/dom.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart';
import 'package:http/http.dart' as http;
import 'package:spotube/models/LyricsModels.dart';
import 'package:spotube/models/SpotifySpotubeCredentials.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart';
import 'package:html/parser.dart' as parser;
import 'package:url_launcher/url_launcher.dart';
abstract class 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 Future<SubtitleSimple?> getTimedLyrics(SpotubeTrack track) async {

View File

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

View File

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

View File

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