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:
parent
8ea6bcd073
commit
4b5d0bd09c
|
|
@ -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),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -71,106 +71,110 @@ class FancyScaffoldState extends State<FancyScaffold>
|
||||||
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
|
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
|
||||||
end: 1.0,
|
end: 1.0,
|
||||||
).animate(dragController);
|
).animate(dragController);
|
||||||
return Stack(
|
return ValueListenableBuilder(
|
||||||
children: [
|
valueListenable: statusNotifier,
|
||||||
Positioned.fill(
|
builder: (context, state, child) => PopScope(
|
||||||
child: Scaffold(
|
canPop: state != AnimationStatus.dismissed,
|
||||||
body: widget.navigationRail != null
|
onPopInvoked: state == AnimationStatus.dismissed
|
||||||
? Row(children: [
|
? null
|
||||||
widget.navigationRail!,
|
: (_) => dragController.fling(velocity: -1.0),
|
||||||
const VerticalDivider(
|
child: child!),
|
||||||
indent: 0.0,
|
child: Stack(
|
||||||
endIndent: 0.0,
|
children: [
|
||||||
width: 2.0,
|
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(
|
||||||
Positioned(
|
bottom: 0,
|
||||||
bottom: 0,
|
left: 0,
|
||||||
left: 0,
|
right: 0,
|
||||||
right: 0,
|
child: AnimatedBuilder(
|
||||||
child: AnimatedBuilder(
|
animation: sizeAnimation,
|
||||||
animation: sizeAnimation,
|
builder: (context, child) {
|
||||||
builder: (context, child) {
|
final x = 1.0 - sizeAnimation.value;
|
||||||
final x = 1.0 - sizeAnimation.value;
|
return Padding(
|
||||||
return Padding(
|
padding: EdgeInsets.only(
|
||||||
padding: EdgeInsets.only(
|
bottom: (defaultBottomPadding /*+ 8.0*/) * x,
|
||||||
bottom: (defaultBottomPadding /*+ 8.0*/) * x,
|
//right: 8.0 * x,
|
||||||
//right: 8.0 * x,
|
//left: 8.0 * x,
|
||||||
//left: 8.0 * x,
|
),
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable: statusNotifier,
|
|
||||||
builder: (context, state, child) {
|
|
||||||
return GestureDetector(
|
|
||||||
onVerticalDragEnd: _onVerticalDragEnd,
|
|
||||||
onVerticalDragUpdate: _onVerticalDragUpdate,
|
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: SizeTransition(
|
child: ValueListenableBuilder(
|
||||||
sizeFactor: sizeAnimation,
|
valueListenable: statusNotifier,
|
||||||
axisAlignment: -1.0,
|
builder: (context, state, child) {
|
||||||
axis: Axis.vertical,
|
return GestureDetector(
|
||||||
child: SizedBox(
|
onVerticalDragEnd: _onVerticalDragEnd,
|
||||||
height: screenHeight,
|
onVerticalDragUpdate: _onVerticalDragUpdate,
|
||||||
width: MediaQuery.of(context).size.width,
|
child: child,
|
||||||
child: ValueListenableBuilder(
|
);
|
||||||
valueListenable: statusNotifier,
|
},
|
||||||
builder: (context, state, _) => Stack(
|
child: SizeTransition(
|
||||||
children: [
|
sizeFactor: sizeAnimation,
|
||||||
if (state != AnimationStatus.dismissed)
|
axisAlignment: -1.0,
|
||||||
PopScope(
|
axis: Axis.vertical,
|
||||||
canPop: false,
|
child: SizedBox(
|
||||||
onPopInvoked: (_) =>
|
height: screenHeight,
|
||||||
dragController.fling(velocity: -1.0),
|
width: MediaQuery.of(context).size.width,
|
||||||
child: Positioned.fill(
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: statusNotifier,
|
||||||
|
builder: (context, state, _) => Stack(
|
||||||
|
children: [
|
||||||
|
if (state != AnimationStatus.dismissed)
|
||||||
|
Positioned.fill(
|
||||||
key: const Key('player_screen'),
|
key: const Key('player_screen'),
|
||||||
child: widget.expandedPanel,
|
child: widget.expandedPanel,
|
||||||
),
|
),
|
||||||
),
|
if (state != AnimationStatus.completed)
|
||||||
if (state != AnimationStatus.completed)
|
Positioned(
|
||||||
Positioned(
|
top: 0,
|
||||||
top: 0,
|
right: 0,
|
||||||
right: 0,
|
left: 0,
|
||||||
left: 0,
|
key: const Key('player_bar'),
|
||||||
key: const Key('player_bar'),
|
child: FadeTransition(
|
||||||
child: FadeTransition(
|
opacity: Tween(begin: 1.0, end: 0.0)
|
||||||
opacity: Tween(begin: 1.0, end: 0.0)
|
.animate(dragController),
|
||||||
.animate(dragController),
|
child: SizedBox(
|
||||||
child: SizedBox(
|
height: widget.bottomPanelHeight,
|
||||||
height: widget.bottomPanelHeight,
|
child: widget.bottomPanel),
|
||||||
child: widget.bottomPanel),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
)),
|
||||||
)),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,14 +248,23 @@ class HomepageRowSection extends StatelessWidget {
|
||||||
final HomePageSection section;
|
final HomePageSection section;
|
||||||
const HomepageRowSection(this.section, {super.key});
|
const HomepageRowSection(this.section, {super.key});
|
||||||
|
|
||||||
Widget buildChild(BuildContext context, List<HomePageItem> items) {
|
Widget buildChild(BuildContext context, List<HomePageItem> items,
|
||||||
return Row(
|
{bool hasMore = false}) {
|
||||||
children: items
|
return Row(children: [
|
||||||
.map((item) => Padding(
|
...items.map((item) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
child: HomePageItemWidget(item),
|
child: HomePageItemWidget(item),
|
||||||
))
|
)),
|
||||||
.toList(growable: false));
|
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) {
|
List<List<T>> _sliceInNLists<T>(List<T> source, int n) {
|
||||||
|
|
@ -279,15 +288,37 @@ class HomepageRowSection extends StatelessWidget {
|
||||||
children: _sliceInNLists(section.items!, 3)
|
children: _sliceInNLists(section.items!, 3)
|
||||||
.map((e) => buildChild(context, e))
|
.map((e) => buildChild(context, e))
|
||||||
.toList(growable: false)),
|
.toList(growable: false)),
|
||||||
_ => buildChild(context, section.items!)
|
_ =>
|
||||||
|
buildChild(context, section.items!, hasMore: section.hasMore ?? false)
|
||||||
};
|
};
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: InkWell(
|
||||||
section.title ?? '',
|
onTap: section.hasMore == true
|
||||||
textAlign: TextAlign.left,
|
? () => Navigator.of(context).pushRoute(
|
||||||
maxLines: 2,
|
builder: (context) => HomePageScreen(
|
||||||
overflow: TextOverflow.ellipsis,
|
title: section.title!,
|
||||||
style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w900),
|
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(
|
subtitle: Scrollbar(
|
||||||
thickness: MainScreen.of(context).isDesktop ? null : 1.0,
|
thickness: MainScreen.of(context).isDesktop ? null : 1.0,
|
||||||
|
|
@ -296,17 +327,6 @@ class HomepageRowSection extends StatelessWidget {
|
||||||
child: child,
|
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:freezer/api/definitions.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/error.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';
|
||||||
|
|
||||||
class LyricsScreen extends StatelessWidget {
|
class LyricsScreen extends StatelessWidget {
|
||||||
const LyricsScreen({super.key});
|
const LyricsScreen({super.key});
|
||||||
|
|
@ -21,11 +24,10 @@ class LyricsScreen extends StatelessWidget {
|
||||||
return PlayerScreenBackground(
|
return PlayerScreenBackground(
|
||||||
enabled: settings.playerBackgroundOnLyrics,
|
enabled: settings.playerBackgroundOnLyrics,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Lyrics'.i18n),
|
|
||||||
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
|
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
|
||||||
context,
|
context,
|
||||||
enabled: settings.playerBackgroundOnLyrics),
|
enabled: settings.playerBackgroundOnLyrics),
|
||||||
backgroundColor: Colors.transparent,
|
forceMaterialTransparency: true,
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -48,7 +50,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
StreamSubscription? _mediaItemSub;
|
StreamSubscription? _mediaItemSub;
|
||||||
StreamSubscription? _positionSub;
|
StreamSubscription? _positionSub;
|
||||||
int? _currentIndex = -1;
|
int _currentIndex = -1;
|
||||||
Duration _nextOffset = Duration.zero;
|
Duration _nextOffset = Duration.zero;
|
||||||
Duration _currentOffset = Duration.zero;
|
Duration _currentOffset = Duration.zero;
|
||||||
String? _currentTrackId;
|
String? _currentTrackId;
|
||||||
|
|
@ -68,6 +70,9 @@ 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;
|
||||||
|
|
@ -137,7 +142,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
} else {
|
} else {
|
||||||
final widgetHeight = _widgetConstraints!.maxHeight;
|
final widgetHeight = _widgetConstraints!.maxHeight;
|
||||||
final minScroll = actualHeight * _currentIndex!;
|
final minScroll = actualHeight * _currentIndex!;
|
||||||
scrollTo = minScroll - widgetHeight / 2 + height / 2;
|
scrollTo = minScroll + height / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollTo < 0.0) scrollTo = 0.0;
|
if (scrollTo < 0.0) scrollTo = 0.0;
|
||||||
|
|
@ -157,10 +162,10 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
if (position < _nextOffset && position > _currentOffset) return;
|
if (position < _nextOffset && position > _currentOffset) return;
|
||||||
|
|
||||||
_currentIndex =
|
_currentIndex =
|
||||||
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
|
_lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position) ?? -1;
|
||||||
if (_currentIndex! < 0) return;
|
if (_currentIndex < 0) return;
|
||||||
|
|
||||||
if (_currentIndex! < _lyrics!.lyrics!.length - 1) {
|
if (_currentIndex < _lyrics!.lyrics!.length - 1) {
|
||||||
// update nextOffset
|
// update nextOffset
|
||||||
_nextOffset = _lyrics!.lyrics![_currentIndex! + 1].offset!;
|
_nextOffset = _lyrics!.lyrics![_currentIndex! + 1].offset!;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -208,7 +213,6 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
print('fuck? $state');
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
_cancelSubscriptions();
|
_cancelSubscriptions();
|
||||||
|
|
@ -240,59 +244,49 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(children: [
|
_error != null
|
||||||
if (_freeScroll && !_loading)
|
? ErrorScreen(message: _error.toString())
|
||||||
Center(
|
:
|
||||||
child: TextButton(
|
// Loading lyrics
|
||||||
onPressed: () {
|
_loading
|
||||||
setState(() => _freeScroll = false);
|
? const Center(child: CircularProgressIndicator())
|
||||||
_scrollToLyric();
|
: LayoutBuilder(builder: (context, constraints) {
|
||||||
},
|
_widgetConstraints = constraints;
|
||||||
style: ButtonStyle(
|
return NotificationListener<ScrollStartNotification>(
|
||||||
foregroundColor: MaterialStateProperty.all(Colors.white)),
|
onNotification: (notification) {
|
||||||
child: Text(
|
if (!_syncedLyrics) return false;
|
||||||
_currentIndex! >= 0
|
final extentDiff =
|
||||||
? (_lyrics?.lyrics?[_currentIndex!].text ?? '...')
|
(notification.metrics.extentBefore -
|
||||||
: '...',
|
notification.metrics.extentAfter)
|
||||||
textAlign: TextAlign.center,
|
.abs();
|
||||||
)),
|
// avoid accidental clicks
|
||||||
),
|
const extentThreshold = 10.0;
|
||||||
Expanded(
|
if (extentDiff >= extentThreshold &&
|
||||||
child: _error != null
|
!_animatedScroll &&
|
||||||
?
|
!_loading &&
|
||||||
//Shouldn't really happen, empty lyrics have own text
|
!_freeScroll) {
|
||||||
ErrorScreen(message: _error.toString())
|
setState(() => _freeScroll = true);
|
||||||
:
|
}
|
||||||
// Loading lyrics
|
return false;
|
||||||
_loading
|
},
|
||||||
? const Center(child: CircularProgressIndicator())
|
child: ScrollConfiguration(
|
||||||
: LayoutBuilder(builder: (context, constraints) {
|
behavior: _scrollBehavior,
|
||||||
_widgetConstraints = constraints;
|
child: FadingEdgeScrollView.fromScrollView(
|
||||||
return NotificationListener<ScrollStartNotification>(
|
gradientFractionOnStart: 0.25,
|
||||||
onNotification:
|
gradientFractionOnEnd: 0.25,
|
||||||
(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(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: 8.0),
|
horizontal: 8.0,
|
||||||
|
vertical: constraints.maxHeight / 2 -
|
||||||
|
height / 2),
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
itemExtent: !_syncedLyrics
|
itemExtent: !_syncedLyrics
|
||||||
? null
|
? null
|
||||||
|
|
@ -300,8 +294,22 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
(_showTranslation
|
(_showTranslation
|
||||||
? additionalTranslationHeight
|
? additionalTranslationHeight
|
||||||
: 0.0),
|
: 0.0),
|
||||||
itemCount: _lyrics!.lyrics!.length,
|
itemCount: _lyrics!.lyrics!.length + 1,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
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(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
|
|
@ -312,15 +320,22 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(8.0),
|
BorderRadius.circular(12.0),
|
||||||
onTap: _syncedLyrics &&
|
onTap: _syncedLyrics &&
|
||||||
_lyrics!.id != null
|
_lyrics!.id != null
|
||||||
? () => audioHandler.seek(
|
? () => audioHandler.seek(
|
||||||
_lyrics!.lyrics![i].offset!)
|
_lyrics!.lyrics![i].offset!)
|
||||||
: null,
|
: null,
|
||||||
child: Center(
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4.0,
|
||||||
|
//vertical: 24.0,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_lyrics!.lyrics![i].text!,
|
_lyrics!.lyrics![i].text!,
|
||||||
|
|
@ -328,6 +343,7 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
? TextAlign.center
|
? TextAlign.center
|
||||||
: TextAlign.start,
|
: TextAlign.start,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
fontSize: _syncedLyrics
|
fontSize: _syncedLyrics
|
||||||
? 26.0
|
? 26.0
|
||||||
: 20.0,
|
: 20.0,
|
||||||
|
|
@ -357,25 +373,35 @@ class _LyricsWidgetState extends State<LyricsWidget>
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
)));
|
))));
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
]),
|
|
||||||
if (_availableTranslation)
|
if (_availableTranslation)
|
||||||
Align(
|
Positioned(
|
||||||
alignment: Alignment.bottomCenter,
|
bottom: 16.0,
|
||||||
child: Padding(
|
left: 0,
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
right: 0,
|
||||||
child: ElevatedButton(
|
child: Center(
|
||||||
onPressed: () {
|
child: ElevatedButton(
|
||||||
setState(() => _showTranslation = !_showTranslation);
|
onPressed: () {
|
||||||
SchedulerBinding.instance
|
setState(() => _showTranslation = !_showTranslation);
|
||||||
.addPostFrameCallback((_) => _scrollToLyric());
|
SchedulerBinding.instance
|
||||||
},
|
.addPostFrameCallback((_) => _scrollToLyric());
|
||||||
child: Text(_showTranslation
|
},
|
||||||
? 'Without translation'.i18n
|
child: Text(_showTranslation
|
||||||
: 'With translation'.i18n)),
|
? '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();
|
||||||
|
})))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,12 +163,15 @@ class PlayerScreenBackground extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildChild(
|
Widget _buildChild(
|
||||||
BuildContext context, BackgroundProvider provider, Widget child) {
|
BuildContext context, BackgroundProvider provider, Widget child) {
|
||||||
|
final isLightMode = Theme.of(context).brightness == Brightness.light;
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
if (provider.imageProvider != null || settings.colorGradientBackground)
|
if (provider.imageProvider != null || settings.colorGradientBackground)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: provider.imageProvider != null
|
child: provider.imageProvider != null
|
||||||
? DecoratedBox(
|
? DecoratedBox(
|
||||||
decoration: const BoxDecoration(color: Colors.black),
|
decoration: BoxDecoration(
|
||||||
|
color: Color.lerp(provider.dominantColor,
|
||||||
|
isLightMode ? Colors.white : Colors.black, 0.75)),
|
||||||
child: ImageFiltered(
|
child: ImageFiltered(
|
||||||
imageFilter: ImageFilter.blur(
|
imageFilter: ImageFilter.blur(
|
||||||
tileMode: TileMode.decal,
|
tileMode: TileMode.decal,
|
||||||
|
|
@ -180,10 +183,7 @@ class PlayerScreenBackground extends StatelessWidget {
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
image: provider.imageProvider!,
|
image: provider.imageProvider!,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
colorFilter: ColorFilter.mode(
|
opacity: 0.35,
|
||||||
Colors.white
|
|
||||||
.withOpacity(settings.isDark ? 0.55 : 0.75),
|
|
||||||
BlendMode.dstATop),
|
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -170,11 +170,10 @@ class _QueueListWidgetState extends State<QueueListWidget> {
|
||||||
trailing: ReorderableDragStartListener(
|
trailing: ReorderableDragStartListener(
|
||||||
index: index, child: const Icon(Icons.drag_handle)),
|
index: index, child: const Icon(Icons.drag_handle)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
audioHandler.skipToQueueItem(index).then((value) {
|
if (widget.shouldPopOnTap) {
|
||||||
if (widget.shouldPopOnTap) {
|
Navigator.pop(context);
|
||||||
Navigator.of(context).pop();
|
}
|
||||||
}
|
audioHandler.skipToQueueItem(index);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onSecondary: (details) => menuSheet.defaultTrackMenu(
|
onSecondary: (details) => menuSheet.defaultTrackMenu(
|
||||||
Track.fromMediaItem(mediaItem),
|
Track.fromMediaItem(mediaItem),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import 'package:freezer/api/player/player_helper.dart';
|
||||||
import 'package:freezer/icons.dart';
|
import 'package:freezer/icons.dart';
|
||||||
import 'package:freezer/main.dart';
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/translations.i18n.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 '../api/definitions.dart';
|
||||||
import 'cached_image.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,
|
artist,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
leading: CachedImage(
|
leading: StreamBuilder<MediaItem?>(
|
||||||
url: artUri,
|
initialData: audioHandler.mediaItem.value,
|
||||||
width: 48.0,
|
stream: audioHandler.mediaItem,
|
||||||
height: 48.0,
|
builder: (context, snapshot) {
|
||||||
),
|
final child = CachedImage(
|
||||||
// StreamBuilder<MediaItem?>(
|
url: artUri,
|
||||||
// initialData: audioHandler.mediaItem.value,
|
width: 48.0,
|
||||||
// stream: audioHandler.mediaItem,
|
height: 48.0,
|
||||||
// builder: (context, snapshot) {
|
);
|
||||||
// final child = CachedImage(
|
|
||||||
// url: artUri,
|
if (snapshot.data?.id == trackId) {
|
||||||
// width: 48.0,
|
return Stack(children: [
|
||||||
// height: 48.0,
|
child,
|
||||||
// );
|
Positioned.fill(
|
||||||
//
|
child: DecoratedBox(
|
||||||
// if (snapshot.data?.id == trackId) {
|
decoration: const BoxDecoration(color: Colors.black26),
|
||||||
// return Stack(children: [
|
child: Center(
|
||||||
// child,
|
child: SizedBox(
|
||||||
// const Positioned.fill(
|
width: 18.0,
|
||||||
// child: DecoratedBox(
|
height: 16.0,
|
||||||
// decoration: BoxDecoration(color: Colors.black26),
|
child: StreamBuilder<bool>(
|
||||||
// child: AnimatedBars()),
|
stream: playerHelper.playing,
|
||||||
// ),
|
builder: (context, snapshot) {
|
||||||
// ]);
|
return MiniMusicVisualizer(
|
||||||
// }
|
color: Colors.white70,
|
||||||
//
|
animate: snapshot.data ?? false);
|
||||||
// return child;
|
})),
|
||||||
// }),
|
)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: normalizeSecondary(onSecondary),
|
onLongPress: normalizeSecondary(onSecondary),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
|
|
|
||||||
|
|
@ -371,7 +371,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.5"
|
||||||
fading_edge_scrollview:
|
fading_edge_scrollview:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fading_edge_scrollview
|
name: fading_edge_scrollview
|
||||||
sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031
|
sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ dependencies:
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
^2.4.1
|
^2.4.1
|
||||||
mini_music_visualizer: ^1.1.0
|
mini_music_visualizer: ^1.1.0
|
||||||
|
fading_edge_scrollview: ^3.0.0
|
||||||
#deezcryptor:
|
#deezcryptor:
|
||||||
#path: deezcryptor/
|
#path: deezcryptor/
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue