improve player screen with blurred album art

ui improvements in lyrics screen
animated bars when track is playing
fix back button when player screen is open
instantly pop when track is changed in queue list
This commit is contained in:
Pato05 2024-04-29 16:23:22 +02:00
parent 8ea6bcd073
commit 4b5d0bd09c
No known key found for this signature in database
GPG Key ID: F53CA394104BA0CB
9 changed files with 287 additions and 292 deletions

View File

@ -1,51 +0,0 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AnimatedBars extends StatefulWidget {
final double size;
final Color? color;
const AnimatedBars({
super.key,
this.size = 24.0,
this.color,
});
@override
State<AnimatedBars> createState() => _AnimatedBarsState();
}
class _AnimatedBarsState extends State<AnimatedBars>
with TickerProviderStateMixin {
late final _controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000))
..repeat(reverse: true);
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final color = widget.color ?? Theme.of(context).colorScheme.onSurface;
const count = 3;
AnimatedIcons.search_ellipsis;
return SizedBox.square(
dimension: widget.size,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
count,
(index) => SizedBox(
width: widget.size / count,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(color: color),
),
)),
));
}
}

View File

@ -71,106 +71,110 @@ class FancyScaffoldState extends State<FancyScaffold>
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
end: 1.0,
).animate(dragController);
return Stack(
children: [
Positioned.fill(
child: Scaffold(
body: widget.navigationRail != null
? Row(children: [
widget.navigationRail!,
const VerticalDivider(
indent: 0.0,
endIndent: 0.0,
width: 2.0,
return ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, child) => PopScope(
canPop: state != AnimationStatus.dismissed,
onPopInvoked: state == AnimationStatus.dismissed
? null
: (_) => dragController.fling(velocity: -1.0),
child: child!),
child: Stack(
children: [
Positioned.fill(
child: Scaffold(
body: widget.navigationRail != null
? Row(children: [
widget.navigationRail!,
const VerticalDivider(
indent: 0.0,
endIndent: 0.0,
width: 2.0,
),
Expanded(child: widget.body)
])
: widget.body,
drawer: widget.drawer,
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: widget.bottomPanelHeight),
if (widget.bottomNavigationBar != null)
SizeTransition(
axisAlignment: -1.0,
sizeFactor:
Tween(begin: 1.0, end: 0.0).animate(sizeAnimation),
child: widget.bottomNavigationBar,
),
Expanded(child: widget.body)
])
: widget.body,
drawer: widget.drawer,
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: widget.bottomPanelHeight),
if (widget.bottomNavigationBar != null)
SizeTransition(
axisAlignment: -1.0,
sizeFactor:
Tween(begin: 1.0, end: 0.0).animate(sizeAnimation),
child: widget.bottomNavigationBar,
),
],
],
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: AnimatedBuilder(
animation: sizeAnimation,
builder: (context, child) {
final x = 1.0 - sizeAnimation.value;
return Padding(
padding: EdgeInsets.only(
bottom: (defaultBottomPadding /*+ 8.0*/) * x,
//right: 8.0 * x,
//left: 8.0 * x,
),
child: child,
);
},
child: ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, child) {
return GestureDetector(
onVerticalDragEnd: _onVerticalDragEnd,
onVerticalDragUpdate: _onVerticalDragUpdate,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: AnimatedBuilder(
animation: sizeAnimation,
builder: (context, child) {
final x = 1.0 - sizeAnimation.value;
return Padding(
padding: EdgeInsets.only(
bottom: (defaultBottomPadding /*+ 8.0*/) * x,
//right: 8.0 * x,
//left: 8.0 * x,
),
child: child,
);
},
child: SizeTransition(
sizeFactor: sizeAnimation,
axisAlignment: -1.0,
axis: Axis.vertical,
child: SizedBox(
height: screenHeight,
width: MediaQuery.of(context).size.width,
child: ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, _) => Stack(
children: [
if (state != AnimationStatus.dismissed)
PopScope(
canPop: false,
onPopInvoked: (_) =>
dragController.fling(velocity: -1.0),
child: Positioned.fill(
child: ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, child) {
return GestureDetector(
onVerticalDragEnd: _onVerticalDragEnd,
onVerticalDragUpdate: _onVerticalDragUpdate,
child: child,
);
},
child: SizeTransition(
sizeFactor: sizeAnimation,
axisAlignment: -1.0,
axis: Axis.vertical,
child: SizedBox(
height: screenHeight,
width: MediaQuery.of(context).size.width,
child: ValueListenableBuilder(
valueListenable: statusNotifier,
builder: (context, state, _) => Stack(
children: [
if (state != AnimationStatus.dismissed)
Positioned.fill(
key: const Key('player_screen'),
child: widget.expandedPanel,
),
),
if (state != AnimationStatus.completed)
Positioned(
top: 0,
right: 0,
left: 0,
key: const Key('player_bar'),
child: FadeTransition(
opacity: Tween(begin: 1.0, end: 0.0)
.animate(dragController),
child: SizedBox(
height: widget.bottomPanelHeight,
child: widget.bottomPanel),
if (state != AnimationStatus.completed)
Positioned(
top: 0,
right: 0,
left: 0,
key: const Key('player_bar'),
child: FadeTransition(
opacity: Tween(begin: 1.0, end: 0.0)
.animate(dragController),
child: SizedBox(
height: widget.bottomPanelHeight,
child: widget.bottomPanel),
),
),
),
],
],
),
),
),
)),
)),
),
),
),
),
],
],
),
);
}

View File

@ -248,14 +248,23 @@ class HomepageRowSection extends StatelessWidget {
final HomePageSection section;
const HomepageRowSection(this.section, {super.key});
Widget buildChild(BuildContext context, List<HomePageItem> items) {
return Row(
children: items
.map((item) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: HomePageItemWidget(item),
))
.toList(growable: false));
Widget buildChild(BuildContext context, List<HomePageItem> items,
{bool hasMore = false}) {
return Row(children: [
...items.map((item) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: HomePageItemWidget(item),
)),
if (hasMore)
TextButton(
onPressed: () => Navigator.of(context).pushRoute(
builder: (context) => HomePageScreen(
title: section.title!,
channel: DeezerChannel(target: section.pagePath),
),
),
child: Text('Show more'.i18n))
]);
}
List<List<T>> _sliceInNLists<T>(List<T> source, int n) {
@ -279,15 +288,37 @@ class HomepageRowSection extends StatelessWidget {
children: _sliceInNLists(section.items!, 3)
.map((e) => buildChild(context, e))
.toList(growable: false)),
_ => buildChild(context, section.items!)
_ =>
buildChild(context, section.items!, hasMore: section.hasMore ?? false)
};
return ListTile(
title: Text(
section.title ?? '',
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
title: InkWell(
onTap: section.hasMore == true
? () => Navigator.of(context).pushRoute(
builder: (context) => HomePageScreen(
title: section.title!,
channel: DeezerChannel(target: section.pagePath),
),
)
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
section.title ?? '',
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style:
const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
)),
if (section.hasMore == true) ...[
const SizedBox(width: 16.0),
const Icon(Icons.keyboard_arrow_right),
],
],
),
),
subtitle: Scrollbar(
thickness: MainScreen.of(context).isDesktop ? null : 1.0,
@ -296,17 +327,6 @@ class HomepageRowSection extends StatelessWidget {
child: child,
),
),
trailing: section.hasMore == true
? const Icon(Icons.keyboard_arrow_right)
: null,
onTap: section.hasMore == true
? () => Navigator.of(context).pushRoute(
builder: (context) => HomePageScreen(
title: section.title!,
channel: DeezerChannel(target: section.pagePath),
),
)
: null,
);
}
}

View File

@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:collection';
import 'package:audio_service/audio_service.dart';
import 'package:dio/dio.dart';
import 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:freezer/api/definitions.dart';
@ -12,6 +14,7 @@ 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';
import 'package:mini_music_visualizer/mini_music_visualizer.dart';
class LyricsScreen extends StatelessWidget {
const LyricsScreen({super.key});
@ -21,11 +24,10 @@ class LyricsScreen extends StatelessWidget {
return PlayerScreenBackground(
enabled: settings.playerBackgroundOnLyrics,
appBar: AppBar(
title: Text('Lyrics'.i18n),
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
context,
enabled: settings.playerBackgroundOnLyrics),
backgroundColor: Colors.transparent,
forceMaterialTransparency: true,
),
child: const Column(
children: [
@ -48,7 +50,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
with WidgetsBindingObserver {
StreamSubscription? _mediaItemSub;
StreamSubscription? _positionSub;
int? _currentIndex = -1;
int _currentIndex = -1;
Duration _nextOffset = Duration.zero;
Duration _currentOffset = Duration.zero;
String? _currentTrackId;
@ -68,6 +70,9 @@ 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;
@ -137,7 +142,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
} else {
final widgetHeight = _widgetConstraints!.maxHeight;
final minScroll = actualHeight * _currentIndex!;
scrollTo = minScroll - widgetHeight / 2 + height / 2;
scrollTo = minScroll + height / 2;
}
if (scrollTo < 0.0) scrollTo = 0.0;
@ -157,10 +162,10 @@ class _LyricsWidgetState extends State<LyricsWidget>
if (position < _nextOffset && position > _currentOffset) return;
_currentIndex =
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
if (_currentIndex! < 0) return;
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position) ?? -1;
if (_currentIndex < 0) return;
if (_currentIndex! < _lyrics!.lyrics!.length - 1) {
if (_currentIndex < _lyrics!.lyrics!.length - 1) {
// update nextOffset
_nextOffset = _lyrics!.lyrics![_currentIndex! + 1].offset!;
} else {
@ -208,7 +213,6 @@ class _LyricsWidgetState extends State<LyricsWidget>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('fuck? $state');
switch (state) {
case AppLifecycleState.paused:
_cancelSubscriptions();
@ -240,59 +244,49 @@ class _LyricsWidgetState extends State<LyricsWidget>
@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;
return Stack(
children: [
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,
_error != null
? ErrorScreen(message: _error.toString())
:
// Loading lyrics
_loading
? const Center(child: CircularProgressIndicator())
: LayoutBuilder(builder: (context, constraints) {
_widgetConstraints = constraints;
return NotificationListener<ScrollStartNotification>(
onNotification: (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: FadingEdgeScrollView.fromScrollView(
gradientFractionOnStart: 0.25,
gradientFractionOnEnd: 0.25,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 8.0),
padding: EdgeInsets.symmetric(
horizontal: 8.0,
vertical: constraints.maxHeight / 2 -
height / 2),
controller: _controller,
itemExtent: !_syncedLyrics
? null
@ -300,8 +294,22 @@ class _LyricsWidgetState extends State<LyricsWidget>
(_showTranslation
? additionalTranslationHeight
: 0.0),
itemCount: _lyrics!.lyrics!.length,
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: MiniMusicVisualizer(
color: textColor,
width: 8.0,
height: 16.0,
animate: _currentIndex == -1,
),
)));
}
return DecoratedBox(
decoration: BoxDecoration(
borderRadius:
@ -312,15 +320,22 @@ class _LyricsWidgetState extends State<LyricsWidget>
),
child: InkWell(
borderRadius:
BorderRadius.circular(8.0),
BorderRadius.circular(12.0),
onTap: _syncedLyrics &&
_lyrics!.id != null
? () => audioHandler.seek(
_lyrics!.lyrics![i].offset!)
: null,
child: Center(
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!,
@ -328,6 +343,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
? TextAlign.center
: TextAlign.start,
style: TextStyle(
color: textColor,
fontSize: _syncedLyrics
? 26.0
: 20.0,
@ -357,25 +373,35 @@ class _LyricsWidgetState extends State<LyricsWidget>
),
)));
},
)));
}),
),
]),
))));
}),
if (_availableTranslation)
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ElevatedButton(
onPressed: () {
setState(() => _showTranslation = !_showTranslation);
SchedulerBinding.instance
.addPostFrameCallback((_) => _scrollToLyric());
},
child: Text(_showTranslation
? 'Without translation'.i18n
: 'With translation'.i18n)),
)),
Positioned(
bottom: 16.0,
left: 0,
right: 0,
child: Center(
child: ElevatedButton(
onPressed: () {
setState(() => _showTranslation = !_showTranslation);
SchedulerBinding.instance
.addPostFrameCallback((_) => _scrollToLyric());
},
child: Text(_showTranslation
? 'Without translation'.i18n
: 'With translation'.i18n)),
),
),
if (_freeScroll)
Positioned(
bottom: 16.0,
right: 16.0,
child: FloatingActionButton(
child: const Icon(Icons.sync),
onPressed: () => setState(() {
_freeScroll = false;
_scrollToLyric();
})))
],
);
}

View File

@ -163,12 +163,15 @@ class PlayerScreenBackground extends StatelessWidget {
Widget _buildChild(
BuildContext context, BackgroundProvider provider, Widget child) {
final isLightMode = Theme.of(context).brightness == Brightness.light;
return Stack(children: [
if (provider.imageProvider != null || settings.colorGradientBackground)
Positioned.fill(
child: provider.imageProvider != null
? DecoratedBox(
decoration: const BoxDecoration(color: Colors.black),
decoration: BoxDecoration(
color: Color.lerp(provider.dominantColor,
isLightMode ? Colors.white : Colors.black, 0.75)),
child: ImageFiltered(
imageFilter: ImageFilter.blur(
tileMode: TileMode.decal,
@ -180,10 +183,7 @@ class PlayerScreenBackground extends StatelessWidget {
image: DecorationImage(
image: provider.imageProvider!,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.white
.withOpacity(settings.isDark ? 0.55 : 0.75),
BlendMode.dstATop),
opacity: 0.35,
)),
),
),

View File

@ -170,11 +170,10 @@ class _QueueListWidgetState extends State<QueueListWidget> {
trailing: ReorderableDragStartListener(
index: index, child: const Icon(Icons.drag_handle)),
onTap: () {
audioHandler.skipToQueueItem(index).then((value) {
if (widget.shouldPopOnTap) {
Navigator.of(context).pop();
}
});
if (widget.shouldPopOnTap) {
Navigator.pop(context);
}
audioHandler.skipToQueueItem(index);
},
onSecondary: (details) => menuSheet.defaultTrackMenu(
Track.fromMediaItem(mediaItem),

View File

@ -8,7 +8,7 @@ import 'package:freezer/api/player/player_helper.dart';
import 'package:freezer/icons.dart';
import 'package:freezer/main.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/animated_bars.dart';
import 'package:mini_music_visualizer/mini_music_visualizer.dart';
import '../api/definitions.dart';
import 'cached_image.dart';
@ -107,15 +107,6 @@ class TrackCardTile extends StatelessWidget {
),
),
);
ListTile(
title: Text(title),
subtitle: Text(artist),
leading: Stack(
children: [CachedImage(url: artUri), PlayItemButton(onTap: onTap)],
),
contentPadding: const EdgeInsets.only(left: 16.0, right: 124.0),
onTap: onTap,
);
}
}
@ -209,34 +200,39 @@ class TrackTile extends StatelessWidget {
artist,
maxLines: 1,
),
leading: CachedImage(
url: artUri,
width: 48.0,
height: 48.0,
),
// StreamBuilder<MediaItem?>(
// initialData: audioHandler.mediaItem.value,
// stream: audioHandler.mediaItem,
// builder: (context, snapshot) {
// final child = CachedImage(
// url: artUri,
// width: 48.0,
// height: 48.0,
// );
//
// if (snapshot.data?.id == trackId) {
// return Stack(children: [
// child,
// const Positioned.fill(
// child: DecoratedBox(
// decoration: BoxDecoration(color: Colors.black26),
// child: AnimatedBars()),
// ),
// ]);
// }
//
// return child;
// }),
leading: StreamBuilder<MediaItem?>(
initialData: audioHandler.mediaItem.value,
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
final child = CachedImage(
url: artUri,
width: 48.0,
height: 48.0,
);
if (snapshot.data?.id == trackId) {
return Stack(children: [
child,
Positioned.fill(
child: DecoratedBox(
decoration: const BoxDecoration(color: Colors.black26),
child: Center(
child: SizedBox(
width: 18.0,
height: 16.0,
child: StreamBuilder<bool>(
stream: playerHelper.playing,
builder: (context, snapshot) {
return MiniMusicVisualizer(
color: Colors.white70,
animate: snapshot.data ?? false);
})),
)),
),
]);
}
return child;
}),
onTap: onTap,
onLongPress: normalizeSecondary(onSecondary),
trailing: Row(

View File

@ -371,7 +371,7 @@ packages:
source: hosted
version: "2.0.5"
fading_edge_scrollview:
dependency: transitive
dependency: "direct main"
description:
name: fading_edge_scrollview
sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031

View File

@ -115,6 +115,7 @@ dependencies:
freezed_annotation:
^2.4.1
mini_music_visualizer: ^1.1.0
fading_edge_scrollview: ^3.0.0
#deezcryptor:
#path: deezcryptor/