mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat(lyrics): tabs for both synced and static lyrics #182
refactor: remove code-style warnings
This commit is contained in:
parent
0b79a1181c
commit
6b6907af3f
@ -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(
|
||||||
|
@ -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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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),
|
||||||
|
@ -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);
|
||||||
|
114
lib/components/Lyrics/GeniusLyrics.dart
Normal file
114
lib/components/Lyrics/GeniusLyrics.dart
Normal 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"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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: () {
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user