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 { 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 _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( 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 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), ], ), ); } }