mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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: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' hide Consumer;
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
@ -71,6 +71,13 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
|
|
||||||
final isSelected = isPlaying || isLoading.value;
|
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 LayoutBuilder(builder: (context, constrains) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (event) {
|
onPointerDown: (event) {
|
||||||
@ -147,11 +154,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
borderRadius: theme.borderRadiusMd,
|
borderRadius: theme.borderRadiusMd,
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
image: UniversalImage.imageProvider(
|
image: imageProvider,
|
||||||
(track.album.images).asUrlString(
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -19,25 +19,65 @@ enum ImagePlaceholder {
|
|||||||
online,
|
online,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SpotubeImageExtensions on List<SpotubeImageObject>? {
|
final placeholderUrlMap = {
|
||||||
String asUrlString({
|
|
||||||
int index = 1,
|
|
||||||
required ImagePlaceholder placeholder,
|
|
||||||
}) {
|
|
||||||
final String placeholderUrl = {
|
|
||||||
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
|
ImagePlaceholder.albumArt: Assets.albumPlaceholder.path,
|
||||||
ImagePlaceholder.artist: Assets.userPlaceholder.path,
|
ImagePlaceholder.artist: Assets.userPlaceholder.path,
|
||||||
ImagePlaceholder.collection: Assets.placeholder.path,
|
ImagePlaceholder.collection: Assets.placeholder.path,
|
||||||
ImagePlaceholder.online:
|
ImagePlaceholder.online:
|
||||||
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
"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!));
|
final sortedImage = this?.sorted((a, b) => a.width!.compareTo(b.width!));
|
||||||
|
|
||||||
return sortedImage != null && sortedImage.isNotEmpty
|
return sortedImage != null && sortedImage.isNotEmpty
|
||||||
? sortedImage[
|
? sortedImage[
|
||||||
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
index > sortedImage.length - 1 ? sortedImage.length - 1 : index]
|
||||||
.url
|
.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;
|
: placeholderUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,32 +42,36 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
final historyNotifier = ref.read(playbackHistoryActionsProvider);
|
||||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
final isPlaylistPlaying = useMemoized<bool>(
|
||||||
() => playlist.containsCollection(album.id),
|
() => playlist.containsCollection(album.id),
|
||||||
[playlist, album.id],
|
[playlist, album.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
|
|
||||||
Future<List<SpotubeFullTrackObject>> fetchAllTrack() async {
|
final fetchAllTrack = useCallback(() async {
|
||||||
await ref.read(metadataPluginAlbumTracksProvider(album.id).future);
|
await ref.read(metadataPluginAlbumTracksProvider(album.id).future);
|
||||||
return ref
|
return ref
|
||||||
.read(metadataPluginAlbumTracksProvider(album.id).notifier)
|
.read(metadataPluginAlbumTracksProvider(album.id).notifier)
|
||||||
.fetchAll();
|
.fetchAll();
|
||||||
}
|
}, [album.id, ref]);
|
||||||
|
|
||||||
var imageUrl = album.images.asUrlString(
|
final imageUrl = useMemoized(
|
||||||
placeholder: ImagePlaceholder.collection,
|
() => album.images.from200PxTo300PxOrSmallestImage(
|
||||||
|
ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
|
[album.images],
|
||||||
);
|
);
|
||||||
var isLoading =
|
|
||||||
|
final isLoading =
|
||||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
(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.navigateTo(AlbumRoute(id: album.id, album: album));
|
||||||
}
|
}, [context, album]);
|
||||||
|
|
||||||
void onPlaybuttonPressed() async {
|
final onPlaybuttonPressed = useCallback(() async {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying) {
|
if (isPlaylistPlaying) {
|
||||||
@ -96,9 +100,20 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
}
|
}, [
|
||||||
|
isPlaylistPlaying,
|
||||||
|
playing,
|
||||||
|
audioPlayer,
|
||||||
|
fetchAllTrack,
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
playlistNotifier,
|
||||||
|
album,
|
||||||
|
historyNotifier,
|
||||||
|
updating
|
||||||
|
]);
|
||||||
|
|
||||||
void onAddToQueuePressed() async {
|
final onAddToQueuePressed = useCallback(() async {
|
||||||
if (isPlaylistPlaying) {
|
if (isPlaylistPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -135,7 +150,16 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
}
|
}, [
|
||||||
|
isPlaylistPlaying,
|
||||||
|
updating.value,
|
||||||
|
fetchAllTrack,
|
||||||
|
playlistNotifier,
|
||||||
|
album.id,
|
||||||
|
historyNotifier,
|
||||||
|
album,
|
||||||
|
context
|
||||||
|
]);
|
||||||
|
|
||||||
if (_isTile) {
|
if (_isTile) {
|
||||||
return PlaybuttonTile(
|
return PlaybuttonTile(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
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' hide Consumer;
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
import 'package:spotube/components/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
import 'package:spotube/components/playbutton_view/playbutton_card.dart';
|
||||||
@ -41,7 +41,8 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
bool isPlaylistPlaying = useMemoized(
|
|
||||||
|
final isPlaylistPlaying = useMemoized<bool>(
|
||||||
() => playlistQueue.containsCollection(playlist.id),
|
() => playlistQueue.containsCollection(playlist.id),
|
||||||
[playlistQueue, playlist.id],
|
[playlistQueue, playlist.id],
|
||||||
);
|
);
|
||||||
@ -49,7 +50,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final me = ref.watch(metadataPluginUserProvider);
|
final me = ref.watch(metadataPluginUserProvider);
|
||||||
|
|
||||||
Future<List<SpotubeTrackObject>> fetchInitialTracks() async {
|
final fetchInitialTracks = useCallback(() async {
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
final tracks = await ref.read(metadataPluginSavedTracksProvider.future);
|
final tracks = await ref.read(metadataPluginSavedTracksProvider.future);
|
||||||
return tracks.items;
|
return tracks.items;
|
||||||
@ -59,9 +60,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
.read(metadataPluginPlaylistTracksProvider(playlist.id).future);
|
.read(metadataPluginPlaylistTracksProvider(playlist.id).future);
|
||||||
|
|
||||||
return result.items;
|
return result.items;
|
||||||
}
|
}, [playlist.id, ref]);
|
||||||
|
|
||||||
Future<List<SpotubeTrackObject>> fetchAllTracks() async {
|
final fetchAllTracks = useCallback(() async {
|
||||||
await fetchInitialTracks();
|
await fetchInitialTracks();
|
||||||
|
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
@ -71,13 +72,13 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return ref
|
return ref
|
||||||
.read(metadataPluginPlaylistTracksProvider(playlist.id).notifier)
|
.read(metadataPluginPlaylistTracksProvider(playlist.id).notifier)
|
||||||
.fetchAll();
|
.fetchAll();
|
||||||
}
|
}, [playlist.id, ref, fetchInitialTracks]);
|
||||||
|
|
||||||
void onTap() {
|
final onTap = useCallback(() {
|
||||||
context.navigateTo(PlaylistRoute(id: playlist.id, playlist: playlist));
|
context.navigateTo(PlaylistRoute(id: playlist.id, playlist: playlist));
|
||||||
}
|
}, [context, playlist]);
|
||||||
|
|
||||||
void onPlaybuttonPressed() async {
|
final onPlaybuttonPressed = useCallback(() async {
|
||||||
try {
|
try {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
if (isPlaylistPlaying && playing) {
|
if (isPlaylistPlaying && playing) {
|
||||||
@ -97,7 +98,7 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
final allTracks = await fetchAllTracks();
|
final allTracks = await fetchAllTracks();
|
||||||
await remotePlayback.load(
|
await remotePlayback.load(
|
||||||
WebSocketLoadEventData.playlist(
|
WebSocketLoadEventData.playlist(
|
||||||
tracks: allTracks as List<SpotubeFullTrackObject>,
|
tracks: allTracks,
|
||||||
collection: playlist,
|
collection: playlist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -116,9 +117,23 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
updating.value = false;
|
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;
|
updating.value = true;
|
||||||
try {
|
try {
|
||||||
if (isPlaylistPlaying) return;
|
if (isPlaylistPlaying) return;
|
||||||
@ -155,11 +170,24 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
}
|
}, [
|
||||||
|
isPlaylistPlaying,
|
||||||
|
fetchAllTracks,
|
||||||
|
playlistNotifier,
|
||||||
|
playlist.id,
|
||||||
|
historyNotifier,
|
||||||
|
playlist,
|
||||||
|
context,
|
||||||
|
updating
|
||||||
|
]);
|
||||||
|
|
||||||
final imageUrl = playlist.images.asUrlString(
|
final imageUrl = useMemoized(
|
||||||
placeholder: ImagePlaceholder.collection,
|
() => playlist.images.from200PxTo300PxOrSmallestImage(
|
||||||
|
ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
|
[playlist.images],
|
||||||
);
|
);
|
||||||
|
|
||||||
final isLoading =
|
final isLoading =
|
||||||
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
(isPlaylistPlaying && isFetchingActiveTrack) || updating.value;
|
||||||
final isOwner = playlist.owner.id == me.asData?.value?.id &&
|
final isOwner = playlist.owner.id == me.asData?.value?.id &&
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
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' hide Consumer;
|
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:spotube/collections/assets.gen.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: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:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_check_yt_dlp_installed.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/bottom_player.dart';
|
||||||
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
||||||
@ -43,15 +44,23 @@ class RootAppPage extends HookConsumerWidget {
|
|||||||
final scaffold = MediaQuery.removeViewInsets(
|
final scaffold = MediaQuery.removeViewInsets(
|
||||||
context: context,
|
context: context,
|
||||||
removeBottom: true,
|
removeBottom: true,
|
||||||
child: const SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
footers: [
|
footers: const [
|
||||||
BottomPlayer(),
|
BottomPlayer(),
|
||||||
SpotubeNavigationBar(),
|
SpotubeNavigationBar(),
|
||||||
],
|
],
|
||||||
floatingFooter: true,
|
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