import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.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'; 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 { //Lyric height, screen height, appbar height double _scrollTo = (height * _currentIndex!) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56; if (0 > _scrollTo) return; _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 Scaffold( appBar: FreezerAppBar('Lyrics'.i18n, systemUiOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.light, systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, systemNavigationBarDividerColor: Color( Theme.of(context).scaffoldBackgroundColor.value - 0x00111111), systemNavigationBarIconBrightness: Brightness.light, )), body: SafeArea( child: Column( children: [ Theme( data: settings.themeData!.copyWith( textButtonTheme: TextButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.all(Colors.white)))), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ if (_freeScroll && !_loading) 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: (Notification notification) { if (_freeScroll || notification is! ScrollStartNotification) return false; if (!_animatedScroll && !_loading) setState(() => _freeScroll = true); return false; }, child: ListView.builder( controller: _controller, padding: EdgeInsets.fromLTRB( 0, 0, 0, settings.lyricsVisualizer! && false ? 100 : 0), 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, // )), // ); // }), // ), ]), ), PlayerBar(shouldHandleClicks: false), ], ), ), ); } }