Skip to content

Commit 14a6a0b

Browse files
authored
feat: add reading lyrics from files (#1373)
1 parent 6000b11 commit 14a6a0b

File tree

5 files changed

+140
-75
lines changed

5 files changed

+140
-75
lines changed

lib/player/view/full_height_player_audio_body.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import '../../common/view/ui_constants.dart';
77
import '../../extensions/build_context_x.dart';
88
import '../../extensions/taget_platform_x.dart';
99
import '../../radio/view/radio_history_list.dart';
10+
import '../../settings/settings_model.dart';
1011
import 'full_height_player_image.dart';
1112
import 'full_height_player_top_controls.dart';
13+
import 'player_lyrics.dart';
1214
import 'player_main_controls.dart';
1315
import 'player_title_and_artist.dart';
1416
import 'player_track.dart';
@@ -33,17 +35,28 @@ class FullHeightPlayerAudioBody extends StatelessWidget with WatchItMixin {
3335
@override
3436
Widget build(BuildContext context) {
3537
final showQueue = watchPropertyValue((AppModel m) => m.showQueueOverlay);
38+
final showPlayerLyrics = watchPropertyValue(
39+
(SettingsModel m) => m.showPlayerLyrics,
40+
);
41+
3642
final playerWithSidePanel =
3743
playerPosition == PlayerPosition.fullWindow &&
3844
context.mediaQuerySize.width > 1000;
3945
final theme = context.theme;
40-
final queueOrHistory = audio?.audioType == AudioType.radio
41-
? const SizedBox(
42-
width: 400,
46+
final queueOrHistory = showPlayerLyrics && audio != null
47+
? PlayerLyrics(
48+
key: ValueKey(audio?.path),
49+
audio: audio!,
4350
height: 500,
44-
child: RadioHistoryList(simpleList: true),
51+
width: 400,
4552
)
46-
: QueueBody(selectedColor: theme.colorScheme.onSurface);
53+
: (audio?.audioType == AudioType.radio
54+
? const SizedBox(
55+
width: 400,
56+
height: 500,
57+
child: RadioHistoryList(simpleList: true),
58+
)
59+
: QueueBody(selectedColor: theme.colorScheme.onSurface));
4760
final column = Column(
4861
mainAxisAlignment: MainAxisAlignment.center,
4962
mainAxisSize: MainAxisSize.min,
Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import 'package:flutter/material.dart';
2-
import 'package:lrc/lrc.dart';
32
import 'package:watch_it/watch_it.dart';
43

5-
import '../../common/data/audio.dart';
64
import '../../common/view/ui_constants.dart';
75
import '../../extensions/build_context_x.dart';
86
import '../../extensions/taget_platform_x.dart';
97
import '../../local_audio/view/local_cover.dart';
10-
import '../../settings/settings_model.dart';
118
import '../player_model.dart';
129
import 'audio_visualizer.dart';
1310
import 'player_fall_back_image.dart';
@@ -36,9 +33,6 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
3633
final showAudioVisualizer = watchPropertyValue(
3734
(PlayerModel m) => m.showAudioVisualizer && this.showAudioVisualizer,
3835
);
39-
final showPlayerLyrics = watchPropertyValue(
40-
(SettingsModel m) => m.showPlayerLyrics,
41-
);
4236

4337
final size = context.isPortrait
4438
? fullHeightPlayerImageSize
@@ -80,10 +74,6 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
8074
return AudioVisualizer(height: height ?? 200);
8175
}
8276

83-
if (showPlayerLyrics && audio != null) {
84-
return PlayerLyrics(audio: audio, size: size);
85-
}
86-
8777
return SizedBox(
8878
height: theHeight,
8979
width: theWidth,
@@ -97,63 +87,3 @@ class FullHeightPlayerImage extends StatelessWidget with WatchItMixin {
9787
);
9888
}
9989
}
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/player_lyrics.dart

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import 'dart:io';
2+
3+
import 'package:collection/collection.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:lrc/lrc.dart';
6+
import 'package:path/path.dart';
7+
import 'package:scroll_to_index/scroll_to_index.dart';
8+
import 'package:watch_it/watch_it.dart';
9+
import 'package:path/path.dart' as p;
10+
11+
import '../../common/data/audio.dart';
12+
import '../../extensions/build_context_x.dart';
13+
import '../player_model.dart';
14+
15+
class PlayerLyrics extends StatefulWidget with WatchItStatefulWidgetMixin {
16+
const PlayerLyrics({
17+
super.key,
18+
required this.audio,
19+
required this.width,
20+
required this.height,
21+
});
22+
23+
final Audio audio;
24+
final double width;
25+
final double height;
26+
27+
@override
28+
State<PlayerLyrics> createState() => _PlayerLyricsState();
29+
}
30+
31+
class _PlayerLyricsState extends State<PlayerLyrics> {
32+
late AutoScrollController _controller;
33+
int? _selectedIndex;
34+
35+
List<LrcLine>? lrc;
36+
String? lyricsString;
37+
38+
@override
39+
void initState() {
40+
super.initState();
41+
_controller = AutoScrollController();
42+
43+
if (widget.audio.lyrics != null) {
44+
if (widget.audio.lyrics!.isValidLrc) {
45+
lrc = Lrc.parse(widget.audio.lyrics!).lyrics;
46+
} else {
47+
lyricsString = widget.audio.lyrics;
48+
}
49+
} else {
50+
if (widget.audio.path != null) {
51+
final base = basenameWithoutExtension(widget.audio.path!);
52+
final dir = File(widget.audio.path!).parent;
53+
final maybe = p.join(dir.path, base + '.lrc');
54+
final file = File(maybe);
55+
if (file.existsSync()) {
56+
final lrcString = file.readAsStringSync();
57+
if (lrcString.isValidLrc) {
58+
lrc = Lrc.parse(lrcString).lyrics;
59+
} else {
60+
lyricsString = lrcString;
61+
}
62+
}
63+
}
64+
}
65+
}
66+
67+
@override
68+
void dispose() {
69+
_controller.dispose();
70+
super.dispose();
71+
}
72+
73+
@override
74+
Widget build(BuildContext context) {
75+
if (lyricsString != null) {
76+
return Text(lyricsString!);
77+
}
78+
79+
if (lrc == null || lrc!.isEmpty) {
80+
return const Text('no lyrcis found');
81+
}
82+
83+
watchPropertyValue((PlayerModel m) {
84+
final maybe = lrc?.firstWhereOrNull(
85+
(e) => e.timestamp.inSeconds == m.position?.inSeconds,
86+
);
87+
if (maybe != null) {
88+
_selectedIndex = lrc?.indexOf(maybe);
89+
}
90+
91+
return m.position;
92+
});
93+
94+
final color = watchPropertyValue(
95+
(PlayerModel m) => m.color ?? context.colorScheme.primary,
96+
);
97+
98+
return SizedBox(
99+
height: widget.height,
100+
width: widget.width,
101+
child: ListView.builder(
102+
controller: _controller,
103+
itemCount: lrc!.length,
104+
itemBuilder: (context, index) => ListTile(
105+
key: ValueKey(index),
106+
selected: _selectedIndex == index,
107+
selectedColor: color,
108+
title: Text(lrc!.elementAt(index).lyrics),
109+
),
110+
),
111+
);
112+
}
113+
}

pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,14 @@ packages:
806806
url: "https://pub.dev"
807807
source: hosted
808808
version: "6.0.0"
809+
listen_it:
810+
dependency: "direct main"
811+
description:
812+
name: listen_it
813+
sha256: dca2c41f8b2b133352bc439f7cf7b8a8c61d5f4c6de60fe13dbd2f1002f8a972
814+
url: "https://pub.dev"
815+
source: hosted
816+
version: "5.0.0"
809817
listenbrainz_dart:
810818
dependency: "direct main"
811819
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies:
4242

4343
intl: ^0.20.2
4444
lastfm: ^0.0.6
45+
listen_it: ^5.0.0
4546
listenbrainz_dart: ^0.0.4
4647
local_notifier: ^0.1.6
4748
lrc: ^1.0.2

0 commit comments

Comments
 (0)