Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
281 lines
11 KiB
Dart
281 lines
11 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:async/async.dart';
|
|
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/audio_handler.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 StatelessWidget {
|
|
const LyricsScreen({super.key});
|
|
|
|
@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: const Column(
|
|
children: [
|
|
Expanded(child: LyricsWidget()),
|
|
Divider(height: 1.0, thickness: 1.0),
|
|
PlayerBar(backgroundColor: Colors.transparent),
|
|
],
|
|
));
|
|
}
|
|
}
|
|
|
|
class LyricsWidget extends StatefulWidget {
|
|
const LyricsWidget({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<LyricsWidget> createState() => _LyricsWidgetState();
|
|
}
|
|
|
|
class _LyricsWidgetState extends State<LyricsWidget> {
|
|
late StreamSubscription _mediaItemSub;
|
|
late StreamSubscription _playbackStateSub;
|
|
int? _currentIndex = -1;
|
|
int? _prevIndex = -1;
|
|
final ScrollController _controller = ScrollController();
|
|
final double height = 90;
|
|
BoxConstraints? _widgetConstraints;
|
|
Lyrics? _lyrics;
|
|
bool _loading = true;
|
|
CancelableOperation<Lyrics>? _lyricsCancelable;
|
|
Object? _error;
|
|
|
|
bool _freeScroll = false;
|
|
bool _animatedScroll = false;
|
|
bool _syncedLyrics = false;
|
|
|
|
Future<void> _loadForId(String trackId) async {
|
|
// cancel current request, if applicable
|
|
await _lyricsCancelable?.cancel();
|
|
|
|
//Fetch
|
|
if (_loading == false && _lyrics != null) {
|
|
setState(() {
|
|
_freeScroll = false;
|
|
_loading = true;
|
|
_lyrics = null;
|
|
});
|
|
}
|
|
|
|
try {
|
|
_lyricsCancelable =
|
|
CancelableOperation.fromFuture(deezerAPI.lyrics(trackId));
|
|
final lyrics = await _lyricsCancelable!.valueOrCancellation(null);
|
|
if (lyrics == null) return;
|
|
_syncedLyrics = lyrics.sync;
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_loading = false;
|
|
_lyrics = lyrics;
|
|
});
|
|
|
|
SchedulerBinding.instance.addPostFrameCallback(
|
|
(_) => _updatePosition(audioHandler.playbackState.value.position));
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _scrollToLyric() async {
|
|
if (!_controller.hasClients) return;
|
|
//Lyric height, screen height, appbar height
|
|
double scrollTo;
|
|
if (_widgetConstraints == null) {
|
|
scrollTo = (height * _currentIndex!) -
|
|
(MediaQuery.of(context).size.height / 4 + height / 2);
|
|
} else {
|
|
final widgetHeight = _widgetConstraints!.maxHeight;
|
|
final minScroll = height * _currentIndex!;
|
|
scrollTo = minScroll - widgetHeight / 2 + 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: const Duration(milliseconds: 250), curve: Curves.ease);
|
|
_animatedScroll = false;
|
|
}
|
|
|
|
void _updatePosition(Duration position) {
|
|
if (_loading) return;
|
|
if (!_syncedLyrics) 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(() {});
|
|
_prevIndex = _currentIndex;
|
|
if (_freeScroll) return;
|
|
_scrollToLyric();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
//Enable visualizer
|
|
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
|
_playbackStateSub = AudioService.position.listen(_updatePosition);
|
|
});
|
|
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();
|
|
}
|
|
|
|
ScrollBehavior get _scrollBehavior {
|
|
if (_freeScroll) {
|
|
return ScrollConfiguration.of(context);
|
|
}
|
|
|
|
return ScrollConfiguration.of(context).copyWith(scrollbars: false);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(children: [
|
|
if (_freeScroll && !_loading)
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: () {
|
|
setState(() => _freeScroll = false);
|
|
_scrollToLyric();
|
|
},
|
|
style: ButtonStyle(
|
|
foregroundColor: MaterialStateProperty.all(Colors.white)),
|
|
child: Text(
|
|
_currentIndex! >= 0
|
|
? (_lyrics?.lyrics?[_currentIndex!].text ?? '...')
|
|
: '...',
|
|
textAlign: TextAlign.center,
|
|
)),
|
|
),
|
|
Expanded(
|
|
child: _error != null
|
|
?
|
|
//Shouldn't really happen, empty lyrics have own text
|
|
ErrorScreen(message: _error.toString())
|
|
:
|
|
// Loading lyrics
|
|
_loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: LayoutBuilder(builder: (context, constraints) {
|
|
_widgetConstraints = constraints;
|
|
return NotificationListener<ScrollStartNotification>(
|
|
onNotification: (ScrollStartNotification notification) {
|
|
if (!_syncedLyrics) return false;
|
|
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: ScrollConfiguration(
|
|
behavior: _scrollBehavior,
|
|
child: ListView.builder(
|
|
controller: _controller,
|
|
itemCount: _lyrics!.lyrics!.length,
|
|
itemBuilder: (BuildContext context, int i) {
|
|
return Padding(
|
|
padding: const 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: _syncedLyrics ? height : null,
|
|
child: InkWell(
|
|
borderRadius:
|
|
BorderRadius.circular(8.0),
|
|
onTap: _syncedLyrics &&
|
|
_lyrics!.id != null
|
|
? () => audioHandler.seek(
|
|
_lyrics!.lyrics![i].offset!)
|
|
: 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),
|
|
),
|
|
),
|
|
))));
|
|
},
|
|
)));
|
|
}),
|
|
),
|
|
]);
|
|
}
|
|
}
|