fix: missing images

This commit is contained in:
Lei Nelissen
2025-05-24 00:09:12 +02:00
parent 58c7645170
commit 90a5d94147
13 changed files with 143 additions and 86 deletions

View File

@@ -28,7 +28,7 @@ interface Props {
margin?: number;
radius?: number;
style?: ViewProps['style'];
src: string;
src?: string;
}
const emptyAlbumLight = require('@/assets/images/empty-album-light.png');
@@ -59,6 +59,8 @@ function CoverImage({
return { imageSize, canvasSize };
}, [blurRadius, margin]);
console.log({ src, hasFailed });
return (
<Container size={imageSize} style={style}>
<BlurContainer size={canvasSize} offset={blurRadius}>
@@ -69,7 +71,7 @@ function CoverImage({
<Shadow dx={0} dy={8} blur={16} color="#0000000d" />
<Shadow dx={0} dy={16} blur={32} color="#0000000d" />
</RoundedRect>
{src && (
{src ? (
<>
<SkiaImage
image={image}
@@ -97,8 +99,7 @@ function CoverImage({
</SkiaImage>
</Mask>
</>
)}
{(!src || hasFailed) && (
) : (
<Mask
mask={
<RoundedRect

View File

@@ -136,7 +136,7 @@ function Downloads() {
<DownloadedTrack>
<View style={{ marginRight: 12 }}>
<ShadowWrapper size="small">
<AlbumImage source={{ uri: getImage(item) }} style={defaultStyles.imageBackground} />
<AlbumImage source={{ uri: getImage(tracks[item]) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
</View>
<View style={{ flexShrink: 1, marginRight: 8 }}>

View File

@@ -48,7 +48,7 @@ const SectionHeading = React.memo(function SectionHeading(props: {
interface GeneratedAlbumItemProps {
id: ReactText;
imageUrl: string;
imageUrl: string | null;
name: string;
artist: string;
onPress: (id: string) => void;
@@ -66,7 +66,7 @@ const GeneratedAlbumItem = React.memo(function GeneratedAlbumItem(props: Generat
<TouchableHandler id={id as string} onPress={onPress}>
<AlbumItem>
<ShadowWrapper size="medium">
<AlbumImage source={{ uri: imageUrl }} style={[defaultStyles.imageBackground]} />
<AlbumImage source={imageUrl ? { uri: imageUrl } : undefined} style={[defaultStyles.imageBackground]} />
</ShadowWrapper>
<Text numberOfLines={1} style={defaultStyles.text}>{name}</Text>
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>{artist}</HalfOpacity>
@@ -136,7 +136,7 @@ const Albums: React.FC = () => {
<GeneratedAlbumItem
key={i}
id={id}
imageUrl={getImage(albums[id])}
imageUrl={albums[id].ImageTags.Primary ? getImage(albums[id]) : null}
name={albums[id]?.Name || ''}
artist={albums[id]?.AlbumArtist || ''}
onPress={selectAlbum}

View File

@@ -14,7 +14,7 @@ import { SafeFlatList, useNavigationOffsets } from '@/components/SafeNavigatorVi
interface GeneratedAlbumItemProps {
id: ReactText;
imageUrl: string;
imageUrl?: string | null;
name: string;
onPress: (id: string) => void;
}
@@ -26,7 +26,7 @@ const GeneratedPlaylistItem = React.memo(function GeneratedPlaylistItem(props: G
return (
<TouchableHandler id={id as string} onPress={onPress}>
<AlbumItem>
<AlbumImage source={{ uri: imageUrl }} style={defaultStyles.imageBackground} />
<AlbumImage source={{ uri: imageUrl || undefined }} style={defaultStyles.imageBackground} />
<Text numberOfLines={1} style={defaultStyles.text}>{name}</Text>
</AlbumItem>
</TouchableHandler>

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React from 'react';
import styled from 'styled-components/native';
import FastImage, { FastImageProps } from 'react-native-fast-image';
import FastImage, { FastImageProps, Source } from '@d11/react-native-fast-image';
import { Dimensions } from 'react-native';
import { useUserOrSystemScheme } from '@/components/Colors';
@@ -22,24 +22,29 @@ const Container = styled(FastImage)`
margin-bottom: 5px;
`;
function AlbumImage(props: FastImageProps) {
const [hasError, setError] = useState(false);
const colorScheme = useUserOrSystemScheme();
export interface AlbumImageProps extends FastImageProps {
source: number | Omit<Source, 'uri'> & {
uri?: string;
};
}
if (!props.source || hasError) {
return (
<Container
{...props}
source={colorScheme === 'light'
? require('@/assets/images/empty-album-light.png')
: require('@/assets/images/empty-album-dark.png')
}
/>
);
const defaultImageDark = require('@/assets/images/empty-album-dark.png');
const defaultImageLight = require('@/assets/images/empty-album-light.png');
function AlbumImage(props: FastImageProps) {
const colorScheme = useUserOrSystemScheme();
const defaultImage = colorScheme === 'light' ? defaultImageLight : defaultImageDark;
// If no source is provided, use the default image as the main source
if (!props.source || (typeof props.source === 'object' && 'uri' in props.source && !props.source.uri)) {
return <Container {...props} source={defaultImage} />;
}
return (
<Container {...props} onError={() => setError(true)} />
<Container
{...props}
defaultSource={defaultImage}
/>
);
}

View File

@@ -100,6 +100,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
const tracks = useTypedSelector((state) => state.music.tracks.entities);
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
const entity = useTypedSelector((state) => itemDisplayStyle === 'album' ? state.music.albums.entities[entityId] : state.music.playlists.entities[entityId]);
const totalDuration = useMemo(() => (
trackIds.reduce<number>((sum, trackId) => (
sum + (tracks[trackId]?.RunTimeTicks || 0)
@@ -173,7 +174,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
>
<View style={{ padding: 24, paddingTop: 32, paddingBottom: 32 }}>
<AlbumImageContainer>
<CoverImage src={getImage(entityId)} />
<CoverImage src={getImage(entity)} />
</AlbumImageContainer>
<Header>{title}</Header>
<SubHeader>{artist}</SubHeader>

View File

@@ -46,7 +46,7 @@ export async function fetchApi<T>(
path: PathOrCredentialInserter,
providedConfig?: RequestInit,
parseResponse = true
) {
) {
// Retrieve the latest credentials from the Redux store
const credentials = asyncFetchStore().getState().settings.credentials;
@@ -67,11 +67,11 @@ export async function fetchApi<T>(
...generateConfig(credentials).headers,
}
};
// Actually perform the request
const response = await fetch(url, config);
if (__DEV__) {
if (__DEV__) {
console.log(`%c[HTTP] → [${response.status}] ${url}`, 'font-weight:bold;');
console.log('\t', config);
}
@@ -96,24 +96,44 @@ export async function fetchApi<T>(
if (parseResponse) {
// Parse body as JSON
const data = await response.json() as Promise<T>;
return data;
}
return null;
}
function formatImageUri(ItemId: string | number, baseUri: string): string {
return encodeURI(`${baseUri}/Items/${ItemId}/Images/Primary?format=jpeg`);
}
/**
* Retrieve an image URL for a given ItemId
*/
export function getImage(ItemId: string | number, credentials?: AppState['settings']['credentials']): string {
export function getImage(item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null, credentials?: AppState['settings']['credentials']): string | undefined {
// Either accept provided credentials, or retrieve them directly from the store
const { uri: serverUri } = credentials
const { uri: serverUri } = credentials
?? asyncFetchStore().getState().settings.credentials ?? {};
// Generate the uri and return
const uri = encodeURI(`${serverUri}/Items/${ItemId}/Images/Primary?format=jpeg`);
return uri;
if (!item || !serverUri) {
return undefined;
// GUARD: If the item's just the id, we'll pass it on directly.
} else if (typeof item === 'string' || typeof item === 'number') {
if (__DEV__) {
console.warn('useGetImage: supplied item is string or number. Please submit an item object instead.', { item });
}
return formatImageUri(item, serverUri);
// GUARD: If the item has an `PrimaryImageItemId` (for Emby servers),
// we'll attemp to return that
} else if ('PrimaryImageItemId' in item) {
return formatImageUri(item.PrimaryImageItemId || item.Id, serverUri);
} else {
if ('ImageTags' in item && item.ImageTags.Primary) {
return formatImageUri(item.Id, serverUri);
}
}
return undefined;
}
/**
@@ -122,18 +142,7 @@ export function getImage(ItemId: string | number, credentials?: AppState['settin
export function useGetImage() {
const credentials = useTypedSelector((state) => state.settings.credentials);
return (item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null) => {
if (!item) {
return '';
// GUARD: If the item's just the id, we'll pass it on directly.
} else if (typeof item === 'string' || typeof item === 'number') {
return getImage(item, credentials);
// GUARD: If the item has an `PrimaryImageItemId` (for Emby servers),
// we'll attemp to return that
} else if ('PrimaryImageItemId' in item) {
return getImage(item.PrimaryImageItemId || item.Id, credentials);
} else {
return getImage(item.Id);
}
return (item: Parameters<typeof getImage>[0]) => {
return getImage(item, credentials);
};
}

View File

@@ -57,7 +57,7 @@ export async function generateTrack(track: AlbumTrack): Promise<Track> {
artist: track.Artists.join(', '),
album: track.Album,
duration: track.RunTimeTicks,
artwork: getImage(track.Id),
artwork: getImage(track),
bitRate: baseTrackOptions.audioBitRate,
};
}