m5, subsonic/jellyfin support

This commit is contained in:
Boof2015
2026-06-27 22:03:37 -04:00
parent 0e3e46293c
commit 3ec3f5ecf2
37 changed files with 3322 additions and 35 deletions
+4 -3
View File
@@ -3,16 +3,17 @@ import { Image } from 'expo-image';
import { Text } from '@/components/Text';
import { AstraLogo } from '@/components/AstraLogo';
import { colors, radius, spacing } from '@/theme';
import { artworkUri } from '@/library/artwork';
import { albumArtworkSource } from '@/library/artwork';
import type { Album } from '@/types/library';
export function AlbumGridItem({ album, onPress }: { album: Album; onPress: () => void }) {
const artUri = albumArtworkSource(album);
return (
<Pressable style={styles.item} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{album.artwork_hash ? (
{artUri ? (
<Image
source={{ uri: artworkUri(album.artwork_hash) }}
source={{ uri: artUri }}
style={styles.artImage}
contentFit="cover"
transition={120}
+4 -3
View File
@@ -3,17 +3,18 @@ import { Image } from 'expo-image';
import { Ionicons } from '@expo/vector-icons';
import { Text } from '@/components/Text';
import { colors, radius, spacing } from '@/theme';
import { artworkUri } from '@/library/artwork';
import { albumArtworkSource } from '@/library/artwork';
import type { Album } from '@/types/library';
/** Compact album list row (search results) — the grid uses AlbumGridItem. */
export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void }) {
const artUri = albumArtworkSource(album);
return (
<Pressable style={styles.row} onPress={onPress} accessibilityRole="button">
<View style={styles.art}>
{album.artwork_hash ? (
{artUri ? (
<Image
source={{ uri: artworkUri(album.artwork_hash) }}
source={{ uri: artUri }}
style={styles.artImage}
contentFit="cover"
/>
+17 -3
View File
@@ -11,6 +11,7 @@ export function PlaylistRow({
missingCount = 0,
coverHash,
pinned = false,
remote = false,
onPress,
onLongPress,
}: {
@@ -20,6 +21,8 @@ export function PlaylistRow({
coverHash: string | null;
/** Favorites pseudo-playlist: heart cover instead of artwork/logo. */
pinned?: boolean;
/** Synced from a remote server — shows a cloud marker. */
remote?: boolean;
onPress: () => void;
onLongPress?: () => void;
}) {
@@ -47,9 +50,12 @@ export function PlaylistRow({
)}
</View>
<View style={styles.meta}>
<Text variant="body" numberOfLines={1}>
{name}
</Text>
<View style={styles.titleRow}>
<Text variant="body" numberOfLines={1} style={styles.title}>
{name}
</Text>
{remote ? <Ionicons name="cloud" size={12} color={colors.accent} /> : null}
</View>
<Text variant="label" numberOfLines={1}>
{`${trackCount} ${trackCount === 1 ? 'track' : 'tracks'}`}
{missingCount > 0 ? (
@@ -92,4 +98,12 @@ const styles = StyleSheet.create({
flex: 1,
gap: 2,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
title: {
flexShrink: 1,
},
});
+1
View File
@@ -141,6 +141,7 @@ export function PlaylistsView() {
trackCount={item.track_count}
missingCount={item.missing_track_count}
coverHash={item.auto_cover_hash}
remote={item.remote_source_id != null}
onPress={() => router.push(`/library/playlist/${item.id}`)}
onLongPress={() => setMenuFor(item)}
/>
+13 -7
View File
@@ -4,10 +4,11 @@ import { Image } from 'expo-image';
import { Text } from '@/components/Text';
import { AstraLogo } from '@/components/AstraLogo';
import { FormatBadges } from '@/components/FormatBadge';
import { RemoteSourceBadge } from '@/components/RemoteSourceBadge';
import { SwipeableRow } from '@/components/SwipeableRow';
import { colors, radius, spacing } from '@/theme';
import { formatDuration } from '@/lib/format';
import { artworkThumbUri } from '@/library/artwork';
import { trackArtworkThumbSource } from '@/library/artwork';
import { dbTrackToTrack } from '@/library/trackAdapter';
import { enqueueEnd, enqueueTop } from '@/audio/playbackController';
import type { DbTrack } from '@/types/library';
@@ -36,11 +37,12 @@ export function TrackRow({
/** Swipe right → play next, swipe left → add to queue. Off in queue-like lists. */
swipeToQueue?: boolean;
}) {
const artworkHash = track.artwork_hash;
const [failedArtworkHash, setFailedArtworkHash] = useState<string | null>(null);
// Key the artwork by hash (local) or identity path (remote) so the error fallback
// and FlashList recycling work for both.
const artKey = track.source_type !== 'local' ? track.path : track.artwork_hash;
const [failedArtKey, setFailedArtKey] = useState<string | null>(null);
const thumbUri =
artworkHash && failedArtworkHash !== artworkHash ? artworkThumbUri(artworkHash) : null;
const thumbUri = failedArtKey !== artKey ? trackArtworkThumbSource(track) : null;
const secondaryText = subtitle ?? (showArtist ? track.artist : null);
const row = (
@@ -57,10 +59,10 @@ export function TrackRow({
style={styles.artImage}
contentFit="cover"
cachePolicy="memory-disk"
recyclingKey={artworkHash}
recyclingKey={artKey ?? undefined}
transition={null}
allowDownscaling
onError={() => setFailedArtworkHash(artworkHash)}
onError={() => setFailedArtKey(artKey)}
/>
) : (
<AstraLogo size={18} />
@@ -87,6 +89,7 @@ export function TrackRow({
</Text>
) : null}
<View style={styles.badges}>
<RemoteSourceBadge sourceType={track.source_type} />
<FormatBadges
track={{
format: track.format,
@@ -171,6 +174,9 @@ const styles = StyleSheet.create({
color: colors.accent,
},
badges: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
marginTop: 2,
},
duration: {