feat: flag to hide spotify generated images with patterns
BIN
assets/patterns/black_white_visualized.jpg
Normal file
After Width: | Height: | Size: 336 KiB |
BIN
assets/patterns/brazil_carnival.jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
assets/patterns/cotton_balls.jpg
Normal file
After Width: | Height: | Size: 498 KiB |
BIN
assets/patterns/cute_worms.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
assets/patterns/flash_cross_axis.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
assets/patterns/memphis_shapes.jpg
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
assets/patterns/oval_gloomy.jpg
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
assets/patterns/oval_sunny.jpg
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
assets/patterns/red_nimbuses.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
assets/patterns/tree_bark.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
assets/patterns/vibrant_pentagons.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
assets/patterns/wiring_pattern.jpg
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
assets/patterns/zigzags_gloomy.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
assets/patterns/zigzags_sunny.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
@ -35,6 +35,84 @@ class $AssetsLogosGen {
|
|||||||
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $AssetsPatternsGen {
|
||||||
|
const $AssetsPatternsGen();
|
||||||
|
|
||||||
|
/// File path: assets/patterns/black_white_visualized.jpg
|
||||||
|
AssetGenImage get blackWhiteVisualized =>
|
||||||
|
const AssetGenImage('assets/patterns/black_white_visualized.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/brazil_carnival.jpg
|
||||||
|
AssetGenImage get brazilCarnival =>
|
||||||
|
const AssetGenImage('assets/patterns/brazil_carnival.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/cotton_balls.jpg
|
||||||
|
AssetGenImage get cottonBalls =>
|
||||||
|
const AssetGenImage('assets/patterns/cotton_balls.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/cute_worms.jpg
|
||||||
|
AssetGenImage get cuteWorms =>
|
||||||
|
const AssetGenImage('assets/patterns/cute_worms.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/flash_cross_axis.jpg
|
||||||
|
AssetGenImage get flashCrossAxis =>
|
||||||
|
const AssetGenImage('assets/patterns/flash_cross_axis.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/memphis_shapes.jpg
|
||||||
|
AssetGenImage get memphisShapes =>
|
||||||
|
const AssetGenImage('assets/patterns/memphis_shapes.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/oval_gloomy.jpg
|
||||||
|
AssetGenImage get ovalGloomy =>
|
||||||
|
const AssetGenImage('assets/patterns/oval_gloomy.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/oval_sunny.jpg
|
||||||
|
AssetGenImage get ovalSunny =>
|
||||||
|
const AssetGenImage('assets/patterns/oval_sunny.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/red_nimbuses.jpg
|
||||||
|
AssetGenImage get redNimbuses =>
|
||||||
|
const AssetGenImage('assets/patterns/red_nimbuses.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/tree_bark.jpg
|
||||||
|
AssetGenImage get treeBark =>
|
||||||
|
const AssetGenImage('assets/patterns/tree_bark.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/vibrant_pentagons.jpg
|
||||||
|
AssetGenImage get vibrantPentagons =>
|
||||||
|
const AssetGenImage('assets/patterns/vibrant_pentagons.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/wiring_pattern.jpg
|
||||||
|
AssetGenImage get wiringPattern =>
|
||||||
|
const AssetGenImage('assets/patterns/wiring_pattern.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/zigzags_gloomy.jpg
|
||||||
|
AssetGenImage get zigzagsGloomy =>
|
||||||
|
const AssetGenImage('assets/patterns/zigzags_gloomy.jpg');
|
||||||
|
|
||||||
|
/// File path: assets/patterns/zigzags_sunny.jpg
|
||||||
|
AssetGenImage get zigzagsSunny =>
|
||||||
|
const AssetGenImage('assets/patterns/zigzags_sunny.jpg');
|
||||||
|
|
||||||
|
/// List of all assets
|
||||||
|
List<AssetGenImage> get values => [
|
||||||
|
blackWhiteVisualized,
|
||||||
|
brazilCarnival,
|
||||||
|
cottonBalls,
|
||||||
|
cuteWorms,
|
||||||
|
flashCrossAxis,
|
||||||
|
memphisShapes,
|
||||||
|
ovalGloomy,
|
||||||
|
ovalSunny,
|
||||||
|
redNimbuses,
|
||||||
|
treeBark,
|
||||||
|
vibrantPentagons,
|
||||||
|
wiringPattern,
|
||||||
|
zigzagsGloomy,
|
||||||
|
zigzagsSunny
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
class $AssetsTutorialGen {
|
class $AssetsTutorialGen {
|
||||||
const $AssetsTutorialGen();
|
const $AssetsTutorialGen();
|
||||||
|
|
||||||
@ -67,6 +145,7 @@ class Assets {
|
|||||||
static const AssetGenImage likedTracks =
|
static const AssetGenImage likedTracks =
|
||||||
AssetGenImage('assets/liked-tracks.jpg');
|
AssetGenImage('assets/liked-tracks.jpg');
|
||||||
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||||
|
static const $AssetsPatternsGen patterns = $AssetsPatternsGen();
|
||||||
static const AssetGenImage placeholder =
|
static const AssetGenImage placeholder =
|
||||||
AssetGenImage('assets/placeholder.png');
|
AssetGenImage('assets/placeholder.png');
|
||||||
static const AssetGenImage spotubeHeroBanner =
|
static const AssetGenImage spotubeHeroBanner =
|
||||||
|
@ -38,6 +38,11 @@ abstract class Env {
|
|||||||
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
@EnviedField(varName: "RELEASE_CHANNEL", defaultValue: "nightly")
|
||||||
static final String _releaseChannel = _Env._releaseChannel;
|
static final String _releaseChannel = _Env._releaseChannel;
|
||||||
|
|
||||||
|
@EnviedField(varName: "DISABLE_SPOTIFY_IMAGES", defaultValue: "0")
|
||||||
|
static final int _disableSpotifyImages = _Env._disableSpotifyImages;
|
||||||
|
|
||||||
|
static bool get disableSpotifyImages => _disableSpotifyImages == 1;
|
||||||
|
|
||||||
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
static ReleaseChannel get releaseChannel => _releaseChannel == "stable"
|
||||||
? ReleaseChannel.stable
|
? ReleaseChannel.stable
|
||||||
: ReleaseChannel.nightly;
|
: ReleaseChannel.nightly;
|
||||||
|
@ -11,14 +11,14 @@ class PlaybuttonCard extends StatelessWidget {
|
|||||||
final void Function()? onAddToQueuePressed;
|
final void Function()? onAddToQueuePressed;
|
||||||
final String? description;
|
final String? description;
|
||||||
|
|
||||||
final String imageUrl;
|
final String? imageUrl;
|
||||||
|
final Widget? image;
|
||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String title;
|
final String title;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
|
|
||||||
const PlaybuttonCard({
|
const PlaybuttonCard({
|
||||||
required this.imageUrl,
|
|
||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.title,
|
required this.title,
|
||||||
@ -27,8 +27,13 @@ class PlaybuttonCard extends StatelessWidget {
|
|||||||
this.onAddToQueuePressed,
|
this.onAddToQueuePressed,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.isOwner = false,
|
this.isOwner = false,
|
||||||
|
this.imageUrl,
|
||||||
|
this.image,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
}) : assert(
|
||||||
|
imageUrl != null || image != null,
|
||||||
|
"imageUrl and image can't be null at the same time",
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -40,17 +45,27 @@ class PlaybuttonCard extends StatelessWidget {
|
|||||||
child: CardImage(
|
child: CardImage(
|
||||||
image: Stack(
|
image: Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
if (imageUrl != null)
|
||||||
width: 150 * scale,
|
Container(
|
||||||
height: 150 * scale,
|
width: 150 * scale,
|
||||||
decoration: BoxDecoration(
|
height: 150 * scale,
|
||||||
borderRadius: context.theme.borderRadiusMd,
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
image: UniversalImage.imageProvider(imageUrl),
|
image: DecorationImage(
|
||||||
fit: BoxFit.cover,
|
image: UniversalImage.imageProvider(imageUrl!),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
width: 150 * scale,
|
||||||
|
height: 150 * scale,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
|
child: image!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
StatedWidget.builder(
|
StatedWidget.builder(
|
||||||
builder: (context, states) {
|
builder: (context, states) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
|
@ -11,14 +11,14 @@ class PlaybuttonTile extends StatelessWidget {
|
|||||||
final void Function()? onAddToQueuePressed;
|
final void Function()? onAddToQueuePressed;
|
||||||
final String? description;
|
final String? description;
|
||||||
|
|
||||||
final String imageUrl;
|
final String? imageUrl;
|
||||||
|
final Widget? image;
|
||||||
final bool isPlaying;
|
final bool isPlaying;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String title;
|
final String title;
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
|
|
||||||
const PlaybuttonTile({
|
const PlaybuttonTile({
|
||||||
required this.imageUrl,
|
|
||||||
required this.isPlaying,
|
required this.isPlaying,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.title,
|
required this.title,
|
||||||
@ -27,8 +27,13 @@ class PlaybuttonTile extends StatelessWidget {
|
|||||||
this.onAddToQueuePressed,
|
this.onAddToQueuePressed,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.isOwner = false,
|
this.isOwner = false,
|
||||||
|
this.imageUrl,
|
||||||
|
this.image,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
}) : assert(
|
||||||
|
imageUrl != null || image != null,
|
||||||
|
"imageUrl and image can't be null at the same time",
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -36,17 +41,26 @@ class PlaybuttonTile extends StatelessWidget {
|
|||||||
final scale = context.theme.scaling;
|
final scale = context.theme.scaling;
|
||||||
|
|
||||||
return Button(
|
return Button(
|
||||||
leading: Container(
|
leading: imageUrl != null
|
||||||
width: 50 * scale,
|
? Container(
|
||||||
height: 50 * scale,
|
width: 50 * scale,
|
||||||
decoration: BoxDecoration(
|
height: 50 * scale,
|
||||||
borderRadius: context.theme.borderRadiusMd,
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
image: UniversalImage.imageProvider(imageUrl),
|
image: DecorationImage(
|
||||||
fit: BoxFit.cover,
|
image: UniversalImage.imageProvider(imageUrl!),
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(
|
||||||
|
width: 50 * scale,
|
||||||
|
height: 50 * scale,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: context.theme.borderRadiusMd,
|
||||||
|
child: image,
|
||||||
|
),
|
||||||
|
),
|
||||||
style: ButtonVariance.ghost.copyWith(
|
style: ButtonVariance.ghost.copyWith(
|
||||||
padding: (context, states, value) {
|
padding: (context, states, value) {
|
||||||
return (ButtonVariance.ghost.padding(context, states) as EdgeInsets)
|
return (ButtonVariance.ghost.padding(context, states) as EdgeInsets)
|
||||||
|
@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/heart_button/heart_button.dart';
|
import 'package:spotube/components/heart_button/heart_button.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
@ -12,6 +14,7 @@ import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/modules/playlist/playlist_create_dialog.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class TrackPresentationTopSection extends HookConsumerWidget {
|
class TrackPresentationTopSection extends HookConsumerWidget {
|
||||||
const TrackPresentationTopSection({super.key});
|
const TrackPresentationTopSection({super.key});
|
||||||
@ -23,6 +26,26 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
|||||||
final scale = context.theme.scaling;
|
final scale = context.theme.scaling;
|
||||||
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
|
final isUserPlaylist = useIsUserPlaylist(ref, options.collectionId);
|
||||||
|
|
||||||
|
final playlistImage = (options.collection is PlaylistSimple &&
|
||||||
|
(options.collection as PlaylistSimple).owner?.displayName ==
|
||||||
|
"Spotify" &&
|
||||||
|
Env.disableSpotifyImages)
|
||||||
|
? ref.watch(playlistImageProvider(options.collectionId))
|
||||||
|
: null;
|
||||||
|
final decorationImage = playlistImage != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: AssetImage(playlistImage.src),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
playlistImage.color,
|
||||||
|
playlistImage.colorBlendMode,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: DecorationImage(
|
||||||
|
image: UniversalImage.imageProvider(options.image),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
|
final imageDimension = mediaQuery.mdAndUp ? 200 : 120;
|
||||||
|
|
||||||
final (:isLoading, :isActive, :onPlay, :onShuffle) =
|
final (:isLoading, :isActive, :onPlay, :onShuffle) =
|
||||||
@ -153,10 +176,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
DecoratedBox(
|
DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: decorationImage,
|
||||||
image: UniversalImage.imageProvider(options.image),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(45),
|
borderRadius: BorderRadius.circular(45),
|
||||||
),
|
),
|
||||||
child: OutlinedContainer(
|
child: OutlinedContainer(
|
||||||
@ -179,11 +199,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
|
|||||||
width: imageDimension * scale,
|
width: imageDimension * scale,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: context.theme.borderRadiusXl,
|
borderRadius: context.theme.borderRadiusXl,
|
||||||
image: DecorationImage(
|
image: decorationImage,
|
||||||
image:
|
|
||||||
UniversalImage.imageProvider(options.image),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
|
@ -29,7 +29,7 @@ class SpotifySectionPlaylist with _$SpotifySectionPlaylist {
|
|||||||
..description = description
|
..description = description
|
||||||
..collaborative = false
|
..collaborative = false
|
||||||
..images = images.map((e) => e.asImage).toList()
|
..images = images.map((e) => e.asImage).toList()
|
||||||
..owner = (User()..displayName = "Spotify")
|
..owner = (User()..displayName = owner)
|
||||||
..uri = uri
|
..uri = uri
|
||||||
..type = "playlist";
|
..type = "playlist";
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/components/image/universal_image.dart';
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:stroke_text/stroke_text.dart';
|
||||||
|
|
||||||
class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
@ -58,15 +61,58 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: theme.borderRadiusSm,
|
borderRadius: theme.borderRadiusSm,
|
||||||
child: UniversalImage(
|
child: playlist.owner?.displayName == "Spotify" &&
|
||||||
path: (playlist.images)!.asUrlString(
|
Env.disableSpotifyImages
|
||||||
placeholder: ImagePlaceholder.collection,
|
? Consumer(
|
||||||
index: 1,
|
builder: (context, ref, _) {
|
||||||
),
|
final (:src, :color, :colorBlendMode, :placement) =
|
||||||
fit: BoxFit.cover,
|
ref.watch(playlistImageProvider(playlist.id!));
|
||||||
height: 100 * theme.scaling,
|
return SizedBox(
|
||||||
width: 100 * theme.scaling,
|
height: 100 * theme.scaling,
|
||||||
),
|
width: 100 * theme.scaling,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
src,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
top: placement == Alignment.topLeft
|
||||||
|
? 10
|
||||||
|
: null,
|
||||||
|
left: 10,
|
||||||
|
bottom: placement == Alignment.bottomLeft
|
||||||
|
? 10
|
||||||
|
: null,
|
||||||
|
child: StrokeText(
|
||||||
|
text: playlist.name!,
|
||||||
|
strokeColor: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
textColor: Colors.black,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: UniversalImage(
|
||||||
|
path: (playlist.images)!.asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
index: 1,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
height: 100 * theme.scaling,
|
||||||
|
width: 100 * theme.scaling,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
playlist.name!,
|
playlist.name!,
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Offset, Image;
|
||||||
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
|
import 'package:spotube/components/image/universal_image.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
@ -16,6 +18,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
|
|||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
import 'package:stroke_text/stroke_text.dart';
|
||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
@ -168,11 +171,52 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final isOwner = playlist.owner?.id == me.asData?.value.id &&
|
final isOwner = playlist.owner?.id == me.asData?.value.id &&
|
||||||
me.asData?.value.id != null;
|
me.asData?.value.id != null;
|
||||||
|
|
||||||
|
final image =
|
||||||
|
playlist.owner?.displayName == "Spotify" && Env.disableSpotifyImages
|
||||||
|
? Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final (:color, :colorBlendMode, :src, :placement) =
|
||||||
|
ref.watch(playlistImageProvider(playlist.id!));
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
src,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
top: placement == Alignment.topLeft ? 10 : null,
|
||||||
|
left: 10,
|
||||||
|
bottom: placement == Alignment.bottomLeft ? 10 : null,
|
||||||
|
child: StrokeText(
|
||||||
|
text: playlist.name!,
|
||||||
|
strokeColor: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
textColor: Colors.black,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: UniversalImage(
|
||||||
|
path: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
|
||||||
if (_isTile) {
|
if (_isTile) {
|
||||||
return PlaybuttonTile(
|
return PlaybuttonTile(
|
||||||
title: playlist.name!,
|
title: playlist.name!,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
imageUrl: imageUrl,
|
image: image,
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
@ -185,7 +229,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
title: playlist.name!,
|
title: playlist.name!,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
imageUrl: imageUrl,
|
image: image,
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
|
@ -104,3 +104,39 @@ final playlistProvider =
|
|||||||
AsyncNotifierProvider.family<PlaylistNotifier, Playlist, String>(
|
AsyncNotifierProvider.family<PlaylistNotifier, Playlist, String>(
|
||||||
() => PlaylistNotifier(),
|
() => PlaylistNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final _blendModes = BlendMode.values
|
||||||
|
.where((e) => switch (e) {
|
||||||
|
BlendMode.clear ||
|
||||||
|
BlendMode.src ||
|
||||||
|
BlendMode.srcATop ||
|
||||||
|
BlendMode.srcIn ||
|
||||||
|
BlendMode.srcOut ||
|
||||||
|
BlendMode.srcOver ||
|
||||||
|
BlendMode.dstOut ||
|
||||||
|
BlendMode.xor =>
|
||||||
|
false,
|
||||||
|
_ => true
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
typedef PlaylistImageInfo = ({
|
||||||
|
Color color,
|
||||||
|
BlendMode colorBlendMode,
|
||||||
|
String src,
|
||||||
|
Alignment placement,
|
||||||
|
});
|
||||||
|
|
||||||
|
final playlistImageProvider = Provider.family<PlaylistImageInfo, String>(
|
||||||
|
(ref, playlistId) {
|
||||||
|
final random = Random();
|
||||||
|
|
||||||
|
return (
|
||||||
|
color: Colors.primaries[random.nextInt(Colors.primaries.length)],
|
||||||
|
colorBlendMode: _blendModes[random.nextInt(_blendModes.length)],
|
||||||
|
src: Assets
|
||||||
|
.patterns.values[random.nextInt(Assets.patterns.values.length)].path,
|
||||||
|
placement: random.nextBool() ? Alignment.topLeft : Alignment.bottomLeft,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
library spotify;
|
library spotify;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
import 'package:spotube/provider/authentication/authentication.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
@ -172,6 +172,7 @@ flutter:
|
|||||||
- assets/tutorial/
|
- assets/tutorial/
|
||||||
- assets/logos/
|
- assets/logos/
|
||||||
- assets/backgrounds/
|
- assets/backgrounds/
|
||||||
|
- assets/patterns/
|
||||||
- LICENSE
|
- LICENSE
|
||||||
fonts:
|
fonts:
|
||||||
- family: GeistSans
|
- family: GeistSans
|
||||||
|