feat(player): animated gradient background

This commit is contained in:
Kingkor Roy Tirtho 2023-04-07 11:25:55 +06:00
parent 80959aa0ca
commit 49b5d0e694
7 changed files with 268 additions and 149 deletions

View File

@ -70,6 +70,18 @@ class PlayerControls extends HookConsumerWidget {
minimumSize: const Size(28, 28),
);
final accentColor = palette?.lightVibrantColor ??
palette?.darkVibrantColor ??
dominantColor;
final resumePauseStyle = IconButton.styleFrom(
backgroundColor: accentColor?.color ?? theme.colorScheme.primary,
foregroundColor:
accentColor?.titleTextColor ?? theme.colorScheme.onPrimary,
padding: const EdgeInsets.all(12),
iconSize: 24,
);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
@ -199,14 +211,7 @@ class PlayerControls extends HookConsumerWidget {
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: IconButton.styleFrom(
backgroundColor:
dominantColor?.color ?? theme.colorScheme.primary,
foregroundColor: dominantColor?.titleTextColor ??
theme.colorScheme.onPrimary,
padding: const EdgeInsets.all(12),
iconSize: 24,
),
style: resumePauseStyle,
onPressed: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),

View File

@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class AnimateGradient extends HookWidget {
const AnimateGradient({
Key? key,
required this.primaryColors,
required this.secondaryColors,
this.child,
this.primaryBegin,
this.primaryEnd,
this.secondaryBegin,
this.secondaryEnd,
AnimationController? controller,
this.duration = const Duration(seconds: 4),
this.animateAlignments = true,
this.reverse = true,
}) : assert(primaryColors.length >= 2),
assert(primaryColors.length == secondaryColors.length),
_controller = controller,
super(key: key);
/// [controller]: pass this to have a fine control over the [Animation]
final AnimationController? _controller;
/// [duration]: Time to switch between [Gradient].
/// By default its value is [Duration(seconds:4)]
final Duration duration;
/// [primaryColors]: These will be the starting colors of the [Animation].
final List<Color> primaryColors;
/// [secondaryColors]: These Colors are those in which the [primaryColors] will transition into.
final List<Color> secondaryColors;
/// [primaryBegin]: This is begin [Alignment] for [primaryColors].
/// By default its value is [Alignment.topLeft]
final Alignment? primaryBegin;
/// [primaryBegin]: This is end [Alignment] for [primaryColors].
/// By default its value is [Alignment.topRight]
final Alignment? primaryEnd;
/// [secondaryBegin]: This is begin [Alignment] for [secondaryColors].
/// By default its value is [Alignment.bottomLeft]
final Alignment? secondaryBegin;
/// [secondaryEnd]: This is end [Alignment] for [secondaryColors].
/// By default its value is [Alignment.bottomRight]
final Alignment? secondaryEnd;
/// [animateAlignments]: set to false if you don't want to animate the alignments.
/// This can provide you way cooler animations
final bool animateAlignments;
/// [reverse]: set it to false if you don't want to reverse the animation.
/// using that it will go into one direction only
final bool reverse;
final Widget? child;
@override
Widget build(BuildContext context) {
// ignore: no_leading_underscores_for_local_identifiers
final __controller = useAnimationController(
duration: duration,
)..repeat(reverse: reverse);
final controller = _controller ?? __controller;
final animation = useMemoized(
() => CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
[controller]);
final colorTween = useMemoized(
() => primaryColors.map((color) {
return ColorTween(
begin: color,
end: color,
);
}).toList(),
[primaryColors]);
final colors = useMemoized(
() => colorTween.map((color) {
return color.evaluate(animation)!;
}).toList(),
[colorTween, animation]);
final begin = useMemoized(
() => AlignmentTween(
begin: primaryBegin ?? Alignment.topLeft,
end: primaryEnd ?? Alignment.topRight,
),
[primaryBegin, primaryEnd]);
final end = useMemoized(
() => AlignmentTween(
begin: secondaryBegin ?? Alignment.bottomLeft,
end: secondaryEnd ?? Alignment.bottomRight,
),
[secondaryBegin, secondaryEnd]);
return AnimatedBuilder(
animation: animation,
child: child,
builder: (BuildContext context, Widget? child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: animateAlignments
? begin.evaluate(animation)
: (primaryBegin as Alignment),
end: animateAlignments
? end.evaluate(animation)
: primaryEnd as Alignment,
colors: colors,
),
),
child: child,
);
},
);
}
}

View File

@ -12,11 +12,13 @@ class UniversalImage extends HookWidget {
final double? width;
final double scale;
final String? placeholder;
final BoxFit? fit;
const UniversalImage({
required this.path,
this.height,
this.width,
this.placeholder,
this.fit,
this.scale = 1,
Key? key,
}) : super(key: key);
@ -57,6 +59,7 @@ class UniversalImage extends HookWidget {
height: height,
width: width,
placeholder: AssetImage(placeholder ?? Assets.placeholder.path),
fit: fit,
);
} else if (Uri.tryParse(path) != null && !path.startsWith("assets")) {
return Image.file(
@ -66,6 +69,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(),
scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
placeholder ?? Assets.placeholder.path,
@ -85,6 +89,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(),
scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
placeholder ?? Assets.placeholder.path,
@ -105,6 +110,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(),
scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
placeholder ?? Assets.placeholder.path,

View File

@ -113,17 +113,6 @@ void main(List<String> rawArgs) async {
enableApplicationParameters: false,
),
FileHandler(await getLogsPath(), printLogs: false),
SnackbarHandler(
const Duration(seconds: 5),
action: SnackBarAction(
label: "Dismiss",
onPressed: () {
ScaffoldMessenger.of(
Catcher.navigatorKey!.currentContext!,
).hideCurrentSnackBar();
},
),
),
],
),
releaseConfig: CatcherOptions(SilentReportMode(), [

View File

@ -9,6 +9,7 @@ import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_actions.dart';
import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/components/shared/animated_gradient.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoints.dart';
@ -75,123 +76,136 @@ class PlayerView extends HookConsumerWidget {
leading: const BackButton(),
),
extendBodyBehindAppBar: true,
body: Container(
decoration: BoxDecoration(
color: palette.dominantColor?.color,
gradient: LinearGradient(
colors: [
palette.dominantColor?.color ?? theme.colorScheme.primary,
palette.mutedColor?.color ?? theme.colorScheme.secondary,
],
transform: const GradientRotation(0.5),
),
),
alignment: Alignment.center,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 580),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 2,
blurRadius: 10,
offset: Offset(0, 0),
body: AnimateGradient(
animateAlignments: true,
primaryBegin: Alignment.topLeft,
primaryEnd: Alignment.bottomLeft,
secondaryBegin: Alignment.bottomRight,
secondaryEnd: Alignment.topRight,
duration: const Duration(seconds: 25),
primaryColors: [
palette.dominantColor?.color ?? theme.colorScheme.primary,
palette.mutedColor?.color ?? theme.colorScheme.secondary,
],
secondaryColors: [
(palette.darkVibrantColor ?? palette.lightVibrantColor)?.color ??
theme.colorScheme.primaryContainer,
(palette.darkMutedColor ?? palette.lightMutedColor)?.color ??
theme.colorScheme.secondaryContainer,
],
child: Container(
alignment: Alignment.center,
width: double.infinity,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 580),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Container(
constraints:
const BoxConstraints(maxHeight: 300, maxWidth: 300),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Colors.black26,
spreadRadius: 2,
blurRadius: 10,
offset: Offset(0, 0),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: UniversalImage(
path: albumArt,
placeholder: Assets.albumPlaceholder.path,
fit: BoxFit.cover,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: UniversalImage(
path: albumArt,
placeholder: Assets.albumPlaceholder.path,
),
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoSizeText(
currentTrack?.name ?? "Not playing",
style: TextStyle(
fontSize: 20,
color: titleTextColor,
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoSizeText(
currentTrack?.name ?? "Not playing",
style: TextStyle(
fontSize: 20,
color: titleTextColor,
),
maxLines: 1,
textAlign: TextAlign.start,
),
maxLines: 1,
textAlign: TextAlign.start,
),
if (isLocalTrack)
Text(
TypeConversionUtils.artists_X_String<Artist>(
if (isLocalTrack)
Text(
TypeConversionUtils.artists_X_String<Artist>(
currentTrack?.artists ?? [],
),
style: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
)
else
TypeConversionUtils.artists_X_ClickableArtists(
currentTrack?.artists ?? [],
textStyle: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
onRouteChange: (route) {
GoRouter.of(context).pop();
GoRouter.of(context).push(route);
},
),
style: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
)
else
TypeConversionUtils.artists_X_ClickableArtists(
currentTrack?.artists ?? [],
textStyle: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
onRouteChange: (route) {
GoRouter.of(context).pop();
GoRouter.of(context).push(route);
],
),
),
const SizedBox(height: 40),
PlayerControls(palette: palette),
const Spacer(),
PlayerActions(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
floatingQueue: false,
extraActions: [
if (auth != null)
IconButton(
tooltip: "Open Lyrics",
icon: const Icon(SpotubeIcons.music),
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true,
enableDrag: true,
isScrollControlled: true,
backgroundColor: Colors.black38,
barrierColor: Colors.black12,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(context).size.height *
0.8,
),
builder: (context) =>
const LyricsPage(isModal: true),
);
},
),
)
],
),
),
const SizedBox(height: 40),
PlayerControls(palette: palette),
const Spacer(),
PlayerActions(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
floatingQueue: false,
extraActions: [
if (auth != null)
IconButton(
tooltip: "Open Lyrics",
icon: const Icon(SpotubeIcons.music),
onPressed: () {
showModalBottomSheet(
context: context,
isDismissible: true,
enableDrag: true,
isScrollControlled: true,
backgroundColor: Colors.black38,
barrierColor: Colors.black12,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(context).size.height * 0.8,
),
builder: (context) =>
const LyricsPage(isModal: true),
);
},
)
],
),
],
],
),
),
),
),

View File

@ -17,23 +17,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.4.0"
animate_gradient:
dependency: "direct main"
description:
path: "."
ref: "2e02ab5d1cb60fc172a5f15f6e91bd34a050af23"
resolved-ref: "2e02ab5d1cb60fc172a5f15f6e91bd34a050af23"
url: "https://github.com/Vikaskumar75/Animated-Gradient"
source: git
version: "0.0.2"
animated_gradient:
dependency: "direct main"
description:
name: animated_gradient
sha256: "9c0c52a093817ae42550e3affec6973a7bae7186d1d5d58749ca9689da3ba245"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
app_package_maker:
dependency: transitive
description:

View File

@ -9,11 +9,6 @@ environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
animate_gradient:
git:
url: https://github.com/Vikaskumar75/Animated-Gradient
ref: 2e02ab5d1cb60fc172a5f15f6e91bd34a050af23
animated_gradient: ^0.0.2
args: ^2.3.2
async: ^2.9.0
audio_service: ^0.18.9