support secondary public server URL

This commit is contained in:
jeffvli
2025-12-30 21:05:38 -08:00
parent 7aeadb531f
commit 72d0fca28b
16 changed files with 244 additions and 68 deletions
@@ -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 && (