mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
support secondary public server URL
This commit is contained in:
@@ -312,6 +312,9 @@
|
||||
"input_password": "password",
|
||||
"input_preferInstantMix": "prefer instant mix",
|
||||
"input_preferInstantMixDescription": "only use instant mix to get similar songs. useful if you have plugins that modify this behavior",
|
||||
"input_preferRemoteUrl": "prefer public url",
|
||||
"input_remoteUrl": "public url",
|
||||
"input_remoteUrlPlaceholder": "optional: public url for external features",
|
||||
"input_savePassword": "save password",
|
||||
"input_url": "url",
|
||||
"input_username": "username",
|
||||
|
||||
@@ -9,6 +9,7 @@ import packageJson from '../../../../package.json';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
|
||||
import { getClientType } from '/@/shared/api/utils';
|
||||
import { ServerListItemWithCredential } from '/@/shared/types/domain-types';
|
||||
@@ -408,7 +409,8 @@ export const jfApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
baseUrl = `${server?.url}`;
|
||||
const serverUrl = getServerUrl(server);
|
||||
baseUrl = serverUrl;
|
||||
token = server?.credential;
|
||||
} else {
|
||||
baseUrl = url;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||
|
||||
import { 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';
|
||||
import { JFSongListSort, JFSortOrder, jfType } from '/@/shared/api/jellyfin/jellyfin-types';
|
||||
import { getFeatures, hasFeature, sortSongList, VersionInfo } from '/@/shared/api/utils';
|
||||
@@ -691,21 +692,22 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body?.TotalRecordCount || 0,
|
||||
};
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!server?.url) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For Jellyfin, we construct the URL pattern
|
||||
// The server will return a 404 or placeholder if no image exists
|
||||
const baseUrl = `${server.url}/Items/${id}/Images/Primary?quality=96${imageSize ? `&width=${imageSize}` : ''}`;
|
||||
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 baseUrl;
|
||||
return imageUrl;
|
||||
},
|
||||
getInternetRadioStations: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs from 'qs';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { authenticationFailure } from '/@/renderer/api/utils';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { resultWithHeaders } from '/@/shared/api/utils';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
@@ -411,7 +412,8 @@ export const ndApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
baseUrl = `${server?.url}/api`;
|
||||
const serverUrl = getServerUrl(server);
|
||||
baseUrl = serverUrl ? `${serverUrl}/api` : undefined;
|
||||
token = server?.ndCredential;
|
||||
} else {
|
||||
baseUrl = url;
|
||||
|
||||
@@ -5,6 +5,7 @@ import qs from 'qs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
@@ -398,7 +399,8 @@ export const ssApiClient = (args: {
|
||||
const { params, path: api } = parsePath(path);
|
||||
|
||||
if (server) {
|
||||
baseUrl = `${server.url}/rest`;
|
||||
const serverUrl = getServerUrl(server);
|
||||
baseUrl = serverUrl ? `${serverUrl}/rest` : undefined;
|
||||
const token = server.credential;
|
||||
const params = token.split(/&?\w=/gm);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from 'zod';
|
||||
|
||||
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||
import {
|
||||
AlbumListSortType,
|
||||
@@ -851,11 +852,12 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
startIndex: query.startIndex,
|
||||
});
|
||||
},
|
||||
getImageUrl: ({ apiClientProps: { server }, query }) => {
|
||||
getImageUrl: ({ apiClientProps: { server }, baseUrl, query }) => {
|
||||
const { id, size } = query;
|
||||
const imageSize = size;
|
||||
const url = baseUrl || getServerUrl(server);
|
||||
|
||||
if (!server?.url || !server?.credential) {
|
||||
if (!url || !server?.credential) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -865,7 +867,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
}
|
||||
|
||||
return (
|
||||
`${server.url}/rest/getCoverArt.view` +
|
||||
`${url}/rest/getCoverArt.view` +
|
||||
`?id=${id}` +
|
||||
`&${server.credential}` +
|
||||
'&v=1.13.0' +
|
||||
|
||||
@@ -4,6 +4,7 @@ import z from 'zod';
|
||||
import { api } from '/@/renderer/api';
|
||||
import {
|
||||
GeneralSettingsSchema,
|
||||
getServerById,
|
||||
useAuthStore,
|
||||
useCurrentServerId,
|
||||
useSettingsStore,
|
||||
@@ -65,10 +66,11 @@ interface UseItemImageUrlProps {
|
||||
serverId?: string;
|
||||
size?: number;
|
||||
type?: keyof z.infer<typeof GeneralSettingsSchema>['imageRes'];
|
||||
useRemoteUrl?: boolean;
|
||||
}
|
||||
|
||||
export const useItemImageUrl = (args: UseItemImageUrlProps) => {
|
||||
const { id, imageUrl, itemType, size, type } = args;
|
||||
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const imageRes = useSettingsStore((store) => store.general.imageRes);
|
||||
@@ -83,17 +85,26 @@ export const useItemImageUrl = (args: UseItemImageUrlProps) => {
|
||||
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.getImageUrl({
|
||||
apiClientProps: { serverId: args.serverId || serverId },
|
||||
apiClientProps: { serverId: targetServerId },
|
||||
baseUrl,
|
||||
query: { id, itemType, size: size ?? sizeByType },
|
||||
}) || undefined
|
||||
);
|
||||
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType]);
|
||||
}, [args.serverId, id, imageUrl, itemType, serverId, size, sizeByType, useRemoteUrl]);
|
||||
};
|
||||
|
||||
export function getItemImageUrl(args: UseItemImageUrlProps) {
|
||||
const { id, imageUrl, itemType, size, type } = args;
|
||||
const { id, imageUrl, itemType, size, type, useRemoteUrl } = args;
|
||||
const authStore = useAuthStore.getState();
|
||||
const currentServerId = authStore.currentServer?.id;
|
||||
const serverId = (args.serverId || currentServerId) as string;
|
||||
@@ -109,9 +120,17 @@ export function getItemImageUrl(args: UseItemImageUrlProps) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let baseUrl: string | undefined;
|
||||
|
||||
if (useRemoteUrl) {
|
||||
const server = getServerById(serverId);
|
||||
baseUrl = server?.remoteUrl || server?.url;
|
||||
}
|
||||
|
||||
return (
|
||||
api.controller.getImageUrl({
|
||||
apiClientProps: { serverId },
|
||||
baseUrl,
|
||||
query: { id, itemType, size: size ?? sizeByType },
|
||||
}) || undefined
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Link, useParams } from 'react-router';
|
||||
|
||||
import styles from './album-detail-header.module.css';
|
||||
|
||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
|
||||
@@ -41,6 +41,7 @@ export const useDiscordRpc = () => {
|
||||
imageUrl: currentSong?.imageUrl,
|
||||
itemType: LibraryItem.SONG,
|
||||
type: 'table',
|
||||
useRemoteUrl: true,
|
||||
});
|
||||
|
||||
const imageUrlRef = useRef<null | string | undefined>(imageUrl);
|
||||
@@ -194,14 +195,13 @@ export const useDiscordRpc = () => {
|
||||
}
|
||||
|
||||
if (discordSettings.showServerImage && song) {
|
||||
// Use imageUrl from useItemImageUrl hook if available and song matches current song
|
||||
if (song._uniqueId === currentSong?._uniqueId && imageUrlRef.current) {
|
||||
activity.largeImageKey = imageUrlRef.current;
|
||||
} else {
|
||||
// Fallback to old logic if song doesn't match (shouldn't happen in normal flow)
|
||||
if (song._serverType === ServerType.JELLYFIN && song.imageUrl) {
|
||||
activity.largeImageKey = song.imageUrl;
|
||||
} else if (song._serverType === ServerType.NAVIDROME) {
|
||||
activity.largeImageKey = imageUrlRef.current;
|
||||
} else if (
|
||||
song._serverType === ServerType.NAVIDROME ||
|
||||
song._serverType === ServerType.SUBSONIC
|
||||
) {
|
||||
try {
|
||||
const info = await api.controller.getAlbumInfo({
|
||||
apiClientProps: { serverId: song._serverId },
|
||||
|
||||
@@ -101,6 +101,8 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
||||
(localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) || 'My Server',
|
||||
password: '',
|
||||
preferInstantMix: undefined,
|
||||
preferRemoteUrl: false,
|
||||
remoteUrl: '',
|
||||
savePassword: undefined,
|
||||
type:
|
||||
(localSettings
|
||||
@@ -166,6 +168,14 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
||||
serverItem.savePassword = values.savePassword;
|
||||
}
|
||||
|
||||
if (values.remoteUrl?.trim()) {
|
||||
serverItem.remoteUrl = values.remoteUrl.trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
if (values.preferRemoteUrl !== undefined) {
|
||||
serverItem.preferRemoteUrl = values.preferRemoteUrl;
|
||||
}
|
||||
|
||||
if (data.ndCredential !== undefined) {
|
||||
serverItem.ndCredential = data.ndCredential;
|
||||
}
|
||||
@@ -247,6 +257,29 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
</Group>
|
||||
<TextInput
|
||||
disabled={isServerLock}
|
||||
label={t('form.addServer.input', {
|
||||
context: 'remoteUrl',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
placeholder={t('form.addServer.input', {
|
||||
context: 'remoteUrlPlaceholder',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
{...form.getInputProps('remoteUrl')}
|
||||
/>
|
||||
{form.values.remoteUrl && (
|
||||
<Checkbox
|
||||
label={t('form.addServer.input', {
|
||||
context: 'preferRemoteUrl',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('preferRemoteUrl', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
label={t('form.addServer.input', {
|
||||
context: 'username',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useAuthStoreActions } from '/@/renderer/store';
|
||||
import { getServerById, useAuthStoreActions } from '/@/renderer/store';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
@@ -55,6 +55,8 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
name: server?.name,
|
||||
password: password || '',
|
||||
preferInstantMix: server.preferInstantMix,
|
||||
preferRemoteUrl: server?.preferRemoteUrl || false,
|
||||
remoteUrl: server?.remoteUrl || '',
|
||||
savePassword: server.savePassword,
|
||||
type: server?.type,
|
||||
url: server?.url,
|
||||
@@ -66,43 +68,81 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
const isNavidrome = form.values.type === ServerType.NAVIDROME;
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
const authFunction = api.controller.authenticate;
|
||||
|
||||
if (!authFunction) {
|
||||
return toast.error({
|
||||
message: t('error.invalidServer', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data: AuthenticationResponse | undefined = await authFunction(
|
||||
values.url,
|
||||
{
|
||||
legacy: values.legacyAuth,
|
||||
password: values.password,
|
||||
username: values.username,
|
||||
},
|
||||
values.type,
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return toast.error({
|
||||
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
// Check if we can skip authentication
|
||||
const usernameChanged = values.username !== server.username;
|
||||
const passwordProvided = values.password && values.password.trim() !== '';
|
||||
const urlChanged = values.url !== server.url;
|
||||
const typeChanged = values.type !== server.type;
|
||||
|
||||
// Skip authentication if username hasn't changed, password is empty, and URL/type haven't changed
|
||||
const canSkipAuth =
|
||||
!usernameChanged && !passwordProvided && !urlChanged && !typeChanged;
|
||||
|
||||
let data: AuthenticationResponse | undefined;
|
||||
let serverItem: ServerListItemWithCredential;
|
||||
|
||||
if (canSkipAuth) {
|
||||
// Use existing server credentials
|
||||
const existingServer = getServerById(server.id);
|
||||
if (!existingServer) {
|
||||
return toast.error({
|
||||
message: t('error.invalidServer', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
|
||||
serverItem = {
|
||||
...existingServer,
|
||||
id: server.id,
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
url: values.url,
|
||||
};
|
||||
} else {
|
||||
// Need to authenticate
|
||||
const authFunction = api.controller.authenticate;
|
||||
|
||||
if (!authFunction) {
|
||||
return toast.error({
|
||||
message: t('error.invalidServer', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
|
||||
data = await authFunction(
|
||||
values.url,
|
||||
{
|
||||
legacy: values.legacyAuth,
|
||||
password: values.password,
|
||||
username: values.username,
|
||||
},
|
||||
values.type,
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return toast.error({
|
||||
message: t('error.authenticationFailed', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
|
||||
serverItem = {
|
||||
credential: data.credential,
|
||||
id: server.id,
|
||||
isAdmin: data.isAdmin,
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
url: values.url,
|
||||
userId: data.userId,
|
||||
username: data.username,
|
||||
};
|
||||
|
||||
if (data.ndCredential !== undefined) {
|
||||
serverItem.ndCredential = data.ndCredential;
|
||||
}
|
||||
}
|
||||
|
||||
const serverItem: ServerListItemWithCredential = {
|
||||
credential: data.credential,
|
||||
id: server.id,
|
||||
isAdmin: data.isAdmin,
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
url: values.url,
|
||||
userId: data.userId,
|
||||
username: data.username,
|
||||
};
|
||||
|
||||
// Update optional fields
|
||||
if (values.preferInstantMix !== undefined) {
|
||||
serverItem.preferInstantMix = values.preferInstantMix;
|
||||
}
|
||||
@@ -111,8 +151,14 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
serverItem.savePassword = values.savePassword;
|
||||
}
|
||||
|
||||
if (data.ndCredential !== undefined) {
|
||||
serverItem.ndCredential = data.ndCredential;
|
||||
if (values.remoteUrl?.trim()) {
|
||||
serverItem.remoteUrl = values.remoteUrl.trim().replace(/\/$/, '');
|
||||
} else {
|
||||
serverItem.remoteUrl = undefined;
|
||||
}
|
||||
|
||||
if (values.preferRemoteUrl !== undefined) {
|
||||
serverItem.preferRemoteUrl = values.preferRemoteUrl;
|
||||
}
|
||||
|
||||
updateServer(server.id, serverItem);
|
||||
@@ -120,19 +166,29 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
message: t('form.updateServer.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
|
||||
// Handle password saving in local settings
|
||||
if (localSettings) {
|
||||
if (values.savePassword) {
|
||||
const saved = await localSettings.passwordSet(values.password, server.id);
|
||||
if (!saved) {
|
||||
toast.error({
|
||||
message: t('form.addServer.error', {
|
||||
context: 'savePassword',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
if (canSkipAuth) {
|
||||
// If we skipped auth, only update savePassword preference
|
||||
// Don't change the actual saved password
|
||||
if (!values.savePassword) {
|
||||
localSettings.passwordRemove(server.id);
|
||||
}
|
||||
} else {
|
||||
localSettings.passwordRemove(server.id);
|
||||
// If we authenticated, update password if savePassword is enabled
|
||||
if (values.savePassword && passwordProvided) {
|
||||
const saved = await localSettings.passwordSet(values.password, server.id);
|
||||
if (!saved) {
|
||||
toast.error({
|
||||
message: t('form.addServer.error', {
|
||||
context: 'savePassword',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else if (!values.savePassword) {
|
||||
localSettings.passwordRemove(server.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +223,32 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
rightSection={form.isDirty('url') && <ModifiedFieldIndicator />}
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('form.addServer.input', {
|
||||
context: 'remoteUrl',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
placeholder={t('form.addServer.input', {
|
||||
context: 'remoteUrlPlaceholder',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
rightSection={form.isDirty('remoteUrl') && <ModifiedFieldIndicator />}
|
||||
{...form.getInputProps('remoteUrl')}
|
||||
/>
|
||||
{form.values.remoteUrl && (
|
||||
<Group gap="xs">
|
||||
<Checkbox
|
||||
label={t('form.addServer.input', {
|
||||
context: 'preferRemoteUrl',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
{...form.getInputProps('preferRemoteUrl', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
/>
|
||||
{form.isDirty('preferRemoteUrl') && <ModifiedFieldIndicator />}
|
||||
</Group>
|
||||
)}
|
||||
<TextInput
|
||||
label={t('form.addServer.input', {
|
||||
context: 'username',
|
||||
@@ -182,7 +264,6 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
|
||||
context: 'password',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
required={isNavidrome || isSubsonic}
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
{localSettings && isNavidrome && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useShareItem } from '/@/renderer/features/sharing/mutations/share-item-mutation';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
|
||||
import { DateTimePicker } from '/@/shared/components/date-time-picker/date-time-picker';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { ModalButton } from '/@/shared/components/modal/model-shared';
|
||||
@@ -69,7 +70,9 @@ export const ShareItemContextModal = ({
|
||||
if (!server) throw new Error('Server not found');
|
||||
if (!_data?.id) throw new Error('Failed to share item');
|
||||
|
||||
const shareUrl = `${server.url}/share/${_data.id}`;
|
||||
const serverUrl = getServerUrl(server, true);
|
||||
if (!serverUrl) throw new Error('Server URL not found');
|
||||
const shareUrl = `${serverUrl}/share/${_data.id}`;
|
||||
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
toast.success({
|
||||
|
||||
@@ -124,6 +124,8 @@ export const useCurrentServer = () =>
|
||||
musicFolderId: state.currentServer?.musicFolderId,
|
||||
name: state.currentServer?.name,
|
||||
preferInstantMix: state.currentServer?.preferInstantMix,
|
||||
preferRemoteUrl: state.currentServer?.preferRemoteUrl,
|
||||
remoteUrl: state.currentServer?.remoteUrl,
|
||||
savePassword: state.currentServer?.savePassword,
|
||||
type: state.currentServer?.type,
|
||||
url: state.currentServer?.url,
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||
|
||||
export const normalizeServerUrl = (url: string) => {
|
||||
// Remove trailing slash
|
||||
return url.endsWith('/') ? url.slice(0, -1) : url;
|
||||
};
|
||||
|
||||
export const getServerUrl = (
|
||||
server: null | ServerListItem | undefined,
|
||||
forceRemoteUrl?: boolean,
|
||||
): string | undefined => {
|
||||
if (!server) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!forceRemoteUrl && !server.preferRemoteUrl) {
|
||||
return server.url;
|
||||
}
|
||||
|
||||
if (!server.remoteUrl) {
|
||||
return server.url;
|
||||
}
|
||||
|
||||
return server.remoteUrl;
|
||||
};
|
||||
|
||||
@@ -91,6 +91,8 @@ export type ServerListItem = {
|
||||
musicFolderId?: string[];
|
||||
name: string;
|
||||
preferInstantMix?: boolean;
|
||||
preferRemoteUrl?: boolean;
|
||||
remoteUrl?: string;
|
||||
savePassword?: boolean;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
@@ -1418,6 +1420,7 @@ export type GetQueueResponse = {
|
||||
};
|
||||
|
||||
export type ImageArgs = BaseEndpointArgs & {
|
||||
baseUrl?: string;
|
||||
query: ImageQuery;
|
||||
};
|
||||
|
||||
|
||||
@@ -250,6 +250,8 @@ export type ServerListItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
ndCredential?: string;
|
||||
preferRemoteUrl?: boolean;
|
||||
remoteUrl?: string;
|
||||
savePassword?: boolean;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
|
||||
Reference in New Issue
Block a user