Skip to content

Commit 6000b11

Browse files
authored
feat: add initial lyrics support (#1372)
1 parent dd59b7f commit 6000b11

File tree

10 files changed

+128
-1
lines changed

10 files changed

+128
-1
lines changed

lib/common/data/audio.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class Audio {
8080
/// Optional art that can belong to a parent element.
8181
final String? albumArtUrl;
8282

83+
final String? lyrics;
84+
8385
const Audio({
8486
this.path,
8587
this.url,
@@ -102,6 +104,7 @@ class Audio {
102104
this.pictureData,
103105
this.fileSize,
104106
this.albumArtUrl,
107+
this.lyrics,
105108
});
106109

107110
Audio copyWith({
@@ -127,6 +130,7 @@ class Audio {
127130
Uint8List? pictureData,
128131
int? fileSize,
129132
String? albumArtUrl,
133+
String? lyrics,
130134
}) {
131135
return Audio(
132136
path: path ?? this.path,
@@ -150,6 +154,7 @@ class Audio {
150154
pictureData: pictureData ?? this.pictureData,
151155
fileSize: fileSize ?? this.fileSize,
152156
albumArtUrl: albumArtUrl ?? this.albumArtUrl,
157+
lyrics: lyrics ?? this.lyrics,
153158
);
154159
}
155160

@@ -219,6 +224,9 @@ class Audio {
219224
if (albumArtUrl != null) {
220225
result.addAll({'albumArtUrl': albumArtUrl});
221226
}
227+
if (lyrics != null) {
228+
result.addAll({'lyrics': lyrics});
229+
}
222230

223231
return result;
224232
}
@@ -250,6 +258,7 @@ class Audio {
250258
: null,
251259
fileSize: map['fileSize']?.toInt(),
252260
albumArtUrl: map['albumArtUrl'],
261+
lyrics: map['lyrics'],
253262
);
254263
}
255264

@@ -259,7 +268,7 @@ class Audio {
259268

260269
@override
261270
String toString() {
262-
return 'Audio(path: $path, url: $url, audioType: $audioType, imageUrl: $imageUrl, description: $description, website: $website, title: $title, durationMs: $durationMs, artist: $artist, album: $album, albumArtist: $albumArtist, trackNumber: $trackNumber, trackTotal: $trackTotal, discNumber: $discNumber, discTotal: $discTotal, year: $year, genre: $genre, pictureMimeType: $pictureMimeType, pictureData: $pictureData, fileSize: $fileSize, albumArtUrl: $albumArtUrl)';
271+
return 'Audio(path: $path, url: $url, audioType: $audioType, imageUrl: $imageUrl, description: $description, website: $website, title: $title, durationMs: $durationMs, artist: $artist, album: $album, albumArtist: $albumArtist, trackNumber: $trackNumber, trackTotal: $trackTotal, discNumber: $discNumber, discTotal: $discTotal, year: $year, genre: $genre, pictureMimeType: $pictureMimeType, pictureData: $pictureData, fileSize: $fileSize, albumArtUrl: $albumArtUrl, lyrics: $lyrics)';
263272
}
264273

265274
@override
@@ -348,6 +357,7 @@ class Audio {
348357
pictureMimeType: data.pictures.firstOrNull?.mimetype,
349358
trackNumber: data.trackNumber,
350359
year: data.year?.year,
360+
lyrics: data.lyrics,
351361
);
352362
}
353363

lib/common/view/icons.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ class Iconz {
1717
? CupertinoIcons.eye
1818
: Icons.remove_red_eye_outlined;
1919

20+
static IconData get showLyrics => yaru
21+
? YaruIcons.chat_bubble_filled
22+
: cupertino
23+
? CupertinoIcons.chat_bubble_text_fill
24+
: Icons.chat_bubble;
25+
26+
static IconData get hideLyrics => yaru
27+
? YaruIcons.chat_bubble
28+
: cupertino
29+
? CupertinoIcons.chat_bubble_text
30+
: Icons.chat_bubble_outline;
31+
2032
static IconData get hide => yaru
2133
? YaruIcons.hide
2234
: cupertino

lib/extensions/shared_preferences_x.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ extension SPKeys on SharedPreferences {
5454
static const podcastArtistSuffix = '_artist';
5555
static const podcastLastUpdatedSuffix = '_last_updated';
5656
static const hideCompletedEpisodes = 'hideCompletedEpisodes';
57+
static const showPlayerLyrics = 'showPlayerLyrics';
5758
}

lib/player/view/full_height_player_image.dart

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import 'package:flutter/material.dart';
2+
import 'package:lrc/lrc.dart';
23
import 'package:watch_it/watch_it.dart';
34

5+
import '../../common/data/audio.dart';
46
import '../../common/view/ui_constants.dart';
57
import '../../extensions/build_context_x.dart';
68
import '../../extensions/taget_platform_x.dart';
79
import '../../local_audio/view/local_cover.dart';
10+
import '../../settings/settings_model.dart';
811
import '../player_model.dart';
912
import 'audio_visualizer.dart';
1013
import 'player_fall_back_image.dart';
@@ -33,6 +36,9 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
3336
final showAudioVisualizer = watchPropertyValue(
3437
(PlayerModel m) => m.showAudioVisualizer && this.showAudioVisualizer,
3538
);
39+
final showPlayerLyrics = watchPropertyValue(
40+
(SettingsModel m) => m.showPlayerLyrics,
41+
);
3642

3743
final size = context.isPortrait
3844
? fullHeightPlayerImageSize
@@ -74,6 +80,10 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
7480
return AudioVisualizer(height: height ?? 200);
7581
}
7682

83+
if (showPlayerLyrics && audio != null) {
84+
return PlayerLyrics(audio: audio, size: size);
85+
}
86+
7787
return SizedBox(
7888
height: theHeight,
7989
width: theWidth,
@@ -87,3 +97,63 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
8797
);
8898
}
8999
}
100+
101+
class PlayerLyrics extends StatefulWidget with WatchItStatefulWidgetMixin {
102+
const PlayerLyrics({super.key, required this.audio, required this.size});
103+
104+
final Audio audio;
105+
final double size;
106+
107+
@override
108+
State<PlayerLyrics> createState() => _PlayerLyricsState();
109+
}
110+
111+
class _PlayerLyricsState extends State<PlayerLyrics> {
112+
List<LrcLine>? lrc;
113+
String? lyrcisString;
114+
115+
@override
116+
void initState() {
117+
super.initState();
118+
119+
if (widget.audio.lyrics != null) {
120+
if (widget.audio.lyrics!.isValidLrc) {
121+
lrc = Lrc.parse(widget.audio.lyrics!).lyrics;
122+
} else {
123+
lyrcisString = widget.audio.lyrics;
124+
}
125+
}
126+
}
127+
128+
@override
129+
Widget build(BuildContext context) {
130+
if (lyrcisString != null) {
131+
return Text(lyrcisString!);
132+
}
133+
134+
if (lrc == null || lrc!.isEmpty) {
135+
return const Text('no lyrcis found');
136+
}
137+
138+
final position = watchPropertyValue((PlayerModel m) => m.position);
139+
final color = watchPropertyValue(
140+
(PlayerModel m) => m.color ?? context.colorScheme.primary,
141+
);
142+
143+
return SizedBox(
144+
height: widget.size,
145+
width: widget.size,
146+
child: ListView.builder(
147+
itemCount: lrc!.length,
148+
itemBuilder: (context, index) {
149+
final line = lrc!.elementAt(index);
150+
return ListTile(
151+
selectedColor: color,
152+
selected: line.timestamp.inSeconds == position?.inSeconds,
153+
title: Text(line.lyrics),
154+
);
155+
},
156+
),
157+
);
158+
}
159+
}

lib/player/view/full_height_player_top_controls.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import '../../l10n/l10n.dart';
2020
import '../../local_audio/view/pin_album_button.dart';
2121
import '../../player/player_model.dart';
2222
import '../../search/search_model.dart';
23+
import '../../settings/settings_model.dart';
2324
import 'playback_rate_button.dart';
2425
import 'player_pause_timer_button.dart';
2526
import 'player_view.dart';
@@ -109,6 +110,7 @@ class FullHeightPlayerTopControls extends StatelessWidget with WatchItMixin {
109110
? Icon(Iconz.show, color: iconColor)
110111
: Icon(Iconz.hide, color: iconColor),
111112
),
113+
ShowPlayerLyricButton(),
112114
IconButton(
113115
tooltip: playerPosition == PlayerPosition.fullWindow
114116
? context.l10n.leaveFullWindow
@@ -126,3 +128,17 @@ class FullHeightPlayerTopControls extends StatelessWidget with WatchItMixin {
126128
);
127129
}
128130
}
131+
132+
class ShowPlayerLyricButton extends StatelessWidget with WatchItMixin {
133+
@override
134+
Widget build(BuildContext context) {
135+
final showPlayerLyrics = watchPropertyValue(
136+
(SettingsModel m) => m.showPlayerLyrics,
137+
);
138+
return IconButton(
139+
icon: Icon(!showPlayerLyrics ? Iconz.hideLyrics : Iconz.showLyrics),
140+
onPressed: () =>
141+
di<SettingsModel>().setShowPlayerLyrics(!showPlayerLyrics),
142+
);
143+
}
144+
}

lib/settings/settings_model.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ class SettingsModel extends SafeChangeNotifier {
153153
Future<void> setHideCompletedEpisodes(bool value) =>
154154
_service.setHideCompletedEpisodes(value);
155155

156+
bool get showPlayerLyrics => _service.showPlayerLyrics;
157+
Future<void> setShowPlayerLyrics(bool value) =>
158+
_service.setShowPlayerLyrics(value);
159+
156160
Future<void> wipeAllSettings() async => _service.wipeAllSettings();
157161

158162
@override

lib/settings/settings_service.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ class SettingsService {
216216
Future<void> setHideCompletedEpisodes(bool value) =>
217217
_preferences.setBool(SPKeys.hideCompletedEpisodes, value).then(notify);
218218

219+
bool get showPlayerLyrics =>
220+
_preferences.getBool(SPKeys.showPlayerLyrics) ?? false;
221+
Future<void> setShowPlayerLyrics(bool value) =>
222+
_preferences.setBool(SPKeys.showPlayerLyrics, value).then(notify);
223+
219224
CloseBtnAction get closeBtnActionIndex =>
220225
_preferences.getString(SPKeys.closeBtnAction) == null
221226
? CloseBtnAction.alwaysAsk

pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,14 @@ packages:
830830
url: "https://pub.dev"
831831
source: hosted
832832
version: "1.3.0"
833+
lrc:
834+
dependency: "direct main"
835+
description:
836+
name: lrc
837+
sha256: "5100362b5c8e97f4d3f03ff87efeb40e73a6dd780eca2cbde9312e0d44b8e5ba"
838+
url: "https://pub.dev"
839+
source: hosted
840+
version: "1.0.2"
833841
m3u_parser_nullsafe:
834842
dependency: "direct main"
835843
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies:
4444
lastfm: ^0.0.6
4545
listenbrainz_dart: ^0.0.4
4646
local_notifier: ^0.1.6
47+
lrc: ^1.0.2
4748
m3u_parser_nullsafe: ^1.0.3
4849
media_kit:
4950
git:

test/test.mp3

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)