import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.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'; class LyricsScreen extends StatefulWidget { final Lyrics lyrics; final String trackId; LyricsScreen({this.lyrics, this.trackId, Key key}) : super(key: key); @override _LyricsScreenState createState() => _LyricsScreenState(); } class _LyricsScreenState extends State { Lyrics lyrics; bool _loading = true; bool _error = false; int _currentIndex = 0; int _prevIndex = 0; Timer _timer; ScrollController _controller = ScrollController(); StreamSubscription _mediaItemSub; final double height = 90; bool _freeScroll = false; Future _load() async { //Already available if (this.lyrics != null) return; if (widget.lyrics?.lyrics != null && widget.lyrics.lyrics.length > 0) { setState(() { lyrics = widget.lyrics; _loading = false; _error = false; }); return; } //Fetch try { Lyrics l = await deezerAPI.lyrics(widget.trackId); setState(() { _loading = false; lyrics = l; }); } catch (e) { setState(() { _error = true; }); } } void _scrollToLyric() { //Lyric height, screen height, appbar height double _scrollTo = (height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56; if (0 > _scrollTo) return; _controller.animateTo(_scrollTo, duration: Duration(milliseconds: 250), curve: Curves.ease); } @override void initState() { _load(); //Enable visualizer if (settings.lyricsVisualizer) playerHelper.startVisualizer(); Timer.periodic(Duration(milliseconds: 350), (timer) { _timer = timer; _currentIndex = lyrics?.lyrics?.lastIndexWhere( (l) => l.offset <= AudioService.playbackState.currentPosition); if (_loading) return; //Scroll to current lyric if (_currentIndex <= 0) return; if (_prevIndex == _currentIndex) return; //Update current lyric index setState(() => null); _prevIndex = _currentIndex; if (_freeScroll) return; _scrollToLyric(); }); //Track change = exit lyrics _mediaItemSub = AudioService.currentMediaItemStream.listen((event) { if (event.id != widget.trackId) Navigator.of(context).pop(); }); super.initState(); } @override void dispose() { if (_timer != null) _timer.cancel(); if (_mediaItemSub != null) _mediaItemSub.cancel(); //Stop visualizer if (settings.lyricsVisualizer) playerHelper.stopVisualizer(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Lyrics'.i18n, height: _freeScroll ? 100 : 56, bottom: _freeScroll ? PreferredSize( preferredSize: Size.fromHeight(46), child: Theme( data: settings.themeData.copyWith( textButtonTheme: TextButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.all( Colors.white)))), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () { setState(() => _freeScroll = false); _scrollToLyric(); }, child: Text( _currentIndex >= 0 ? lyrics.lyrics[_currentIndex].text : '...', textAlign: TextAlign.center, ), style: ButtonStyle( foregroundColor: MaterialStateProperty.all(Colors.white))) ], )), )) : null), body: Stack( children: [ //Visualizer if (settings.lyricsVisualizer) Align( alignment: Alignment.bottomCenter, 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, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate( data.length, (i) => AnimatedContainer( duration: Duration(milliseconds: 130), color: Theme.of(context).primaryColor, height: data[i] * 100, width: width, )), ); }), ), //Lyrics Padding( padding: EdgeInsets.fromLTRB( 0, 0, 0, settings.lyricsVisualizer ? 100 : 0), child: _error ? //Shouldn't really happen, empty lyrics have own text ErrorScreen() : // Loading _loading ? Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()], ), ) : NotificationListener( onNotification: (Notification notification) { if (notification is! ScrollEndNotification) return false; if (_freeScroll) return false; double _currentScroll = _controller.position.pixels; double _expectedScroll = (height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56; print( 'current: $_currentScroll, expected: $_expectedScroll'); if (_currentScroll != _expectedScroll) 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 ? () => AudioService.seekTo( 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), ), )))); }, )), ) ], )); } }