mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
cover art on track listings
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Pressable, StyleSheet } from 'react-native';
|
||||
import { Image } from 'expo-image';
|
||||
import { Text } from '@/components/Text';
|
||||
import { AstraLogo } from '@/components/AstraLogo';
|
||||
import { FormatBadges } from '@/components/FormatBadge';
|
||||
import { colors, spacing } from '@/theme';
|
||||
import { colors, radius, spacing } from '@/theme';
|
||||
import { formatDuration } from '@/lib/format';
|
||||
import { artworkThumbUri } from '@/library/artwork';
|
||||
import type { DbTrack } from '@/types/library';
|
||||
|
||||
const ART_SIZE = 44;
|
||||
const ROW_MIN_HEIGHT = ART_SIZE + (spacing.sm + 2) * 2;
|
||||
|
||||
export function TrackRow({
|
||||
track,
|
||||
onPress,
|
||||
@@ -20,6 +27,12 @@ export function TrackRow({
|
||||
showArtist?: boolean;
|
||||
active?: boolean;
|
||||
}) {
|
||||
const artworkHash = track.artwork_hash;
|
||||
const [failedArtworkHash, setFailedArtworkHash] = useState<string | null>(null);
|
||||
|
||||
const thumbUri =
|
||||
artworkHash && failedArtworkHash !== artworkHash ? artworkThumbUri(artworkHash) : null;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.row}
|
||||
@@ -27,9 +40,26 @@ export function TrackRow({
|
||||
onLongPress={onLongPress}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
{track.track_number != null && !showArtist ? (
|
||||
<View style={styles.art}>
|
||||
{thumbUri ? (
|
||||
<Image
|
||||
source={{ uri: thumbUri }}
|
||||
style={styles.artImage}
|
||||
contentFit="cover"
|
||||
cachePolicy="memory-disk"
|
||||
recyclingKey={artworkHash}
|
||||
transition={null}
|
||||
allowDownscaling
|
||||
onError={() => setFailedArtworkHash(artworkHash)}
|
||||
/>
|
||||
) : (
|
||||
<AstraLogo size={18} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!showArtist ? (
|
||||
<Text variant="mono" style={styles.trackNumber}>
|
||||
{track.track_number}
|
||||
{track.track_number ?? ''}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
@@ -68,19 +98,38 @@ const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
minHeight: ROW_MIN_HEIGHT,
|
||||
paddingVertical: spacing.sm + 2,
|
||||
gap: spacing.md,
|
||||
borderBottomColor: colors.glassBorder,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
art: {
|
||||
width: ART_SIZE,
|
||||
height: ART_SIZE,
|
||||
flexShrink: 0,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.bgTertiary,
|
||||
borderColor: colors.glassBorder,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
artImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
trackNumber: {
|
||||
width: 24,
|
||||
flexShrink: 0,
|
||||
fontSize: 12,
|
||||
color: colors.textTertiary,
|
||||
textAlign: 'right',
|
||||
},
|
||||
meta: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
gap: 2,
|
||||
},
|
||||
title: {
|
||||
@@ -93,7 +142,10 @@ const styles = StyleSheet.create({
|
||||
marginTop: 2,
|
||||
},
|
||||
duration: {
|
||||
minWidth: 42,
|
||||
flexShrink: 0,
|
||||
fontSize: 12,
|
||||
color: colors.textTertiary,
|
||||
textAlign: 'right',
|
||||
},
|
||||
});
|
||||
|
||||
+49
-2
@@ -4,10 +4,57 @@
|
||||
import { AstraLibraryScanner } from '../../modules/astra-library-scanner';
|
||||
|
||||
let artworkDir: string | null = null;
|
||||
let artworkThumbDir: string | null = null;
|
||||
|
||||
export function artworkUri(hash: string): string {
|
||||
type ArtworkThumbScanner = {
|
||||
getArtworkThumbDirPath?: () => string;
|
||||
ensureArtworkThumbnails?: (hashes: string[]) => Promise<number>;
|
||||
};
|
||||
|
||||
function getArtworkDir(): string {
|
||||
if (!artworkDir) {
|
||||
artworkDir = AstraLibraryScanner.getArtworkDirPath();
|
||||
}
|
||||
return `file://${artworkDir}/${hash}`;
|
||||
return artworkDir;
|
||||
}
|
||||
|
||||
function fallbackArtworkThumbDir(): string {
|
||||
const dir = getArtworkDir();
|
||||
return dir.endsWith('/artwork') ? `${dir.slice(0, -'/artwork'.length)}/artwork-thumbs` : `${dir}-thumbs`;
|
||||
}
|
||||
|
||||
function getArtworkThumbDir(): string {
|
||||
if (!artworkThumbDir) {
|
||||
const scanner = AstraLibraryScanner as unknown as ArtworkThumbScanner;
|
||||
artworkThumbDir = scanner.getArtworkThumbDirPath?.() ?? fallbackArtworkThumbDir();
|
||||
}
|
||||
return artworkThumbDir;
|
||||
}
|
||||
|
||||
function artworkThumbFileName(hash: string): string {
|
||||
const dot = hash.lastIndexOf('.');
|
||||
const stem = dot > 0 ? hash.slice(0, dot) : hash;
|
||||
return `${stem}.jpg`;
|
||||
}
|
||||
|
||||
export function artworkUri(hash: string): string {
|
||||
return `file://${getArtworkDir()}/${hash}`;
|
||||
}
|
||||
|
||||
export function artworkThumbUri(hash: string): string {
|
||||
return `file://${getArtworkThumbDir()}/${artworkThumbFileName(hash)}`;
|
||||
}
|
||||
|
||||
export async function ensureArtworkThumbnails(
|
||||
hashes: readonly (string | null | undefined)[]
|
||||
): Promise<number> {
|
||||
const unique = new Set<string>();
|
||||
for (const hash of hashes) {
|
||||
const cleanHash = hash?.trim();
|
||||
if (cleanHash) unique.add(cleanHash);
|
||||
}
|
||||
if (unique.size === 0) return 0;
|
||||
|
||||
const scanner = AstraLibraryScanner as unknown as ArtworkThumbScanner;
|
||||
return scanner.ensureArtworkThumbnails?.([...unique]) ?? 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||
import type { Album, Artist, DbTrack, LibraryFolder } from '@/types/library';
|
||||
import { openLibraryDb } from '@/db/database';
|
||||
import { getAlbums, getAllTracks, getTrackCount } from '@/db/queries';
|
||||
import { ensureArtworkThumbnails } from '@/library/artwork';
|
||||
import { buildArtistList } from '@/library/artistGrouping';
|
||||
import {
|
||||
addFolderViaPicker,
|
||||
@@ -123,6 +124,11 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
loadFolders(),
|
||||
getTrackCount(db),
|
||||
]);
|
||||
try {
|
||||
await ensureArtworkThumbnails(tracks.map((track) => track.artwork_hash));
|
||||
} catch {
|
||||
// Missing thumbnails should not prevent the library itself from loading.
|
||||
}
|
||||
// The artist list is derived in JS so it can honor the grouping mode.
|
||||
const artists = buildArtistList(tracks, useSettingsStore.getState().artistGroupingMode);
|
||||
set({ tracks, albums, artists, folders, totalTrackCount });
|
||||
|
||||
Reference in New Issue
Block a user