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), 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( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () { onTap: () {
@ -199,14 +211,7 @@ class PlayerControls extends HookConsumerWidget {
: Icon( : Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play, playing ? SpotubeIcons.pause : SpotubeIcons.play,
), ),
style: IconButton.styleFrom( style: resumePauseStyle,
backgroundColor:
dominantColor?.color ?? theme.colorScheme.primary,
foregroundColor: dominantColor?.titleTextColor ??
theme.colorScheme.onPrimary,
padding: const EdgeInsets.all(12),
iconSize: 24,
),
onPressed: Actions.handler<PlayPauseIntent>( onPressed: Actions.handler<PlayPauseIntent>(
context, context,
PlayPauseIntent(ref), 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? width;
final double scale; final double scale;
final String? placeholder; final String? placeholder;
final BoxFit? fit;
const UniversalImage({ const UniversalImage({
required this.path, required this.path,
this.height, this.height,
this.width, this.width,
this.placeholder, this.placeholder,
this.fit,
this.scale = 1, this.scale = 1,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -57,6 +59,7 @@ class UniversalImage extends HookWidget {
height: height, height: height,
width: width, width: width,
placeholder: AssetImage(placeholder ?? Assets.placeholder.path), placeholder: AssetImage(placeholder ?? Assets.placeholder.path),
fit: fit,
); );
} else if (Uri.tryParse(path) != null && !path.startsWith("assets")) { } else if (Uri.tryParse(path) != null && !path.startsWith("assets")) {
return Image.file( return Image.file(
@ -66,6 +69,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(), cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(), cacheWidth: width?.toInt(),
scale: scale, scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Image.asset( return Image.asset(
placeholder ?? Assets.placeholder.path, placeholder ?? Assets.placeholder.path,
@ -85,6 +89,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(), cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(), cacheWidth: width?.toInt(),
scale: scale, scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Image.asset( return Image.asset(
placeholder ?? Assets.placeholder.path, placeholder ?? Assets.placeholder.path,
@ -105,6 +110,7 @@ class UniversalImage extends HookWidget {
cacheHeight: height?.toInt(), cacheHeight: height?.toInt(),
cacheWidth: width?.toInt(), cacheWidth: width?.toInt(),
scale: scale, scale: scale,
fit: fit,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
return Image.asset( return Image.asset(
placeholder ?? Assets.placeholder.path, placeholder ?? Assets.placeholder.path,

View File

@ -113,17 +113,6 @@ void main(List<String> rawArgs) async {
enableApplicationParameters: false, enableApplicationParameters: false,
), ),
FileHandler(await getLogsPath(), printLogs: 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(), [ 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/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_actions.dart'; import 'package:spotube/components/player/player_actions.dart';
import 'package:spotube/components/player/player_controls.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/page_window_title_bar.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/hooks/use_breakpoints.dart'; import 'package:spotube/hooks/use_breakpoints.dart';
@ -75,123 +76,136 @@ class PlayerView extends HookConsumerWidget {
leading: const BackButton(), leading: const BackButton(),
), ),
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
body: Container( body: AnimateGradient(
decoration: BoxDecoration( animateAlignments: true,
color: palette.dominantColor?.color, primaryBegin: Alignment.topLeft,
gradient: LinearGradient( primaryEnd: Alignment.bottomLeft,
colors: [ secondaryBegin: Alignment.bottomRight,
palette.dominantColor?.color ?? theme.colorScheme.primary, secondaryEnd: Alignment.topRight,
palette.mutedColor?.color ?? theme.colorScheme.secondary, duration: const Duration(seconds: 25),
], primaryColors: [
transform: const GradientRotation(0.5), palette.dominantColor?.color ?? theme.colorScheme.primary,
), palette.mutedColor?.color ?? theme.colorScheme.secondary,
), ],
alignment: Alignment.center, secondaryColors: [
child: ConstrainedBox( (palette.darkVibrantColor ?? palette.lightVibrantColor)?.color ??
constraints: const BoxConstraints(maxWidth: 580), theme.colorScheme.primaryContainer,
child: SafeArea( (palette.darkMutedColor ?? palette.lightMutedColor)?.color ??
child: Padding( theme.colorScheme.secondaryContainer,
padding: const EdgeInsets.all(8.0), ],
child: Column( child: Container(
children: [ alignment: Alignment.center,
DecoratedBox( width: double.infinity,
decoration: BoxDecoration( child: ConstrainedBox(
borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints(maxWidth: 580),
boxShadow: const [ child: SafeArea(
BoxShadow( child: Padding(
color: Colors.black26, padding: const EdgeInsets.all(8.0),
spreadRadius: 2, child: Column(
blurRadius: 10, children: [
offset: Offset(0, 0), 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),
const SizedBox(height: 10), Container(
Container( padding: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 16), alignment: Alignment.centerLeft,
alignment: Alignment.centerLeft, child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ AutoSizeText(
AutoSizeText( currentTrack?.name ?? "Not playing",
currentTrack?.name ?? "Not playing", style: TextStyle(
style: TextStyle( fontSize: 20,
fontSize: 20, color: titleTextColor,
color: titleTextColor, ),
maxLines: 1,
textAlign: TextAlign.start,
), ),
maxLines: 1, if (isLocalTrack)
textAlign: TextAlign.start, Text(
), TypeConversionUtils.artists_X_String<Artist>(
if (isLocalTrack) currentTrack?.artists ?? [],
Text( ),
TypeConversionUtils.artists_X_String<Artist>( style: theme.textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
color: bodyTextColor,
),
)
else
TypeConversionUtils.artists_X_ClickableArtists(
currentTrack?.artists ?? [], 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, ),
), const SizedBox(height: 40),
) PlayerControls(palette: palette),
else const Spacer(),
TypeConversionUtils.artists_X_ClickableArtists( PlayerActions(
currentTrack?.artists ?? [], mainAxisAlignment: MainAxisAlignment.spaceEvenly,
textStyle: theme.textTheme.bodyMedium!.copyWith( floatingQueue: false,
fontWeight: FontWeight.bold, extraActions: [
color: bodyTextColor, if (auth != null)
), IconButton(
onRouteChange: (route) { tooltip: "Open Lyrics",
GoRouter.of(context).pop(); icon: const Icon(SpotubeIcons.music),
GoRouter.of(context).push(route); 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" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.0" 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: app_package_maker:
dependency: transitive dependency: transitive
description: description:

View File

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