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)
bool useColorTrayIcon = false;
@HiveField(51, defaultValue: 1)
int lyricsStyle = 1;
static LazyBox<Settings>? __box;
static Future<LazyBox<Settings>> get _box async =>
__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/translations.i18n.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_screen.dart';
import 'package:mini_music_visualizer/mini_music_visualizer.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class LyricsScreen extends StatelessWidget {
const LyricsScreen({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreenBackground(
enabled: settings.playerBackgroundOnLyrics,
appBar: AppBar(
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
context,
enabled: settings.playerBackgroundOnLyrics),
forceMaterialTransparency: true,
),
child: const Column(
children: [
Expanded(child: LyricsWidget()),
Divider(height: 1.0, thickness: 1.0),
PlayerBar(backgroundColor: Colors.transparent),
],
));
return PlayerArtColorScheme(
child: PlayerScreenBackground(
enabled: settings.playerBackgroundOnLyrics,
appBar: AppBar(
systemOverlayStyle:
PlayerScreenBackground.getSystemUiOverlayStyle(context,
enabled: settings.playerBackgroundOnLyrics),
forceMaterialTransparency: true,
),
child: const Column(
children: [
Expanded(child: LyricsWidget()),
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 _currentOffset = Duration.zero;
String? _currentTrackId;
final ScrollController _controller = ScrollController();
static const double height = 110.0;
final _controller = AutoScrollController(
suggestedRowHeight: height,
axis: Axis.vertical,
);
static const double height = 82.0;
static const double additionalTranslationHeight = 40.0;
BoxConstraints? _widgetConstraints;
Lyrics? _lyrics;
bool _loading = true;
CancelToken? _lyricsCancelToken;
@ -71,9 +77,6 @@ class _LyricsWidgetState extends State<LyricsWidget>
bool _showTranslation = 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 {
if (_currentTrackId == trackId) return;
_currentTrackId = trackId;
@ -133,20 +136,12 @@ class _LyricsWidgetState extends State<LyricsWidget>
void _scrollToLyric() {
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;
_controller
.animateTo(scrollTo,
duration: const Duration(milliseconds: 250), curve: Curves.ease)
.scrollToIndex(_currentIndex + 1,
duration: const Duration(milliseconds: 250),
preferPosition: AutoScrollPosition.middle)
.then((_) => _animatedScroll = false);
}
@ -236,14 +231,71 @@ class _LyricsWidgetState extends State<LyricsWidget>
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
Widget build(BuildContext context) {
final textColor =
settings.playerBackgroundOnLyrics && settings.blurPlayerBackground
? Theme.of(context).brightness == Brightness.light
? Colors.black87
: Colors.white70
: Theme.of(context).colorScheme.onBackground;
final lyricsStyle = LyricsStyle.fromIndex(settings.lyricsStyle);
return Stack(
children: [
_error != null
@ -253,17 +305,17 @@ class _LyricsWidgetState extends State<LyricsWidget>
_loading
? const Center(child: CircularProgressIndicator())
: LayoutBuilder(builder: (context, constraints) {
_widgetConstraints = constraints;
return NotificationListener<ScrollStartNotification>(
onNotification: (notification) {
if (!_syncedLyrics) return false;
final extentDiff =
final extentDelta =
(notification.metrics.extentBefore -
notification.metrics.extentAfter)
.abs();
// avoid accidental clicks
const extentThreshold = 10.0;
if (extentDiff >= extentThreshold &&
const extentThreshold = 9000.0;
print('delta: $extentDelta');
if (extentDelta >= extentThreshold &&
!_animatedScroll &&
!_loading &&
!_freeScroll) {
@ -282,98 +334,14 @@ class _LyricsWidgetState extends State<LyricsWidget>
vertical: constraints.maxHeight / 2 -
height / 2),
controller: _controller,
itemExtent: !_syncedLyrics
? null
: height +
(_showTranslation
? additionalTranslationHeight
: 0.0),
itemCount: _lyrics!.lyrics!.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i-- == 0) {
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) &&
_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)),
],
),
)));
return AutoScrollTag(
key: ValueKey(i),
controller: _controller,
index: i,
child:
_buildLyricWidget(i, lyricsStyle));
},
))));
}),

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

View File

@ -1305,6 +1305,14 @@ packages:
url: "https://github.com/Pato05/Scrobblenaut.git"
source: git
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:
dependency: "direct main"
description:

View File

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