1
+ import 'package:audioplayers/audioplayers.dart' ;
1
2
import 'package:flutter/material.dart' ;
3
+ import 'package:flutter_lyric/lyrics_reader.dart' ;
4
+ import 'package:flutter_lyric/lyrics_reader_model.dart' ;
2
5
import 'package:resonate/models/chapter.dart' ;
3
6
4
7
class ChapterPlayScreen extends StatefulWidget {
@@ -15,13 +18,36 @@ class ChapterPlayScreen extends StatefulWidget {
15
18
16
19
class ChapterPlayScreenState extends State <ChapterPlayScreen > {
17
20
int currentPage = 0 ;
18
- double currentTime = 0 ;
21
+ int lyricProgress = 0 ;
22
+ double sliderProgress = 0 ;
23
+ bool isPlaying = false ;
24
+ AudioPlayer ? audioPlayer;
25
+ late Duration chapterDuration;
26
+ late LyricsReaderModel lyricModel;
27
+ UINetease lyricUI = UINetease ();
28
+
29
+ @override
30
+ void initState () {
31
+ super .initState ();
32
+ chapterDuration = getChapterDurationFromString (widget.chapter.playDuration);
33
+ lyricModel = LyricsModelBuilder .create ()
34
+ .bindLyricToMain (widget.chapter.lyrics)
35
+ .getModel ();
36
+ }
37
+
38
+ Duration getChapterDurationFromString (String songLength) {
39
+ List <String > parts = songLength.split (':' );
40
+ int minutes = int .parse (parts[0 ]);
41
+ int seconds = int .parse (parts[1 ]);
42
+ return Duration (minutes: minutes, seconds: seconds);
43
+ }
19
44
20
45
@override
21
46
Widget build (BuildContext context) {
22
47
return Scaffold (
23
48
body: SafeArea (
24
49
child: Column (
50
+ crossAxisAlignment: CrossAxisAlignment .start,
25
51
children: [
26
52
Container (
27
53
height: MediaQuery .of (context).size.height * 0.65 ,
@@ -60,34 +86,66 @@ class ChapterPlayScreenState extends State<ChapterPlayScreen> {
60
86
const SizedBox (height: 20 ),
61
87
Text (
62
88
widget.chapter.title,
63
- style: const TextStyle (
89
+ style: TextStyle (
64
90
fontSize: 26 ,
65
91
fontWeight: FontWeight .bold,
66
- color: Colors .black87,
92
+ color: widget.chapter.tintColor.computeLuminance () <
93
+ 0.5
94
+ ? Colors .white
95
+ : Colors .black87,
67
96
),
68
97
),
69
98
],
70
99
),
71
100
),
72
101
73
102
// Lyrics Screen
74
- Container (
75
- padding: const EdgeInsets .all (16.0 ),
76
- child: SingleChildScrollView (
103
+ LyricsReader (
104
+ padding: const EdgeInsets .symmetric (horizontal: 16 ),
105
+ model: lyricModel,
106
+ position: lyricProgress,
107
+ lyricUi: lyricUI,
108
+ playing: isPlaying,
109
+ size: Size (double .infinity,
110
+ MediaQuery .of (context).size.height * 65 ),
111
+ emptyBuilder: () => Center (
77
112
child: Text (
78
- widget.chapter.lyrics,
79
- style: const TextStyle (
80
- fontSize: 18 ,
81
- color: Colors .black87,
82
- height: 1.5 ,
83
- ),
113
+ "No lyrics" ,
114
+ style: UINetease ().getOtherMainTextStyle (),
84
115
),
85
116
),
86
- ),
117
+ selectLineBuilder: (progress, confirm) {
118
+ return Row (
119
+ children: [
120
+ IconButton (
121
+ onPressed: () {
122
+ confirm.call ();
123
+ setState (() {
124
+ audioPlayer
125
+ ? .seek (Duration (milliseconds: progress));
126
+ });
127
+ },
128
+ icon: Icon (Icons .play_arrow,
129
+ color:
130
+ Theme .of (context).colorScheme.primary)),
131
+ Expanded (
132
+ child: Container (
133
+ decoration: BoxDecoration (
134
+ color: Theme .of (context).colorScheme.primary),
135
+ height: 1 ,
136
+ width: double .infinity,
137
+ ),
138
+ ),
139
+ ],
140
+ );
141
+ },
142
+ )
87
143
],
88
144
),
89
145
),
90
-
146
+ const SizedBox (
147
+ height: 10 ,
148
+ ),
91
149
// Page Indicator
92
150
Row (
93
151
mainAxisAlignment: MainAxisAlignment .center,
@@ -100,12 +158,12 @@ class ChapterPlayScreenState extends State<ChapterPlayScreen> {
100
158
decoration: BoxDecoration (
101
159
shape: BoxShape .circle,
102
160
color: currentPage == index
103
- ? Colors .greenAccent
161
+ ? widget.chapter.tintColor
104
162
: Colors .grey.shade400,
105
163
boxShadow: currentPage == index
106
164
? [
107
165
BoxShadow (
108
- color: Colors .greenAccent .withOpacity (0.5 ),
166
+ color: widget.chapter.tintColor .withOpacity (0.5 ),
109
167
blurRadius: 4 ,
110
168
),
111
169
]
@@ -114,66 +172,156 @@ class ChapterPlayScreenState extends State<ChapterPlayScreen> {
114
172
),
115
173
),
116
174
),
117
-
175
+ const SizedBox (height : 20 ),
118
176
// Play Controls and Progress Bar
119
- Padding (
120
- padding: const EdgeInsets .all (16.0 ),
121
- child: Column (
122
- children: [
123
- // Redesigned Progress Bar
124
- Slider (
125
- value: currentTime,
126
- onChanged: (value) {
127
- setState (() {
128
- currentTime = value;
129
- });
130
- },
131
- min: 0 ,
132
- max:
133
- double .parse (widget.chapter.playDuration.split (':' )[0 ]),
134
- activeColor: Colors .greenAccent,
135
- inactiveColor: Colors .grey.shade300,
136
- ),
137
- Row (
138
- mainAxisAlignment: MainAxisAlignment .spaceBetween,
177
+ Expanded (
178
+ child: SingleChildScrollView (
179
+ child: Padding (
180
+ padding: const EdgeInsets .symmetric (horizontal: 16.0 ),
181
+ child: Column (
182
+ crossAxisAlignment: CrossAxisAlignment .start,
139
183
children: [
140
- Text ("${currentTime .toStringAsFixed (0 )} min" ),
141
- Text ("${widget .chapter .playDuration } min" ),
142
- ],
143
- ),
144
- ],
145
- ),
146
- ),
184
+ Text (
185
+ widget.chapter.title,
186
+ style: Theme .of (context).textTheme.bodyMedium! .copyWith (
187
+ color: Theme .of (context).colorScheme.onSurface,
188
+ fontWeight: FontWeight .bold,
189
+ fontSize: 24 ,
190
+ fontStyle: FontStyle .normal,
191
+ fontFamily: 'Inter' ,
192
+ ),
193
+ ),
194
+ const SizedBox (
195
+ height: 5 ,
196
+ ),
197
+ // Redesigned Progress Bar
198
+ Slider (
199
+ value: sliderProgress,
200
+ onChanged: (value) {
201
+ setState (() {
202
+ sliderProgress = value;
203
+ });
204
+ },
205
+ onChangeEnd: (double value) {
206
+ setState (() {
207
+ lyricProgress = value.toInt ();
208
+ });
209
+ audioPlayer
210
+ ? .seek (Duration (milliseconds: value.toInt ()));
211
+ },
212
+ min: 0 ,
213
+ max: chapterDuration.inMilliseconds.toDouble (),
214
+ activeColor: widget.chapter.tintColor,
215
+ inactiveColor: Colors .grey.shade300,
216
+ ),
217
+ Stack (
218
+ alignment: Alignment .topCenter,
219
+ children: [
220
+ Row (
221
+ mainAxisAlignment: MainAxisAlignment .spaceBetween,
222
+ children: [
223
+ Text ("${formatDuration (sliderProgress )} min" ),
224
+ Text ("${widget .chapter .playDuration } min" ),
225
+ ],
226
+ ),
227
+ IconButton (
228
+ iconSize: 34 ,
229
+ style: IconButton .styleFrom (
230
+ backgroundColor:
231
+ Theme .of (context).colorScheme.primary),
232
+ onPressed: () {
233
+ if (isPlaying) {
234
+ audioPlayer? .pause ();
235
+ } else {
236
+ if (audioPlayer == null ) {
237
+ audioPlayer = AudioPlayer ()
238
+ ..play (UrlSource (
239
+ widget.chapter.audioFileUrl));
240
+ setState (() {
241
+ isPlaying = true ;
242
+ });
147
243
148
- // Title and About Button
149
- Padding (
150
- padding: const EdgeInsets .symmetric (horizontal: 16.0 ),
151
- child: Column (
152
- crossAxisAlignment: CrossAxisAlignment .start,
153
- mainAxisAlignment: MainAxisAlignment .start,
154
- children: [
155
- Text (
156
- widget.chapter.title,
157
- style: Theme .of (context).textTheme.bodyMedium! .copyWith (
158
- color: Theme .of (context).colorScheme.onSurface,
159
- fontWeight: FontWeight .bold,
160
- fontSize: 24 ,
161
- fontStyle: FontStyle .normal,
162
- fontFamily: 'Inter' ,
244
+ audioPlayer? .onPositionChanged
245
+ .listen ((Duration event) {
246
+ setState (() {
247
+ sliderProgress =
248
+ event.inMilliseconds.toDouble ();
249
+ lyricProgress = event.inMilliseconds;
250
+ });
251
+ });
252
+
253
+ audioPlayer? .onPlayerStateChanged
254
+ .listen ((PlayerState state) {
255
+ setState (() {
256
+ isPlaying =
257
+ state == PlayerState .playing;
258
+ });
259
+ });
260
+ } else {
261
+ audioPlayer? .resume ();
262
+ }
263
+ }
264
+ },
265
+ icon: Icon (
266
+ isPlaying ? Icons .pause : Icons .play_arrow)),
267
+ ],
268
+ ),
269
+
270
+ const SizedBox (height: 40 ),
271
+ Container (
272
+ padding: const EdgeInsets .all (10 ),
273
+ decoration: BoxDecoration (
274
+ color: const Color .fromARGB (106 , 40 , 39 , 39 ),
275
+ borderRadius: BorderRadius .circular (10 ),
163
276
),
164
- ),
165
- const SizedBox (height: 8 ),
166
- Text (
167
- widget.chapter.description,
168
- style: Theme .of (context).textTheme.bodyMedium! .copyWith (
169
- color: Theme .of (context).colorScheme.onSurface,
170
- fontWeight: FontWeight .w500,
171
- fontSize: 17 ,
172
- fontStyle: FontStyle .normal,
173
- fontFamily: 'Inter' ,
277
+ width: double .infinity,
278
+ child: Column (
279
+ crossAxisAlignment: CrossAxisAlignment .start,
280
+ children: [
281
+ Text (
282
+ "About" ,
283
+ style: Theme .of (context)
284
+ .textTheme
285
+ .bodyMedium!
286
+ .copyWith (
287
+ color:
288
+ Theme .of (context).colorScheme.onSurface,
289
+ fontWeight: FontWeight .w500,
290
+ fontSize: 17 ,
291
+ fontStyle: FontStyle .normal,
292
+ fontFamily: 'Inter' ,
293
+ ),
294
+ ),
295
+ const SizedBox (
296
+ height: 10 ,
297
+ ),
298
+ Padding (
299
+ padding:
300
+ const EdgeInsets .symmetric (horizontal: 5.0 ),
301
+ child: Text (
302
+ widget.chapter.description,
303
+ style: Theme .of (context)
304
+ .textTheme
305
+ .bodyMedium!
306
+ .copyWith (
307
+ color: Theme .of (context)
308
+ .colorScheme
309
+ .onSurface,
310
+ fontWeight: FontWeight .w300,
311
+ fontSize: 16 ,
312
+ fontStyle: FontStyle .normal,
313
+ fontFamily: 'Inter' ,
314
+ ),
315
+ ),
316
+ ),
317
+ const SizedBox (height: 5 )
318
+ ],
174
319
),
320
+ ),
321
+ const SizedBox (height: 20 )
322
+ ],
175
323
),
176
- ] ,
324
+ ) ,
177
325
),
178
326
),
179
327
],
@@ -182,3 +330,15 @@ class ChapterPlayScreenState extends State<ChapterPlayScreen> {
182
330
);
183
331
}
184
332
}
333
+
334
+ // Helper function to format Duration into minutes:seconds format
335
+ String formatDuration (double milliseconds) {
336
+ double totalSeconds = milliseconds / 1000 ; // Convert milliseconds to seconds
337
+ int minutes = (totalSeconds / 60 ).floor ();
338
+ int remainingSeconds = (totalSeconds % 60 ).floor ();
339
+
340
+ String twoDigits (int n) => n.toString ().padLeft (2 , "0" );
341
+ String secondsStr = twoDigits (remainingSeconds);
342
+
343
+ return "$minutes :$secondsStr " ;
344
+ }
0 commit comments