mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-19 04:06:43 +02:00
m5, subsonic/jellyfin support
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { colors } from '@/theme';
|
||||
import type { TrackSourceType } from '@/types/library';
|
||||
|
||||
/**
|
||||
* Small "this track streams from a server" marker. Renders nothing for local files.
|
||||
* A cloud icon reads as remote/streamed regardless of provider (Subsonic/Jellyfin).
|
||||
*/
|
||||
export function RemoteSourceBadge({
|
||||
sourceType,
|
||||
size = 12,
|
||||
color = colors.accent,
|
||||
}: {
|
||||
sourceType?: TrackSourceType | null;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}) {
|
||||
if (!sourceType || sourceType === 'local') return null;
|
||||
return (
|
||||
<Ionicons
|
||||
name="cloud"
|
||||
size={size}
|
||||
color={color}
|
||||
accessibilityLabel={`Streaming from ${sourceType}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoteSourceBadge;
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user