fix album art skipping song

new lyrics ui + improvements
This commit is contained in:
Pato05 2024-05-06 17:10:18 +02:00
parent aaacd150a5
commit 070ab2d6f2
No known key found for this signature in database
GPG key ID: F53CA394104BA0CB
9 changed files with 244 additions and 149 deletions

View file

@ -178,6 +178,9 @@ class Settings {
@HiveField(50, defaultValue: false) @HiveField(50, defaultValue: false)
bool useColorTrayIcon = false; bool useColorTrayIcon = false;
@HiveField(51, defaultValue: 1)
int lyricsStyle = 1;
static LazyBox<Settings>? __box; static LazyBox<Settings>? __box;
static Future<LazyBox<Settings>> get _box async => static Future<LazyBox<Settings>> get _box async =>
__box ??= await Hive.openLazyBox<Settings>('settings'); __box ??= await Hive.openLazyBox<Settings>('settings');

View file

@ -13,30 +13,34 @@ import 'package:freezer/api/player/player_helper.dart';
import 'package:freezer/settings.dart'; import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/lyrics_styles/classic.dart';
import 'package:freezer/ui/lyrics_styles/lyrics_style.dart';
import 'package:freezer/ui/player_bar.dart'; import 'package:freezer/ui/player_bar.dart';
import 'package:freezer/ui/player_screen.dart'; import 'package:freezer/ui/player_screen.dart';
import 'package:mini_music_visualizer/mini_music_visualizer.dart'; import 'package:mini_music_visualizer/mini_music_visualizer.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class LyricsScreen extends StatelessWidget { class LyricsScreen extends StatelessWidget {
const LyricsScreen({super.key}); const LyricsScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PlayerScreenBackground( return PlayerArtColorScheme(
enabled: settings.playerBackgroundOnLyrics, child: PlayerScreenBackground(
appBar: AppBar( enabled: settings.playerBackgroundOnLyrics,
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle( appBar: AppBar(
context, systemOverlayStyle:
enabled: settings.playerBackgroundOnLyrics), PlayerScreenBackground.getSystemUiOverlayStyle(context,
forceMaterialTransparency: true, enabled: settings.playerBackgroundOnLyrics),
), forceMaterialTransparency: true,
child: const Column( ),
children: [ child: const Column(
Expanded(child: LyricsWidget()), children: [
Divider(height: 1.0, thickness: 1.0), Expanded(child: LyricsWidget()),
PlayerBar(backgroundColor: Colors.transparent), Divider(height: 1.0, thickness: 1.0),
], PlayerBar(backgroundColor: Colors.transparent),
)); ],
)));
} }
} }
@ -55,10 +59,12 @@ class _LyricsWidgetState extends State<LyricsWidget>
Duration _nextOffset = Duration.zero; Duration _nextOffset = Duration.zero;
Duration _currentOffset = Duration.zero; Duration _currentOffset = Duration.zero;
String? _currentTrackId; String? _currentTrackId;
final ScrollController _controller = ScrollController(); final _controller = AutoScrollController(
static const double height = 110.0; suggestedRowHeight: height,
axis: Axis.vertical,
);
static const double height = 82.0;
static const double additionalTranslationHeight = 40.0; static const double additionalTranslationHeight = 40.0;
BoxConstraints? _widgetConstraints;
Lyrics? _lyrics; Lyrics? _lyrics;
bool _loading = true; bool _loading = true;
CancelToken? _lyricsCancelToken; CancelToken? _lyricsCancelToken;
@ -71,9 +77,6 @@ class _LyricsWidgetState extends State<LyricsWidget>
bool _showTranslation = false; bool _showTranslation = false;
bool _availableTranslation = false; bool _availableTranslation = false;
// each individual lyric widget's height, either computed or cached
final _lyricHeights = HashMap<int, double>();
Future<void> _loadForId(String trackId) async { Future<void> _loadForId(String trackId) async {
if (_currentTrackId == trackId) return; if (_currentTrackId == trackId) return;
_currentTrackId = trackId; _currentTrackId = trackId;
@ -133,20 +136,12 @@ class _LyricsWidgetState extends State<LyricsWidget>
void _scrollToLyric() { void _scrollToLyric() {
if (!_controller.hasClients) return; if (!_controller.hasClients) return;
//Lyric height, screen height, appbar height
final actualHeight =
height + (_showTranslation ? additionalTranslationHeight : 0.0);
// _currentIndex + 1 because there's also the initial one (though we need to sum half)
var scrollTo = actualHeight * (_currentIndex + 1);
if (scrollTo < 0.0) scrollTo = 0.0;
if (scrollTo > _controller.position.maxScrollExtent) {
scrollTo = _controller.position.maxScrollExtent;
}
_animatedScroll = true; _animatedScroll = true;
_controller _controller
.animateTo(scrollTo, .scrollToIndex(_currentIndex + 1,
duration: const Duration(milliseconds: 250), curve: Curves.ease) duration: const Duration(milliseconds: 250),
preferPosition: AutoScrollPosition.middle)
.then((_) => _animatedScroll = false); .then((_) => _animatedScroll = false);
} }
@ -236,14 +231,71 @@ class _LyricsWidgetState extends State<LyricsWidget>
return ScrollConfiguration.of(context).copyWith(scrollbars: false); return ScrollConfiguration.of(context).copyWith(scrollbars: false);
} }
Color get activeTextColor => Theme.of(context).colorScheme.onBackground;
Color get inactiveTextColor => activeTextColor.withOpacity(0.25);
Widget _buildLyricWidget(int i, LyricsStyle lyricsStyle) {
final isActive = _currentIndex == --i;
final textColor =
isActive || _freeScroll ? activeTextColor : inactiveTextColor;
if (i == -1) {
return SizedBox(
height: height,
child: Center(
child: SizedBox(
width: 8.0 * 3 + 6.0,
child: StreamBuilder<bool>(
initialData: playerHelper.playing.valueOrNull,
stream: playerHelper.playing,
builder: (context, snapshot) {
return MiniMusicVisualizer(
color: textColor,
width: 8.0,
height: 16.0,
animate: (snapshot.data ?? false) && isActive,
);
}),
)));
}
return DecoratedBox(
decoration: lyricsStyle.getBoxDecoration(isActive),
child: InkWell(
borderRadius: BorderRadius.circular(12.0),
onTap: _syncedLyrics && _lyrics!.id != null
? () => audioHandler.seek(_lyrics!.lyrics![i].offset!)
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 28.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(_lyrics!.lyrics![i].text!,
textAlign: !_syncedLyrics
? TextAlign.start
: lyricsStyle.getTextAlignment(),
style: lyricsStyle.getTextStyle(
isActive, textColor, _syncedLyrics ? 26.0 : 20.0)),
if (_showTranslation)
Text(_lyrics!.lyrics![i].translated!,
textAlign: TextAlign.start,
style: TextStyle(
color: Color.lerp(
Theme.of(context).colorScheme.onBackground,
Colors.black,
0.12),
fontSize: 20.0)),
],
),
)));
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textColor = final lyricsStyle = LyricsStyle.fromIndex(settings.lyricsStyle);
settings.playerBackgroundOnLyrics && settings.blurPlayerBackground
? Theme.of(context).brightness == Brightness.light
? Colors.black87
: Colors.white70
: Theme.of(context).colorScheme.onBackground;
return Stack( return Stack(
children: [ children: [
_error != null _error != null
@ -253,17 +305,17 @@ class _LyricsWidgetState extends State<LyricsWidget>
_loading _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: LayoutBuilder(builder: (context, constraints) { : LayoutBuilder(builder: (context, constraints) {
_widgetConstraints = constraints;
return NotificationListener<ScrollStartNotification>( return NotificationListener<ScrollStartNotification>(
onNotification: (notification) { onNotification: (notification) {
if (!_syncedLyrics) return false; if (!_syncedLyrics) return false;
final extentDiff = final extentDelta =
(notification.metrics.extentBefore - (notification.metrics.extentBefore -
notification.metrics.extentAfter) notification.metrics.extentAfter)
.abs(); .abs();
// avoid accidental clicks // avoid accidental clicks
const extentThreshold = 10.0; const extentThreshold = 9000.0;
if (extentDiff >= extentThreshold && print('delta: $extentDelta');
if (extentDelta >= extentThreshold &&
!_animatedScroll && !_animatedScroll &&
!_loading && !_loading &&
!_freeScroll) { !_freeScroll) {
@ -282,98 +334,14 @@ class _LyricsWidgetState extends State<LyricsWidget>
vertical: constraints.maxHeight / 2 - vertical: constraints.maxHeight / 2 -
height / 2), height / 2),
controller: _controller, controller: _controller,
itemExtent: !_syncedLyrics
? null
: height +
(_showTranslation
? additionalTranslationHeight
: 0.0),
itemCount: _lyrics!.lyrics!.length + 1, itemCount: _lyrics!.lyrics!.length + 1,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
if (i-- == 0) { return AutoScrollTag(
return SizedBox( key: ValueKey(i),
height: height, controller: _controller,
child: Center( index: i,
child: SizedBox( child:
width: 8.0 * 3 + 6.0, _buildLyricWidget(i, lyricsStyle));
child: StreamBuilder<bool>(
initialData: playerHelper
.playing.valueOrNull,
stream: playerHelper.playing,
builder: (context, snapshot) {
return MiniMusicVisualizer(
color: textColor,
width: 8.0,
height: 16.0,
animate: (snapshot.data ??
false) &&
_currentIndex == -1,
);
}),
)));
}
return DecoratedBox(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: _currentIndex == i
? Colors.grey.withOpacity(0.25)
: Colors.transparent,
),
child: InkWell(
borderRadius:
BorderRadius.circular(12.0),
onTap: _syncedLyrics &&
_lyrics!.id != null
? () => audioHandler.seek(
_lyrics!.lyrics![i].offset!)
: null,
child: Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 4.0,
//vertical: 24.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
_lyrics!.lyrics![i].text!,
textAlign: _syncedLyrics
? TextAlign.center
: TextAlign.start,
style: TextStyle(
color: textColor,
fontSize: _syncedLyrics
? 26.0
: 20.0,
fontWeight:
(_currentIndex == i)
? FontWeight
.bold
: FontWeight
.normal),
),
if (_showTranslation)
Text(
_lyrics!.lyrics![i]
.translated!,
textAlign:
TextAlign.center,
style: TextStyle(
color: Color.lerp(
Theme.of(
context)
.colorScheme
.onBackground,
Colors.black,
0.12),
fontSize: 20.0)),
],
),
)));
}, },
)))); ))));
}), }),

View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:freezer/ui/lyrics_styles/lyrics_style.dart';
class ClassicLyricsStyle implements LyricsStyle {
@override
TextAlign getTextAlignment() => TextAlign.center;
@override
TextStyle getTextStyle(bool isActive, Color textColor, double textSize) =>
TextStyle(
fontWeight: FontWeight.bold, color: textColor, fontSize: textSize);
@override
BoxDecoration getBoxDecoration(bool isActive) => BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: isActive ? Colors.grey.withOpacity(0.25) : Colors.transparent);
}

View file

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:freezer/ui/lyrics_styles/classic.dart';
import 'package:freezer/ui/lyrics_styles/modern.dart';
class LyricsStyle {
static const classic = 0;
static const modern = 1;
LyricsStyle._();
factory LyricsStyle.fromIndex(int i) {
return switch (i) {
classic => ClassicLyricsStyle(),
modern => ModernLyricsStyle(),
_ => LyricsStyle._(),
};
}
TextAlign getTextAlignment() =>
throw Exception('getTextAlignment() has not been implemented');
TextStyle getTextStyle(bool isActive, Color textColor, double textSize) =>
throw Exception('getTextStyle() has not been implemented');
BoxDecoration getBoxDecoration(bool isActive) =>
throw Exception('getBoxDecoration() has not been implemented');
}

View file

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'package:freezer/ui/lyrics_styles/lyrics_style.dart';
class ModernLyricsStyle implements LyricsStyle {
@override
TextAlign getTextAlignment() => TextAlign.start;
@override
TextStyle getTextStyle(bool isActive, Color textColor, double textSize) =>
TextStyle(
fontWeight: FontWeight.bold, color: textColor, fontSize: textSize);
@override
BoxDecoration getBoxDecoration(bool isActive) => const BoxDecoration();
}

View file

@ -197,7 +197,7 @@ class PlayerScreenBackground extends StatelessWidget {
if (provider.dominantColor != null) if (provider.dominantColor != null)
Color.lerp(provider.dominantColor, Color.lerp(provider.dominantColor,
isLightMode ? Colors.white : Colors.black, 0.5)!, isLightMode ? Colors.white : Colors.black, 0.5)!,
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).colorScheme.background,
], ],
stops: const [0.0, 0.6], stops: const [0.0, 0.6],
)), )),
@ -886,6 +886,8 @@ class BigAlbumArt extends StatefulWidget {
} }
class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver { class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
static final _logger = Logger('_BigAlbumArtState');
final _pageController = PageController( final _pageController = PageController(
initialPage: playerHelper.queueIndex, initialPage: playerHelper.queueIndex,
keepPage: false, keepPage: false,
@ -893,13 +895,13 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
); );
StreamSubscription? _currentItemSub; StreamSubscription? _currentItemSub;
/// is true on pointer down event /// Always true, except when [PageController.animateToPage] is called programmatically
/// used to distinguish between [PageController.animateToPage] and user gesture ///
/// Gets reset to true when [onPageChanged] is called again
bool _userScroll = true; bool _userScroll = true;
/// whether the user has already scrolled the [PageView], /// true when the [PageController.animateToPage] [Future] hasn't completed yet.
/// so to avoid calling [PageController.animateToPage] again. bool _isAnimationRunning = false;
bool _initiatedByUser = false;
void _listenForMediaItemChanges() { void _listenForMediaItemChanges() {
if (_currentItemSub != null) return; if (_currentItemSub != null) return;
@ -909,21 +911,32 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
} }
_currentItemSub = audioHandler.mediaItem.listen((event) async { _currentItemSub = audioHandler.mediaItem.listen((event) async {
if (_initiatedByUser) {
_initiatedByUser = false;
return;
}
if (!_pageController.hasClients) return; if (!_pageController.hasClients) return;
if (_pageController.page?.toInt() == playerHelper.queueIndex) return; if (_pageController.page?.toInt() == playerHelper.queueIndex) return;
print('animating controller to page');
if (((_pageController.page?.toInt() ?? 0) - playerHelper.queueIndex)
.abs() >
1) {
_userScroll = false;
_logger.fine('jumping to page (difference > 1)');
_pageController.jumpToPage(playerHelper.queueIndex);
return;
}
_logger.fine('animating controller to page');
_userScroll = false; _userScroll = false;
_isAnimationRunning = true;
await _pageController.animateToPage(playerHelper.queueIndex, await _pageController.animateToPage(playerHelper.queueIndex,
duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); duration: const Duration(milliseconds: 300), curve: Curves.ease);
_userScroll = true; _isAnimationRunning = false;
}); });
} }
void _cancelMediaItemSubscription() {
_currentItemSub?.cancel();
_currentItemSub = null;
}
@override @override
void initState() { void initState() {
_listenForMediaItemChanges(); _listenForMediaItemChanges();
@ -934,7 +947,7 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (state) {
case AppLifecycleState.paused: case AppLifecycleState.paused:
_currentItemSub?.cancel(); _cancelMediaItemSubscription();
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
_listenForMediaItemChanges(); _listenForMediaItemChanges();
default: default:
@ -971,6 +984,7 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
FadePageRoute( FadePageRoute(
blur: true,
barrierDismissible: true, barrierDismissible: true,
opaque: false, opaque: false,
builder: (context) { builder: (context) {
@ -994,9 +1008,13 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
return PageView.builder( return PageView.builder(
controller: _pageController, controller: _pageController,
onPageChanged: (int index) { onPageChanged: (int index) {
_logger.finest('onPageChanged()');
// ignore if not initiated by user. // ignore if not initiated by user.
if (!_userScroll) return; if (_isAnimationRunning || !_userScroll) {
Logger('BigAlbumArt') _userScroll = true;
return;
}
_logger
.fine('page changed, skipping to media item'); .fine('page changed, skipping to media item');
if (queue[index].id == if (queue[index].id ==
audioHandler.mediaItem.value?.id) { audioHandler.mediaItem.value?.id) {
@ -1020,6 +1038,16 @@ class _BigAlbumArtState extends State<BigAlbumArt> with WidgetsBindingObserver {
), ),
)); ));
}), }),
if (kDebugMode) ...[
TextButton(
onPressed: _cancelMediaItemSubscription,
child: const Text('Unsubscribe')),
Positioned(
right: 0,
child: TextButton(
onPressed: _listenForMediaItemChanges,
child: const Text('Subscribe'))),
],
if (widget.showLyricsButton) if (widget.showLyricsButton)
StreamBuilder<MediaItem?>( StreamBuilder<MediaItem?>(
initialData: audioHandler.mediaItem.valueOrNull, initialData: audioHandler.mediaItem.valueOrNull,
@ -1171,7 +1199,6 @@ class PlayerScreenTopRow extends StatelessWidget {
.copyWith(fontSize: textSize ?? 38.sp))); .copyWith(fontSize: textSize ?? 38.sp)));
}), }),
), ),
PlayerMenuButtonDesktop(size: size),
desktopMode desktopMode
? PlayerMenuButtonDesktop(size: size) ? PlayerMenuButtonDesktop(size: size)
: PlayerMenuButton(size: size) : PlayerMenuButton(size: size)

View file

@ -16,6 +16,7 @@ import 'package:freezer/api/player/systray.dart';
import 'package:freezer/icons.dart'; import 'package:freezer/icons.dart';
import 'package:freezer/ui/login_on_other_device.dart'; import 'package:freezer/ui/login_on_other_device.dart';
import 'package:freezer/ui/login_screen.dart'; import 'package:freezer/ui/login_screen.dart';
import 'package:freezer/ui/lyrics_styles/lyrics_style.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:scrobblenaut/scrobblenaut.dart';
@ -292,6 +293,34 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
settings.save(); settings.save();
} }
: null), : null),
ListTile(
title: Text('Lyrics style'.i18n),
subtitle: Text(
'Style of the transition between screens within the app'.i18n),
leading: const Icon(Icons.auto_awesome_motion),
onTap: () => showDialog<int>(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Select lyrics style'.i18n),
children: <Widget>[
SimpleDialogOption(
child: const Text('Modern (default)'),
onPressed: () =>
Navigator.pop(context, LyricsStyle.modern),
),
SimpleDialogOption(
child: const Text('Classic'),
onPressed: () =>
Navigator.pop(context, LyricsStyle.classic),
),
]);
}).then((value) {
if (value == null) return;
settings.lyricsStyle = value;
settings.save();
}),
),
ListTile( ListTile(
title: const Text('Screens style'), title: const Text('Screens style'),
subtitle: const Text( subtitle: const Text(

View file

@ -1305,6 +1305,14 @@ packages:
url: "https://github.com/Pato05/Scrobblenaut.git" url: "https://github.com/Pato05/Scrobblenaut.git"
source: git source: git
version: "3.0.0" version: "3.0.0"
scroll_to_index:
dependency: "direct main"
description:
name: scroll_to_index
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
url: "https://pub.dev"
source: hosted
version: "3.0.1"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -100,6 +100,7 @@ dependencies:
mini_music_visualizer: mini_music_visualizer:
git: https://github.com/Pato05/mini_music_visualizer.git git: https://github.com/Pato05/mini_music_visualizer.git
fading_edge_scrollview: ^3.0.0 fading_edge_scrollview: ^3.0.0
scroll_to_index: ^3.0.1
#deezcryptor: #deezcryptor:
#path: deezcryptor/ #path: deezcryptor/