freezer/lib/ui/lyrics_screen.dart

275 lines
11 KiB
Dart
Raw Normal View History

2023-07-29 02:17:26 +00:00
import 'dart:async';
import 'package:async/async.dart';
2023-07-29 02:17:26 +00:00
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/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 {
const LyricsScreen({Key? key}) : super(key: key);
2023-07-29 02:17:26 +00:00
@override
State<LyricsScreen> createState() => _LyricsScreenState();
2023-07-29 02:17:26 +00:00
}
class _LyricsScreenState extends State<LyricsScreen> {
late StreamSubscription _mediaItemSub;
late StreamSubscription _playbackStateSub;
int? _currentIndex = -1;
int? _prevIndex = -1;
final ScrollController _controller = ScrollController();
2023-07-29 02:17:26 +00:00
final double height = 90;
Lyrics? _lyrics;
2023-07-29 02:17:26 +00:00
bool _loading = true;
CancelableOperation<Lyrics>? _lyricsCancelable;
2023-07-29 02:17:26 +00:00
Object? _error;
bool _freeScroll = false;
bool _animatedScroll = false;
bool _syncedLyrics = false;
2023-07-29 02:17:26 +00:00
Future<void> _loadForId(String trackId) async {
// cancel current request, if applicable
await _lyricsCancelable?.cancel();
2023-07-29 02:17:26 +00:00
//Fetch
if (_loading == false && _lyrics != null) {
2023-07-29 02:17:26 +00:00
setState(() {
_freeScroll = false;
_loading = true;
_lyrics = null;
2023-07-29 02:17:26 +00:00
});
}
2023-07-29 02:17:26 +00:00
try {
_lyricsCancelable =
CancelableOperation.fromFuture(deezerAPI.lyrics(trackId));
final lyrics = await _lyricsCancelable!.valueOrCancellation(null);
if (lyrics == null) return;
_syncedLyrics = lyrics.sync;
if (!mounted) return;
2023-07-29 02:17:26 +00:00
setState(() {
_loading = false;
_lyrics = lyrics;
2023-07-29 02:17:26 +00:00
});
_scrollToLyric();
} catch (e) {
if (!mounted) return;
2023-07-29 02:17:26 +00:00
setState(() {
_error = e;
});
}
}
Future<void> _scrollToLyric() async {
if (!_controller.hasClients) return;
//Lyric height, screen height, appbar height
double scrollTo = (height * _currentIndex!) -
2023-07-29 02:17:26 +00:00
(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;
}
2023-07-29 02:17:26 +00:00
_animatedScroll = true;
await _controller.animateTo(scrollTo,
duration: const Duration(milliseconds: 250), curve: Curves.ease);
2023-07-29 02:17:26 +00:00
_animatedScroll = false;
}
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_playbackStateSub = AudioService.position.listen((position) {
if (_loading) return;
if (!_syncedLyrics) return;
2023-07-29 02:17:26 +00:00
_currentIndex =
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
2023-07-29 02:17:26 +00:00
//Scroll to current lyric
if (_currentIndex! < 0) return;
if (_prevIndex == _currentIndex) return;
//Update current lyric index
setState(() {});
2023-07-29 02:17:26 +00:00
_prevIndex = _currentIndex;
if (_freeScroll) return;
_scrollToLyric();
});
});
if (audioHandler.mediaItem.value != null) {
2023-07-29 02:17:26 +00:00
_loadForId(audioHandler.mediaItem.value!.id);
}
2023-07-29 02:17:26 +00:00
/// 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: AppBar(
title: Text('Lyrics'.i18n),
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
context,
enabled: settings.playerBackgroundOnLyrics),
backgroundColor: Colors.transparent,
),
child: Column(
children: [
if (_freeScroll && !_loading)
Center(
child: TextButton(
onPressed: () {
setState(() => _freeScroll = false);
_scrollToLyric();
},
style: ButtonStyle(
foregroundColor: MaterialStateProperty.all(Colors.white)),
2023-07-29 02:17:26 +00:00
child: Text(
_currentIndex! >= 0
? (_lyrics?.lyrics?[_currentIndex!].text ?? '...')
2023-07-29 02:17:26 +00:00
: '...',
textAlign: TextAlign.center,
)),
2023-07-29 02:17:26 +00:00
),
Expanded(
child: Stack(children: [
//Lyrics
_error != null
?
//Shouldn't really happen, empty lyrics have own text
ErrorScreen(message: _error.toString())
:
// Loading lyrics
_loading
? const Center(child: CircularProgressIndicator())
2023-07-29 02:17:26 +00:00
: NotificationListener<ScrollStartNotification>(
onNotification:
(ScrollStartNotification notification) {
if (!_syncedLyrics) return false;
2023-07-29 02:17:26 +00:00
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,
2023-07-29 02:17:26 +00:00
itemBuilder: (BuildContext context, int i) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0),
2023-07-29 02:17:26 +00:00
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: _currentIndex == i
? Colors.grey.withOpacity(0.25)
: Colors.transparent,
),
height: _syncedLyrics ? height : null,
2023-07-29 02:17:26 +00:00
child: InkWell(
borderRadius:
BorderRadius.circular(8.0),
onTap: _syncedLyrics &&
_lyrics!.id != null
2023-07-29 02:17:26 +00:00
? () => audioHandler.seek(
_lyrics!.lyrics![i].offset!)
2023-07-29 02:17:26 +00:00
: null,
child: Center(
child: Padding(
padding: _currentIndex == i
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 1.0),
child: Text(
_lyrics!.lyrics![i].text!,
textAlign: _syncedLyrics
? TextAlign.center
: TextAlign.start,
style: TextStyle(
fontSize: _syncedLyrics
? 26.0
: 20.0,
fontWeight:
(_currentIndex == i)
? FontWeight.bold
: FontWeight
.normal),
),
2023-07-29 02:17:26 +00:00
),
))));
},
)),
//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,
// )),
// );
// }),
// ),
]),
),
const Divider(height: 1.0, thickness: 1.0),
const PlayerBar(backgroundColor: Colors.transparent),
2023-07-29 02:17:26 +00:00
],
),
);
}
}