249 lines
9.4 KiB
Dart
249 lines
9.4 KiB
Dart
|
import 'dart:async';
|
||
|
|
||
|
import 'package:audio_service/audio_service.dart';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:flutter/scheduler.dart';
|
||
|
import 'package:freezer/api/deezer.dart';
|
||
|
import 'package:freezer/api/definitions.dart';
|
||
|
import 'package:freezer/api/player.dart';
|
||
|
import 'package:freezer/settings.dart';
|
||
|
import 'package:freezer/ui/elements.dart';
|
||
|
import 'package:freezer/translations.i18n.dart';
|
||
|
import 'package:freezer/ui/error.dart';
|
||
|
import 'package:freezer/ui/player_bar.dart';
|
||
|
import 'package:freezer/ui/player_screen.dart';
|
||
|
|
||
|
class LyricsScreen extends StatefulWidget {
|
||
|
LyricsScreen({Key? key}) : super(key: key);
|
||
|
|
||
|
@override
|
||
|
_LyricsScreenState createState() => _LyricsScreenState();
|
||
|
}
|
||
|
|
||
|
class _LyricsScreenState extends State<LyricsScreen> {
|
||
|
late StreamSubscription _mediaItemSub;
|
||
|
late StreamSubscription _playbackStateSub;
|
||
|
int? _currentIndex = -1;
|
||
|
int? _prevIndex = -1;
|
||
|
ScrollController _controller = ScrollController();
|
||
|
final double height = 90;
|
||
|
Lyrics? lyrics;
|
||
|
bool _loading = true;
|
||
|
Object? _error;
|
||
|
|
||
|
bool _freeScroll = false;
|
||
|
bool _animatedScroll = false;
|
||
|
|
||
|
Future _loadForId(String trackId) async {
|
||
|
//Fetch
|
||
|
if (_loading == false && lyrics != null) {
|
||
|
setState(() {
|
||
|
_freeScroll = false;
|
||
|
_loading = true;
|
||
|
lyrics = null;
|
||
|
});
|
||
|
}
|
||
|
try {
|
||
|
Lyrics l = await deezerAPI.lyrics(trackId);
|
||
|
setState(() {
|
||
|
_loading = false;
|
||
|
lyrics = l;
|
||
|
});
|
||
|
_scrollToLyric();
|
||
|
} catch (e) {
|
||
|
setState(() {
|
||
|
_error = e;
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Future<void> _scrollToLyric() async {
|
||
|
if (!_controller.hasClients) return;
|
||
|
//Lyric height, screen height, appbar height
|
||
|
double _scrollTo = (height * _currentIndex!) -
|
||
|
(MediaQuery.of(context).size.height / 4 + height / 2);
|
||
|
|
||
|
print(
|
||
|
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
|
||
|
if (0 > _scrollTo) return;
|
||
|
if (_scrollTo > _controller.position.maxScrollExtent)
|
||
|
_scrollTo = _controller.position.maxScrollExtent;
|
||
|
_animatedScroll = true;
|
||
|
await _controller.animateTo(_scrollTo,
|
||
|
duration: Duration(milliseconds: 250), curve: Curves.ease);
|
||
|
_animatedScroll = false;
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void initState() {
|
||
|
SchedulerBinding.instance!.addPostFrameCallback((_) {
|
||
|
//Enable visualizer
|
||
|
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||
|
_playbackStateSub = AudioService.position.listen((position) {
|
||
|
if (_loading) return;
|
||
|
_currentIndex =
|
||
|
lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
||
|
//Scroll to current lyric
|
||
|
if (_currentIndex! < 0) return;
|
||
|
if (_prevIndex == _currentIndex) return;
|
||
|
//Update current lyric index
|
||
|
setState(() => null);
|
||
|
_prevIndex = _currentIndex;
|
||
|
if (_freeScroll) return;
|
||
|
_scrollToLyric();
|
||
|
});
|
||
|
});
|
||
|
if (audioHandler.mediaItem.value != null)
|
||
|
_loadForId(audioHandler.mediaItem.value!.id);
|
||
|
|
||
|
/// Track change = ~exit~ reload lyrics
|
||
|
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
|
||
|
if (mediaItem == null) return;
|
||
|
if (_controller.hasClients) _controller.jumpTo(0.0);
|
||
|
_loadForId(mediaItem.id);
|
||
|
});
|
||
|
|
||
|
super.initState();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void dispose() {
|
||
|
_mediaItemSub.cancel();
|
||
|
_playbackStateSub.cancel();
|
||
|
//Stop visualizer
|
||
|
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
|
||
|
super.dispose();
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
return PlayerScreenBackground(
|
||
|
enabled: settings.playerBackgroundOnLyrics,
|
||
|
appBar: FreezerAppBar(
|
||
|
'Lyrics'.i18n,
|
||
|
systemUiOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
|
||
|
context,
|
||
|
enabled: settings.playerBackgroundOnLyrics),
|
||
|
backgroundColor: Colors.transparent,
|
||
|
),
|
||
|
child: Column(
|
||
|
children: [
|
||
|
if (_freeScroll && !_loading)
|
||
|
Center(
|
||
|
child: TextButton(
|
||
|
onPressed: () {
|
||
|
setState(() => _freeScroll = false);
|
||
|
_scrollToLyric();
|
||
|
},
|
||
|
child: Text(
|
||
|
_currentIndex! >= 0
|
||
|
? (lyrics?.lyrics?[_currentIndex!].text ?? '...')
|
||
|
: '...',
|
||
|
textAlign: TextAlign.center,
|
||
|
),
|
||
|
style: ButtonStyle(
|
||
|
foregroundColor:
|
||
|
MaterialStateProperty.all(Colors.white))),
|
||
|
),
|
||
|
Expanded(
|
||
|
child: Stack(children: [
|
||
|
//Lyrics
|
||
|
_error != null
|
||
|
?
|
||
|
//Shouldn't really happen, empty lyrics have own text
|
||
|
ErrorScreen(message: _error.toString())
|
||
|
:
|
||
|
// Loading lyrics
|
||
|
_loading
|
||
|
? Center(child: CircularProgressIndicator())
|
||
|
: NotificationListener<ScrollStartNotification>(
|
||
|
onNotification:
|
||
|
(ScrollStartNotification notification) {
|
||
|
final extentDiff =
|
||
|
(notification.metrics.extentBefore -
|
||
|
notification.metrics.extentAfter)
|
||
|
.abs();
|
||
|
// avoid accidental clicks
|
||
|
const extentThreshold = 10.0;
|
||
|
if (extentDiff >= extentThreshold &&
|
||
|
!_animatedScroll &&
|
||
|
!_loading &&
|
||
|
!_freeScroll) {
|
||
|
setState(() => _freeScroll = true);
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
child: ListView.builder(
|
||
|
controller: _controller,
|
||
|
itemCount: lyrics!.lyrics!.length,
|
||
|
itemBuilder: (BuildContext context, int i) {
|
||
|
return Padding(
|
||
|
padding:
|
||
|
EdgeInsets.symmetric(horizontal: 8.0),
|
||
|
child: Container(
|
||
|
decoration: BoxDecoration(
|
||
|
borderRadius:
|
||
|
BorderRadius.circular(8.0),
|
||
|
color: _currentIndex == i
|
||
|
? Colors.grey.withOpacity(0.25)
|
||
|
: Colors.transparent,
|
||
|
),
|
||
|
height: height,
|
||
|
child: InkWell(
|
||
|
borderRadius:
|
||
|
BorderRadius.circular(8.0),
|
||
|
onTap: lyrics!.id != null
|
||
|
? () => audioHandler.seek(
|
||
|
lyrics!.lyrics![i].offset!)
|
||
|
: null,
|
||
|
child: Center(
|
||
|
child: Text(
|
||
|
lyrics!.lyrics![i].text!,
|
||
|
textAlign: TextAlign.center,
|
||
|
style: TextStyle(
|
||
|
fontSize: 26.0,
|
||
|
fontWeight:
|
||
|
(_currentIndex == i)
|
||
|
? FontWeight.bold
|
||
|
: FontWeight.normal),
|
||
|
),
|
||
|
))));
|
||
|
},
|
||
|
)),
|
||
|
|
||
|
//Visualizer
|
||
|
//if (settings.lyricsVisualizer)
|
||
|
// Positioned(
|
||
|
// bottom: 0,
|
||
|
// left: 0,
|
||
|
// right: 0,
|
||
|
// child: StreamBuilder(
|
||
|
// stream: playerHelper.visualizerStream,
|
||
|
// builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||
|
// List<double> data = snapshot.data ?? [];
|
||
|
// double width = MediaQuery.of(context).size.width /
|
||
|
// data.length; //- 0.25;
|
||
|
// return Row(
|
||
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
||
|
// children: List.generate(
|
||
|
// data.length,
|
||
|
// (i) => AnimatedContainer(
|
||
|
// duration: Duration(milliseconds: 130),
|
||
|
// color: settings.primaryColor,
|
||
|
// height: data[i] * 100,
|
||
|
// width: width,
|
||
|
// )),
|
||
|
// );
|
||
|
// }),
|
||
|
// ),
|
||
|
]),
|
||
|
),
|
||
|
Divider(height: 1.0, thickness: 1.0),
|
||
|
PlayerBar(
|
||
|
shouldHandleClicks: false, backgroundColor: Colors.transparent),
|
||
|
],
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
}
|