mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-13 20:10:07 +02:00
rewrite Image component
- remove react-image dependency - use manual blob load - abort load when exiting viewport
This commit is contained in:
@@ -442,6 +442,25 @@ export const controller: GeneralController = {
|
||||
}),
|
||||
);
|
||||
},
|
||||
getImageRequest(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
apiController(
|
||||
'getImageRequest',
|
||||
server.type,
|
||||
)?.(
|
||||
addContext({
|
||||
...args,
|
||||
apiClientProps: { ...args.apiClientProps, server },
|
||||
}),
|
||||
) || null
|
||||
);
|
||||
},
|
||||
getImageUrl(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||
import { createAuthHeader, jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
|
||||
import { useRadioStore } from '/@/renderer/features/radio/store/radio-store';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize';
|
||||
@@ -15,10 +15,13 @@ import {
|
||||
albumListSortMap,
|
||||
Folder,
|
||||
genreListSortMap,
|
||||
ImageArgs,
|
||||
ImageRequest,
|
||||
InternalControllerEndpoint,
|
||||
LibraryItem,
|
||||
Played,
|
||||
playlistListSortMap,
|
||||
ReplaceApiClientProps,
|
||||
ServerType,
|
||||
Song,
|
||||
SongListSort,
|
||||
@@ -29,6 +32,33 @@ import {
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
const getJellyfinImageRequest = ({
|
||||
apiClientProps: { server },
|
||||
baseUrl,
|
||||
query,
|
||||
}: ReplaceApiClientProps<ImageArgs>): ImageRequest | null => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cacheKey: ['jellyfin', server.id, baseUrl || '', id, imageSize || ''].join(':'),
|
||||
headers: server.credential
|
||||
? { Authorization: createAuthHeader().concat(`, Token="${server.credential}"`) }
|
||||
: { Authorization: createAuthHeader() },
|
||||
url: `${url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`,
|
||||
};
|
||||
};
|
||||
|
||||
const formatCommaDelimitedString = (value: string[]) => {
|
||||
return value.join(',');
|
||||
};
|
||||
@@ -789,23 +819,8 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body?.TotalRecordCount || 0,
|
||||
};
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For Jellyfin, we construct the URL pattern
|
||||
// The server will return a 404 or placeholder if no image exists
|
||||
const imageUrl = `${url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
|
||||
|
||||
// For songs, we might want to fall back to album art, but we don't have albumId here
|
||||
// The caller can handle this if needed
|
||||
return imageUrl;
|
||||
},
|
||||
getImageRequest: getJellyfinImageRequest,
|
||||
getImageUrl: (args) => getJellyfinImageRequest(args)?.url || null,
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
|
||||
@@ -545,6 +545,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
},
|
||||
getImageRequest: SubsonicController.getImageRequest,
|
||||
getImageUrl: SubsonicController.getImageUrl,
|
||||
getInternetRadioStations: SubsonicController.getInternetRadioStations,
|
||||
getLyrics: SubsonicController.getLyrics,
|
||||
|
||||
@@ -20,9 +20,12 @@ import { hasFeature, sortAlbumArtistList, sortAlbumList, sortSongList } from '/@
|
||||
import {
|
||||
AlbumListSort,
|
||||
GenreListSort,
|
||||
ImageArgs,
|
||||
ImageRequest,
|
||||
InternalControllerEndpoint,
|
||||
LibraryItem,
|
||||
PlaylistListSort,
|
||||
ReplaceApiClientProps,
|
||||
ServerType,
|
||||
Song,
|
||||
SongListSort,
|
||||
@@ -30,6 +33,36 @@ import {
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature, ServerFeatures } from '/@/shared/types/features-types';
|
||||
|
||||
const getSubsonicImageRequest = ({
|
||||
apiClientProps: { server },
|
||||
baseUrl,
|
||||
query,
|
||||
}: ReplaceApiClientProps<ImageArgs>): ImageRequest | null => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!url || !server?.credential) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for default placeholder image ID
|
||||
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cacheKey: ['subsonic', server.id, baseUrl || '', id, imageSize || ''].join(':'),
|
||||
url:
|
||||
`${url}/rest/getCoverArt.view` +
|
||||
`?id=${id}` +
|
||||
`&${server.credential}` +
|
||||
'&v=1.13.0' +
|
||||
'&c=Feishin' +
|
||||
(imageSize ? `&size=${imageSize}` : ''),
|
||||
};
|
||||
};
|
||||
|
||||
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
||||
[AlbumListSort.ARTIST]: undefined,
|
||||
@@ -952,29 +985,8 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!url || !server?.credential) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for default placeholder image ID
|
||||
if (id.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${url}/rest/getCoverArt.view` +
|
||||
`?id=${id}` +
|
||||
`&${server.credential}` +
|
||||
'&v=1.13.0' +
|
||||
'&c=Feishin' +
|
||||
(imageSize ? `&size=${imageSize}` : '')
|
||||
);
|
||||
},
|
||||
getImageRequest: getSubsonicImageRequest,
|
||||
getImageUrl: (args) => getSubsonicImageRequest(args)?.url || null,
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { BaseImage, ImageProps } from '/@/shared/components/image/image';
|
||||
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { ExplicitStatus, ImageRequest, LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
const getUnloaderIcon = (itemType: LibraryItem) => {
|
||||
switch (itemType) {
|
||||
@@ -54,10 +54,19 @@ const BaseItemImage = (
|
||||
type: props.type,
|
||||
});
|
||||
|
||||
const imageRequest = useItemImageRequest({
|
||||
id: props.id,
|
||||
imageUrl: src,
|
||||
itemType: props.itemType,
|
||||
serverId: serverId || undefined,
|
||||
type: props.type,
|
||||
});
|
||||
|
||||
const isExplicit = blurExplicitImages && explicitStatus === ExplicitStatus.EXPLICIT;
|
||||
|
||||
return (
|
||||
<BaseImage
|
||||
imageRequest={imageRequest}
|
||||
isExplicit={isExplicit}
|
||||
src={imageUrl}
|
||||
unloaderIcon={getUnloaderIcon(props.itemType)}
|
||||
@@ -113,6 +122,79 @@ export const useItemImageUrl = (args: UseItemImageUrlProps) => {
|
||||
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType, useRemoteUrl]);
|
||||
};
|
||||
|
||||
export const useItemImageRequest = (args: UseItemImageUrlProps) => {
|
||||
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const imageRes = useImageRes();
|
||||
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
|
||||
|
||||
return useMemo(() => {
|
||||
if (imageUrl) {
|
||||
return {
|
||||
cacheKey: imageUrl,
|
||||
url: imageUrl,
|
||||
} satisfies ImageRequest;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetServerId = args.serverId || serverId;
|
||||
let baseUrl: string | undefined;
|
||||
|
||||
if (useRemoteUrl) {
|
||||
const server = getServerById(targetServerId);
|
||||
baseUrl = server?.remoteUrl || server?.url;
|
||||
}
|
||||
|
||||
return (
|
||||
api.controller.getImageRequest({
|
||||
apiClientProps: { serverId: targetServerId },
|
||||
baseUrl,
|
||||
query: { id, itemType, size: size ?? sizeByType },
|
||||
}) || undefined
|
||||
);
|
||||
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType, useRemoteUrl]);
|
||||
};
|
||||
|
||||
export function getItemImageRequest(args: UseItemImageUrlProps) {
|
||||
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
|
||||
const authStore = useAuthStore.getState();
|
||||
const currentServerId = authStore.currentServer?.id;
|
||||
const serverId = (args.serverId || currentServerId) as string;
|
||||
|
||||
const imageRes = useSettingsStore.getState().general.imageRes;
|
||||
const sizeByType: number | undefined = type ? imageRes[type] : undefined;
|
||||
|
||||
if (imageUrl) {
|
||||
return {
|
||||
cacheKey: imageUrl,
|
||||
url: imageUrl,
|
||||
} satisfies ImageRequest;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let baseUrl: string | undefined;
|
||||
|
||||
if (useRemoteUrl) {
|
||||
const server = getServerById(serverId);
|
||||
baseUrl = server?.remoteUrl || server?.url;
|
||||
}
|
||||
|
||||
return (
|
||||
api.controller.getImageRequest({
|
||||
apiClientProps: { serverId },
|
||||
baseUrl,
|
||||
query: { id, itemType, size: size ?? sizeByType },
|
||||
}) || undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function getItemImageUrl(args: UseItemImageUrlProps) {
|
||||
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
|
||||
const authStore = useAuthStore.getState();
|
||||
|
||||
Reference in New Issue
Block a user