mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat(lyrics): use official spotify API for fetching lyrics and add zoom controls
This commit is contained in:
parent
bd48ca44ee
commit
10d0660972
@ -65,4 +65,6 @@ abstract class SpotubeIcons {
|
||||
static const minimize = FeatherIcons.chevronDown;
|
||||
static const personalized = FeatherIcons.star;
|
||||
static const genres = FeatherIcons.music;
|
||||
static const zoomIn = FeatherIcons.zoomIn;
|
||||
static const zoomOut = FeatherIcons.zoomOut;
|
||||
}
|
||||
|
54
lib/components/lyrics/zoom_controls.dart
Normal file
54
lib/components/lyrics/zoom_controls.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
class ZoomControls extends HookWidget {
|
||||
final int value;
|
||||
final ValueChanged<int> onChanged;
|
||||
final int min;
|
||||
final int max;
|
||||
|
||||
const ZoomControls({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.min = 50,
|
||||
this.max = 200,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: PlatformTheme.of(context)
|
||||
.secondaryBackgroundColor
|
||||
?.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 50),
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PlatformIconButton(
|
||||
icon: const Icon(SpotubeIcons.zoomOut),
|
||||
onPressed: () {
|
||||
if (value == min) return;
|
||||
onChanged(value - 10);
|
||||
},
|
||||
),
|
||||
PlatformText("$value%"),
|
||||
PlatformIconButton(
|
||||
icon: const Icon(SpotubeIcons.zoomIn),
|
||||
onPressed: () {
|
||||
if (value == max) return;
|
||||
onChanged(value + 10);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -9,6 +9,26 @@ class SubtitleSimple {
|
||||
required this.lyrics,
|
||||
required this.rating,
|
||||
});
|
||||
|
||||
factory SubtitleSimple.fromJson(Map<String, dynamic> json) {
|
||||
return SubtitleSimple(
|
||||
uri: Uri.parse(json["uri"] as String),
|
||||
name: json["name"] as String,
|
||||
lyrics: (json["lyrics"] as List<dynamic>)
|
||||
.map((e) => LyricSlice.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
rating: json["rating"] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"uri": uri.toString(),
|
||||
"name": name,
|
||||
"lyrics": lyrics.map((e) => e.toJson()).toList(),
|
||||
"rating": rating,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class LyricSlice {
|
||||
@ -17,6 +37,20 @@ class LyricSlice {
|
||||
|
||||
LyricSlice({required this.time, required this.text});
|
||||
|
||||
factory LyricSlice.fromJson(Map<String, dynamic> json) {
|
||||
return LyricSlice(
|
||||
time: Duration(milliseconds: json["time"]),
|
||||
text: json["text"] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"time": time.inMilliseconds,
|
||||
"text": text,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "LyricsSlice({time: $time, text: $text})";
|
||||
|
@ -1,102 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class GeniusLyrics extends HookConsumerWidget {
|
||||
final PaletteColor palette;
|
||||
final bool? isModal;
|
||||
const GeniusLyrics({
|
||||
required this.palette,
|
||||
this.isModal,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||
final geniusLyricsQuery = useQueries.lyrics.static(
|
||||
playlist?.activeTrack,
|
||||
ref.watch(userPreferencesProvider).geniusAccessToken,
|
||||
);
|
||||
final breakpoint = useBreakpoints();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isModal != true) ...[
|
||||
Center(
|
||||
child: Text(
|
||||
playlist?.activeTrack.name ?? "",
|
||||
style: breakpoint >= Breakpoints.md
|
||||
? textTheme.displaySmall
|
||||
: textTheme.headlineMedium?.copyWith(
|
||||
fontSize: 25,
|
||||
color: palette.titleTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
playlist?.activeTrack.artists ?? []),
|
||||
style: (breakpoint >= Breakpoints.md
|
||||
? textTheme.headlineSmall
|
||||
: textTheme.titleLarge)
|
||||
?.copyWith(color: palette.bodyTextColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (geniusLyricsQuery.isLoading ||
|
||||
geniusLyricsQuery.isRefreshing) {
|
||||
return const ShimmerLyrics();
|
||||
} else if (geniusLyricsQuery.hasError) {
|
||||
return Text(
|
||||
"Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${geniusLyricsQuery.error.toString()}",
|
||||
style: textTheme.bodyLarge?.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lyrics = geniusLyricsQuery.data;
|
||||
|
||||
return Text(
|
||||
lyrics == null && playlist?.activeTrack == null
|
||||
? "No Track being played currently"
|
||||
: lyrics ?? "",
|
||||
style:
|
||||
TextStyle(color: palette.bodyTextColor, fontSize: 18),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text("Powered by genius.com"),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/hooks/use_custom_status_bar_color.dart';
|
||||
import 'package:spotube/hooks/use_palette_color.dart';
|
||||
import 'package:spotube/pages/lyrics/genius_lyrics.dart';
|
||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
@ -41,7 +41,7 @@ class LyricsPage extends HookConsumerWidget {
|
||||
|
||||
Widget body = [
|
||||
SyncedLyrics(palette: palette, isModal: isModal),
|
||||
GeniusLyrics(palette: palette, isModal: isModal),
|
||||
PlainLyrics(palette: palette, isModal: isModal),
|
||||
][index.value];
|
||||
|
||||
final tabbar = PreferredSize(
|
||||
@ -58,7 +58,7 @@ class LyricsPage extends HookConsumerWidget {
|
||||
color: PlatformTextTheme.of(context).caption?.color,
|
||||
),
|
||||
PlatformTab(
|
||||
label: "Genius",
|
||||
label: "Plain",
|
||||
icon: const SizedBox.shrink(),
|
||||
color: PlatformTextTheme.of(context).caption?.color,
|
||||
),
|
||||
@ -71,7 +71,7 @@ class LyricsPage extends HookConsumerWidget {
|
||||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).backgroundColor.withOpacity(.4),
|
||||
color: Theme.of(context).colorScheme.background.withOpacity(.4),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(10),
|
||||
|
132
lib/pages/lyrics/plain_lyrics.dart
Normal file
132
lib/pages/lyrics/plain_lyrics.dart
Normal file
@ -0,0 +1,132 @@
|
||||
import 'package:collection/collection.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/lyrics/zoom_controls.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlainLyrics extends HookConsumerWidget {
|
||||
final PaletteColor palette;
|
||||
final bool? isModal;
|
||||
const PlainLyrics({
|
||||
required this.palette,
|
||||
this.isModal,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final playlist = ref.watch(PlaylistQueueNotifier.provider);
|
||||
final lyricsQuery = useQueries.lyrics.spotifySynced(
|
||||
ref,
|
||||
playlist?.activeTrack,
|
||||
);
|
||||
final breakpoint = useBreakpoints();
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
final textZoomLevel = useState<int>(100);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isModal != true) ...[
|
||||
Center(
|
||||
child: Text(
|
||||
playlist?.activeTrack.name ?? "",
|
||||
style: breakpoint >= Breakpoints.md
|
||||
? textTheme.displaySmall
|
||||
: textTheme.headlineMedium?.copyWith(
|
||||
fontSize: 25,
|
||||
color: palette.titleTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
playlist?.activeTrack.artists ?? []),
|
||||
style: (breakpoint >= Breakpoints.md
|
||||
? textTheme.headlineSmall
|
||||
: textTheme.titleLarge)
|
||||
?.copyWith(color: palette.bodyTextColor),
|
||||
),
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (lyricsQuery.isLoading || lyricsQuery.isRefreshing) {
|
||||
return const ShimmerLyrics();
|
||||
} else if (lyricsQuery.hasError) {
|
||||
return Text(
|
||||
"Sorry, no Lyrics were found for `${playlist?.activeTrack.name}` :'(\n${lyricsQuery.error.toString()}",
|
||||
style: textTheme.bodyLarge?.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lyrics =
|
||||
lyricsQuery.data?.lyrics.mapIndexed((i, e) {
|
||||
final next =
|
||||
lyricsQuery.data?.lyrics.elementAtOrNull(i + 1);
|
||||
if (next != null &&
|
||||
e.time - next.time >
|
||||
const Duration(milliseconds: 700)) {
|
||||
return "${e.text}\n";
|
||||
}
|
||||
|
||||
return e.text;
|
||||
}).join("\n");
|
||||
|
||||
return AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
color: palette.bodyTextColor,
|
||||
fontSize: 24 * textZoomLevel.value / 100,
|
||||
height: textZoomLevel.value < 70
|
||||
? 1.5
|
||||
: textZoomLevel.value > 150
|
||||
? 1.7
|
||||
: 2,
|
||||
),
|
||||
child: Text(
|
||||
lyrics == null && playlist?.activeTrack == null
|
||||
? "No Track being played currently"
|
||||
: lyrics ?? "",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ZoomControls(
|
||||
value: textZoomLevel.value,
|
||||
onChanged: (value) => textZoomLevel.value = value,
|
||||
min: 50,
|
||||
max: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -6,14 +5,13 @@ import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:platform_ui/platform_ui.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/lyrics/zoom_controls.dart';
|
||||
import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart';
|
||||
import 'package:spotube/components/shared/spotube_marquee_text.dart';
|
||||
import 'package:spotube/components/lyrics/lyric_delay_adjust_dialog.dart';
|
||||
import 'package:spotube/hooks/use_auto_scroll_controller.dart';
|
||||
import 'package:spotube/hooks/use_breakpoints.dart';
|
||||
import 'package:spotube/hooks/use_synced_lyrics.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:spotube/provider/playlist_queue_provider.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
@ -44,7 +42,8 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
final breakpoint = useBreakpoints();
|
||||
final controller = useAutoScrollController();
|
||||
|
||||
final timedLyricsQuery = useQueries.lyrics.synced(playlist?.activeTrack);
|
||||
final timedLyricsQuery =
|
||||
useQueries.lyrics.spotifySynced(ref, playlist?.activeTrack);
|
||||
final lyricValue = timedLyricsQuery.data;
|
||||
final lyricsMap = useMemoized(
|
||||
() =>
|
||||
@ -56,6 +55,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
[lyricValue],
|
||||
);
|
||||
final currentTime = useSyncedLyrics(ref, lyricsMap, lyricDelay);
|
||||
final textZoomLevel = useState<int>(100);
|
||||
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@ -128,7 +128,8 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
fontWeight: isActive
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
fontSize: isActive ? 30 : 26,
|
||||
fontSize: (isActive ? 30 : 26) *
|
||||
(textZoomLevel.value / 100),
|
||||
),
|
||||
child: Text(
|
||||
lyricSlice.text,
|
||||
@ -169,6 +170,15 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ZoomControls(
|
||||
value: textZoomLevel.value,
|
||||
onChanged: (value) => textZoomLevel.value = value,
|
||||
min: 50,
|
||||
max: 200,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -1,10 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/map.dart';
|
||||
import 'package:spotube/hooks/use_spotify_query.dart';
|
||||
import 'package:spotube/models/lyrics.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class LyricsQueries {
|
||||
const LyricsQueries();
|
||||
@ -48,4 +54,61 @@ class LyricsQueries {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The Concept behind this method was shamelessly stolen from
|
||||
/// https://github.com/akashrchandran/spotify-lyrics-api
|
||||
///
|
||||
/// Thanks to [akashrchandran](https://github.com/akashrchandran) for the idea
|
||||
///
|
||||
/// Special thanks to [raptag](https://github.com/raptag) for discovering this
|
||||
/// jem
|
||||
|
||||
Query<SubtitleSimple, dynamic> spotifySynced(WidgetRef ref, Track? track) {
|
||||
return useSpotifyQuery<SubtitleSimple, dynamic>(
|
||||
"spotify-synced-lyrics/${track?.id}}",
|
||||
(spotify) async {
|
||||
if (track == null) {
|
||||
throw "No track currently";
|
||||
}
|
||||
final token = await spotify.getCredentials();
|
||||
final res = await http.get(
|
||||
Uri.parse(
|
||||
"https://spclient.wg.spotify.com/color-lyrics/v2/track/${track.id}?format=json&market=from_token",
|
||||
),
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36",
|
||||
"App-platform": "WebPlayer",
|
||||
"authorization": "Bearer ${token.accessToken}"
|
||||
});
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception("Unable to find lyrics");
|
||||
}
|
||||
final linesRaw = Map.castFrom<dynamic, dynamic, String, dynamic>(
|
||||
jsonDecode(res.body),
|
||||
)["lyrics"]?["lines"] as List?;
|
||||
|
||||
final lines = linesRaw?.map((line) {
|
||||
return LyricSlice(
|
||||
time: Duration(milliseconds: int.parse(line["startTimeMs"])),
|
||||
text: line["words"] as String,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
return SubtitleSimple(
|
||||
lyrics: lines,
|
||||
name: track.name!,
|
||||
uri: res.request!.url,
|
||||
rating: 100,
|
||||
);
|
||||
},
|
||||
jsonConfig: JsonConfig(
|
||||
fromJson: (json) => SubtitleSimple.fromJson(json.castKeyDeep<String>()),
|
||||
toJson: (data) => data.toJson(),
|
||||
),
|
||||
ref: ref,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -535,7 +535,7 @@ packages:
|
||||
description:
|
||||
path: "packages/fl_query"
|
||||
ref: new-architecture
|
||||
resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459"
|
||||
resolved-ref: "5332bd16d389e703b0eaf17ab79bd59382500d08"
|
||||
url: "https://github.com/KRTirtho/fl-query.git"
|
||||
source: git
|
||||
version: "0.3.1"
|
||||
@ -544,7 +544,7 @@ packages:
|
||||
description:
|
||||
path: "packages/fl_query_hooks"
|
||||
ref: new-architecture
|
||||
resolved-ref: "0c819d4e11572d592b5334280b8b4f2657f21459"
|
||||
resolved-ref: "5332bd16d389e703b0eaf17ab79bd59382500d08"
|
||||
url: "https://github.com/KRTirtho/fl-query.git"
|
||||
source: git
|
||||
version: "0.3.1"
|
||||
|
Loading…
Reference in New Issue
Block a user