mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: enhance image handling
This commit is contained in:
parent
f23a078b64
commit
90f9cc28eb
@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
@ -71,6 +71,13 @@ class TrackTile extends HookConsumerWidget {
|
||||
|
||||
final isSelected = isPlaying || isLoading.value;
|
||||
|
||||
final imageProvider = useMemoized(
|
||||
() => UniversalImage.imageProvider(
|
||||
(track.album.images).smallest(ImagePlaceholder.albumArt),
|
||||
),
|
||||
[track.album.images],
|
||||
);
|
||||
|
||||
return LayoutBuilder(builder: (context, constrains) {
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
@ -147,11 +154,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
borderRadius: theme.borderRadiusMd,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: UniversalImage.imageProvider(
|
||||
(track.album.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
),
|
||||
),
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -19,25 +19,65 @@ enum ImagePlaceholder {
|
||||
online,
|
||||
}
|
||||
|
||||
extension SpotubeImageExtensions on List<SpotubeImageObject>? {
|
||||
String asUrlString({
|
||||
int index = 1,
|
||||
required ImagePlaceholder placeholder,
|
||||
}) {
|
||||
final String placeholderUrl = {
|
||||
final placeholderUrlMap = {
|
||||
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
|
||||
ImagePlaceholder.artist: Assets.userPlaceholder.path,
|
||||
ImagePlaceholder.collection: Assets.placeholder.path,
|
||||
ImagePlaceholder.online:
|
||||
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
||||
}[placeholder]!;
|
||||
};
|
||||
|
||||
extension SpotubeImageExtensions on List<SpotubeImageObject>? {
|
||||
/// Returns the URL of the image at the specified index.
|
||||
String asUrlString({
|
||||
int index = 1,
|
||||
required ImagePlaceholder placeholder,
|
||||
}) {
|
||||
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
|
||||
|
||||
return sortedImage != null && sortedImage.isNotEmpty
|
||||
? sortedImage[
|
||||
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
||||
.url
|
||||
: placeholderUrlMap[placeholder]!;
|
||||
}
|
||||
|
||||
String smallest(ImagePlaceholder placeholder) {
|
||||
final sortedImage = this?.sorted((a, b) {
|
||||
final widthComparison = (a.width ?? 0).compareTo(b.width ?? 0);
|
||||
if (widthComparison != 0) return widthComparison;
|
||||
return (a.height ?? 0).compareTo(b.height ?? 0);
|
||||
});
|
||||
|
||||
return sortedImage != null && sortedImage.isNotEmpty
|
||||
? sortedImage.first.url
|
||||
: placeholderUrlMap[placeholder]!;
|
||||
}
|
||||
|
||||
String from200PxTo300PxOrSmallestImage([
|
||||
ImagePlaceholder placeholder = ImagePlaceholder.albumArt,
|
||||
]) {
|
||||
final placeholderUrl = placeholderUrlMap[placeholder]!;
|
||||
|
||||
// Sort images by width and height to find the smallest one
|
||||
final sortedImage = this?.sorted((a, b) {
|
||||
final widthComparison = (a.width ?? 0).compareTo(b.width ?? 0);
|
||||
if (widthComparison != 0) return widthComparison;
|
||||
return (a.height ?? 0).compareTo(b.height ?? 0);
|
||||
});
|
||||
|
||||
return sortedImage != null && sortedImage.isNotEmpty
|
||||
? sortedImage.firstWhere(
|
||||
(image) {
|
||||
final width = image.width ?? 0;
|
||||
final height = image.height ?? 0;
|
||||
return width >= 200 &&
|
||||
height >= 200 &&
|
||||
width <= 300 &&
|
||||
height <= 300;
|
||||
},
|
||||
orElse: () => sortedImage.first,
|
||||
).url
|
||||
: placeholderUrl;
|
||||
}
|
||||
}
|
||||
|
@ -42,32 +42,36 @@ class AlbumCard extends HookConsumerWidget {
|
||||
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||
|
||||
bool isPlaylistPlaying = useMemoized(
|
||||
final isPlaylistPlaying = useMemoized<bool>(
|
||||
() => playlist.containsCollection(album.id),
|
||||
[playlist, album.id],
|
||||
);
|
||||
|
||||
final updating = useState(false);
|
||||
|
||||
Future<List<SpotubeFullTrackObject>> fetchAllTrack() async {
|
||||
final fetchAllTrack = useCallback(() async {
|
||||
await ref.read(metadataPluginAlbumTracksProvider(album.id).future);
|
||||
return ref
|
||||
.read(metadataPluginAlbumTracksProvider(album.id).notifier)
|
||||
.fetchAll();
|
||||
}
|
||||
}, [album.id, ref]);
|
||||
|
||||
var imageUrl = album.images.asUrlString(
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
final imageUrl = useMemoized(
|
||||
() => album.images.from200PxTo300PxOrSmallestImage(
|
||||
ImagePlaceholder.collection,
|
||||
),
|
||||
[album.images],
|
||||
);
|
||||
var isLoading =
|
||||
|
||||
final isLoading =
|
||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||
var description = "${album.albumType.name} • ${album.artists.asString()}";
|
||||
final description = "${album.albumType.name} • ${album.artists.asString()}";
|
||||
|
||||
void onTap() {
|
||||
final onTap = useCallback(() {
|
||||
context.navigateTo(AlbumRoute(id: album.id, album: album));
|
||||
}
|
||||
}, [context, album]);
|
||||
|
||||
void onPlaybuttonPressed() async {
|
||||
final onPlaybuttonPressed = useCallback(() async {
|
||||
updating.value = true;
|
||||
try {
|
||||
if (isPlaylistPlaying) {
|
||||
@ -96,9 +100,20 @@ class AlbumCard extends HookConsumerWidget {
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isPlaylistPlaying,
|
||||
playing,
|
||||
audioPlayer,
|
||||
fetchAllTrack,
|
||||
context,
|
||||
ref,
|
||||
playlistNotifier,
|
||||
album,
|
||||
historyNotifier,
|
||||
updating
|
||||
]);
|
||||
|
||||
void onAddToQueuePressed() async {
|
||||
final onAddToQueuePressed = useCallback(() async {
|
||||
if (isPlaylistPlaying) {
|
||||
return;
|
||||
}
|
||||
@ -135,7 +150,16 @@ class AlbumCard extends HookConsumerWidget {
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isPlaylistPlaying,
|
||||
updating.value,
|
||||
fetchAllTrack,
|
||||
playlistNotifier,
|
||||
album.id,
|
||||
historyNotifier,
|
||||
album,
|
||||
context
|
||||
]);
|
||||
|
||||
if (_isTile) {
|
||||
return PlaybuttonTile(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/collections/routes.gr.dart';
|
||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||
@ -41,7 +41,8 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
|
||||
final playing =
|
||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||
bool isPlaylistPlaying = useMemoized(
|
||||
|
||||
final isPlaylistPlaying = useMemoized<bool>(
|
||||
() => playlistQueue.containsCollection(playlist.id),
|
||||
[playlistQueue, playlist.id],
|
||||
);
|
||||
@ -49,7 +50,7 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
final updating = useState(false);
|
||||
final me = ref.watch(metadataPluginUserProvider);
|
||||
|
||||
Future<List<SpotubeTrackObject>> fetchInitialTracks() async {
|
||||
final fetchInitialTracks = useCallback(() async {
|
||||
if (playlist.id == 'user-liked-tracks') {
|
||||
final tracks = await ref.read(metadataPluginSavedTracksProvider.future);
|
||||
return tracks.items;
|
||||
@ -59,9 +60,9 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
.read(metadataPluginPlaylistTracksProvider(playlist.id).future);
|
||||
|
||||
return result.items;
|
||||
}
|
||||
}, [playlist.id, ref]);
|
||||
|
||||
Future<List<SpotubeTrackObject>> fetchAllTracks() async {
|
||||
final fetchAllTracks = useCallback(() async {
|
||||
await fetchInitialTracks();
|
||||
|
||||
if (playlist.id == 'user-liked-tracks') {
|
||||
@ -71,13 +72,13 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
return ref
|
||||
.read(metadataPluginPlaylistTracksProvider(playlist.id).notifier)
|
||||
.fetchAll();
|
||||
}
|
||||
}, [playlist.id, ref, fetchInitialTracks]);
|
||||
|
||||
void onTap() {
|
||||
final onTap = useCallback(() {
|
||||
context.navigateTo(PlaylistRoute(id: playlist.id, playlist: playlist));
|
||||
}
|
||||
}, [context, playlist]);
|
||||
|
||||
void onPlaybuttonPressed() async {
|
||||
final onPlaybuttonPressed = useCallback(() async {
|
||||
try {
|
||||
updating.value = true;
|
||||
if (isPlaylistPlaying && playing) {
|
||||
@ -97,7 +98,7 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
final allTracks = await fetchAllTracks();
|
||||
await remotePlayback.load(
|
||||
WebSocketLoadEventData.playlist(
|
||||
tracks: allTracks as List<SpotubeFullTrackObject>,
|
||||
tracks: allTracks,
|
||||
collection: playlist,
|
||||
),
|
||||
);
|
||||
@ -116,9 +117,23 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isPlaylistPlaying,
|
||||
playing,
|
||||
fetchInitialTracks,
|
||||
context,
|
||||
showSelectDeviceDialog,
|
||||
ref,
|
||||
connectProvider,
|
||||
fetchAllTracks,
|
||||
playlistNotifier,
|
||||
playlist.id,
|
||||
historyNotifier,
|
||||
playlist,
|
||||
updating
|
||||
]);
|
||||
|
||||
void onAddToQueuePressed() async {
|
||||
final onAddToQueuePressed = useCallback(() async {
|
||||
updating.value = true;
|
||||
try {
|
||||
if (isPlaylistPlaying) return;
|
||||
@ -155,11 +170,24 @@ class PlaylistCard extends HookConsumerWidget {
|
||||
} finally {
|
||||
updating.value = false;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isPlaylistPlaying,
|
||||
fetchAllTracks,
|
||||
playlistNotifier,
|
||||
playlist.id,
|
||||
historyNotifier,
|
||||
playlist,
|
||||
context,
|
||||
updating
|
||||
]);
|
||||
|
||||
final imageUrl = playlist.images.asUrlString(
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
final imageUrl = useMemoized(
|
||||
() => playlist.images.from200PxTo300PxOrSmallestImage(
|
||||
ImagePlaceholder.collection,
|
||||
),
|
||||
[playlist.images],
|
||||
);
|
||||
|
||||
final isLoading =
|
||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||
final isOwner = playlist.owner.id == me.asData?.value?.id &&
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
|
@ -85,7 +85,7 @@ class HomePage extends HookConsumerWidget {
|
||||
};
|
||||
},
|
||||
),
|
||||
const HomePageBrowseSection(),
|
||||
const SliverSafeArea(sliver: HomePageBrowseSection()),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/hooks/configurators/use_check_yt_dlp_installed.dart';
|
||||
import 'package:spotube/modules/root/bottom_player.dart';
|
||||
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
||||
@ -43,15 +44,23 @@ class RootAppPage extends HookConsumerWidget {
|
||||
final scaffold = MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
removeBottom: true,
|
||||
child: const SafeArea(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Scaffold(
|
||||
footers: [
|
||||
footers: const [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(),
|
||||
],
|
||||
floatingFooter: true,
|
||||
child: Sidebar(child: AutoRouter()),
|
||||
child: Sidebar(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: MediaQuery.paddingOf(context)
|
||||
.copyWith(bottom: 100 * context.theme.scaling),
|
||||
),
|
||||
child: const AutoRouter(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user