feat: flag to hide spotify generated images with patterns

This commit is contained in:
Kingkor Roy Tirtho 2025-01-10 23:41:22 +06:00
parent 5a14f587a0
commit b25ae332b8
25 changed files with 307 additions and 49 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -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 =

View File

@ -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;

View File

@ -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,16 +45,26 @@ class PlaybuttonCard extends StatelessWidget {
child: CardImage( child: CardImage(
image: Stack( image: Stack(
children: [ children: [
if (imageUrl != null)
Container( Container(
width: 150 * scale, width: 150 * scale,
height: 150 * scale, height: 150 * scale,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: context.theme.borderRadiusMd, borderRadius: context.theme.borderRadiusMd,
image: DecorationImage( image: DecorationImage(
image: UniversalImage.imageProvider(imageUrl), image: UniversalImage.imageProvider(imageUrl!),
fit: BoxFit.cover, 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) {

View File

@ -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,16 +41,25 @@ class PlaybuttonTile extends StatelessWidget {
final scale = context.theme.scaling; final scale = context.theme.scaling;
return Button( return Button(
leading: Container( leading: imageUrl != null
? Container(
width: 50 * scale, width: 50 * scale,
height: 50 * scale, height: 50 * scale,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: context.theme.borderRadiusMd, borderRadius: context.theme.borderRadiusMd,
image: DecorationImage( image: DecorationImage(
image: UniversalImage.imageProvider(imageUrl), image: UniversalImage.imageProvider(imageUrl!),
fit: BoxFit.cover, 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) {

View File

@ -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(

View File

@ -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";
} }

View File

@ -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,7 +61,50 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: theme.borderRadiusSm, borderRadius: theme.borderRadiusSm,
child: UniversalImage( child: playlist.owner?.displayName == "Spotify" &&
Env.disableSpotifyImages
? Consumer(
builder: (context, ref, _) {
final (:src, :color, :colorBlendMode, :placement) =
ref.watch(playlistImageProvider(playlist.id!));
return SizedBox(
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( path: (playlist.images)!.asUrlString(
placeholder: ImagePlaceholder.collection, placeholder: ImagePlaceholder.collection,
index: 1, index: 1,

View File

@ -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,

View File

@ -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,
);
},
);

View File

@ -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';

View File

@ -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