Add initial support for static server

This commit is contained in:
jeffvli 2023-11-17 17:42:03 -08:00
parent 1d2e9484d8
commit 9c355ce5bd
12 changed files with 203 additions and 29 deletions

View File

@ -43,6 +43,11 @@ const configuration: webpack.Configuration = {
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
FS_SERVER_NAME: process.env.SERVER_URL ?? null,
FS_SERVER_URL: process.env.SERVER_URL ?? null,
FS_SERVER_TYPE: process.env.SERVER_URL ?? null,
FS_SERVER_USERNAME: process.env.SERVER_URL ?? null,
FS_SERVER_PASSWORD: process.env.SERVER_URL ?? null,
}),
],

View File

@ -7,6 +7,7 @@
"deletePlaylist": "delete $t(entity.playlist_one)",
"deselectAll": "deselect all",
"editPlaylist": "edit $t(entity.playlist_one)",
"signIn": "sign in",
"goToPage": "go to page",
"moveToBottom": "move to bottom",
"moveToTop": "move to top",
@ -14,6 +15,7 @@
"removeFromFavorites": "remove from $t(entity.favorite_other)",
"removeFromPlaylist": "remove from $t(entity.playlist_one)",
"removeFromQueue": "remove from queue",
"removeServer": "remove server",
"setRating": "set rating",
"toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor",
"viewPlaylists": "view $t(entity.playlist_other)"

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
@ -22,8 +22,19 @@ import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-han
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
import {
PlayerState,
useAuthStoreActions,
usePlayerStore,
useQueueControls,
} from '/@/renderer/store';
import {
FontType,
PlaybackType,
PlayerStatus,
ServerListItem,
ServerType,
} from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import i18n from '/@/i18n/i18n';
@ -48,6 +59,57 @@ export const App = () => {
const { clearQueue, restoreQueue } = useQueueControls();
const remoteSettings = useRemoteSettings();
const textStyleRef = useRef<HTMLStyleElement>();
const { addServer } = useAuthStoreActions();
const setStaticServer = useCallback(() => {
console.log('process.env.FS_SERVER_URL :>> ', process.env.FS_SERVER_URL);
console.log('process.env.FS_SERVER_NAME', process.env.FS_SERVER_NAME);
console.log('process.env.FS_SERVER_TYPE :>> ', process.env.FS_SERVER_TYPE);
const url = process.env.FS_SERVER_URL;
let serverType: ServerType | null = null;
const name =
process.env.FS_SERVER_NAME || serverType === ServerType.NAVIDROME
? 'Navidrome'
: 'Jellyfin';
switch (process.env.FS_SERVER_TYPE?.toLocaleLowerCase()) {
case 'jellyfin':
serverType = ServerType.JELLYFIN;
break;
case 'navidrome':
serverType = ServerType.NAVIDROME;
break;
case 'subsonic':
serverType = ServerType.SUBSONIC;
break;
default:
serverType = null;
break;
}
if (!url || !serverType) {
return;
}
const serverItem: ServerListItem = {
credential: undefined,
id: 'static-server',
name,
ndCredential: undefined,
static: true,
type: serverType,
url: url.replace(/\/$/, ''),
userId: undefined,
username: undefined,
};
addServer(serverItem);
}, [addServer]);
useEffect(() => {
setStaticServer();
}, [setStaticServer]);
useDiscordRpc();
useEffect(() => {

View File

@ -1,19 +1,70 @@
import { Text } from '/@/renderer/components';
import { openModal, closeAllModals } from '@mantine/modals';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { Button, DropdownMenu } from '/@/renderer/components';
import { useCurrentServer } from '/@/renderer/store';
import { RiKeyFill, RiMenuFill } from 'react-icons/ri';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
const localSettings = isElectron() ? window.electron.localSettings : null;
export const ServerCredentialRequired = () => {
const { t } = useTranslation();
const currentServer = useCurrentServer();
const handleCredentialsModal = async () => {
if (!currentServer) {
return;
}
const server = currentServer;
let password: string | null = null;
try {
if (localSettings && server.savePassword) {
password = await localSettings.passwordGet(server.id);
}
} catch (error) {
console.error(error);
}
openModal({
children: server && (
<EditServerForm
isUpdate
password={password}
server={server}
onCancel={closeAllModals}
/>
),
size: 'sm',
title: server.name,
});
};
return (
<>
<Text>
The selected server &apos;{currentServer?.name}&apos; requires an additional login
to access.
</Text>
<Text>
Add your credentials in the &apos;manage servers&apos; menu or switch to a different
server.
</Text>
<Button
leftIcon={<RiKeyFill />}
variant="filled"
onClick={handleCredentialsModal}
>
{t('action.signIn', { postProcess: 'titleCase' })}
</Button>
<DropdownMenu>
<DropdownMenu.Target>
<Button
leftIcon={<RiMenuFill />}
variant="filled"
>
{t('common.menu', { postProcess: 'titleCase' })}
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<AppMenu />
</DropdownMenu.Dropdown>
</DropdownMenu>
</>
);
};

View File

@ -1,8 +1,11 @@
import { useTranslation } from 'react-i18next';
import { RiMenuFill } from 'react-icons/ri';
import { Button, DropdownMenu, Text } from '/@/renderer/components';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
export const ServerRequired = () => {
const { t } = useTranslation();
return (
<>
<Text>No server selected.</Text>
@ -12,7 +15,7 @@ export const ServerRequired = () => {
leftIcon={<RiMenuFill />}
variant="filled"
>
Open menu
{t('common.menu', { postProcess: 'titleCase' })}
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>

View File

@ -4,7 +4,7 @@ import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { RiCheckFill } from 'react-icons/ri';
import { Link, Navigate } from 'react-router-dom';
import { Button, PageHeader, Text } from '/@/renderer/components';
import { Button, PageHeader } from '/@/renderer/components';
import { ActionRequiredContainer } from '/@/renderer/features/action-required/components/action-required-container';
import { MpvRequired } from '/@/renderer/features/action-required/components/mpv-required';
import { ServerCredentialRequired } from '/@/renderer/features/action-required/components/server-credential-required';
@ -12,15 +12,36 @@ import { ServerRequired } from '/@/renderer/features/action-required/components/
import { AnimatedPage } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { ServerListItem, ServerType } from '/@/renderer/types';
const localSettings = isElectron() ? window.electron.localSettings : null;
export const getIsCredentialRequired = (currentServer: ServerListItem | null) => {
if (currentServer === null) {
return false;
}
if (currentServer.type === ServerType.NAVIDROME) {
return !currentServer.ndCredential || !currentServer.credential || !currentServer.username;
}
if (currentServer.type === ServerType.JELLYFIN) {
return !currentServer.credential || !currentServer.username;
}
if (currentServer.type === ServerType.SUBSONIC) {
return !currentServer.credential || !currentServer.username;
}
return false;
};
const ActionRequiredRoute = () => {
const { t } = useTranslation();
const currentServer = useCurrentServer();
const [isMpvRequired, setIsMpvRequired] = useState(false);
const isServerRequired = !currentServer;
const isCredentialRequired = false;
const isCredentialRequired = getIsCredentialRequired(currentServer);
useEffect(() => {
const getMpvPath = async () => {
@ -85,7 +106,6 @@ const ActionRequiredRoute = () => {
color="var(--success-color)"
size={30}
/>
<Text size="xl">No issues found</Text>
</Group>
<Button
component={Link}
@ -93,7 +113,7 @@ const ActionRequiredRoute = () => {
to={AppRoute.HOME}
variant="filled"
>
Go back
{t('page.appMenu.goBack', { postProcess: 'sentenceCase' })}
</Button>
</>
)}

View File

@ -46,7 +46,7 @@ export const EditServerForm = ({ isUpdate, password, server, onCancel }: EditSer
savePassword: server.savePassword || false,
type: server?.type,
url: server?.url,
username: server?.username,
username: server?.username || '',
},
});
@ -125,6 +125,7 @@ export const EditServerForm = ({ isUpdate, password, server, onCancel }: EditSer
<Stack ref={focusTrapRef}>
<TextInput
required
disabled={server?.static}
label={t('form.addServer.input', {
context: 'name',
postProcess: 'titleCase',
@ -134,6 +135,7 @@ export const EditServerForm = ({ isUpdate, password, server, onCancel }: EditSer
/>
<TextInput
required
disabled={server?.static}
label={t('form.addServer.input', {
context: 'url',
postProcess: 'titleCase',
@ -143,6 +145,7 @@ export const EditServerForm = ({ isUpdate, password, server, onCancel }: EditSer
/>
<TextInput
required
data-autofocus={!server?.username}
label={t('form.addServer.input', {
context: 'username',
postProcess: 'titleCase',
@ -151,8 +154,8 @@ export const EditServerForm = ({ isUpdate, password, server, onCancel }: EditSer
{...form.getInputProps('username')}
/>
<PasswordInput
data-autofocus
required
data-autofocus={server?.username}
label={t('form.addServer.input', {
context: 'password',
postProcess: 'titleCase',

View File

@ -3,6 +3,7 @@ import { Stack, Group, Divider } from '@mantine/core';
import { Button, Text, TimeoutButton } from '/@/renderer/components';
import { useDisclosure } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { RiDeleteBin2Line, RiEdit2Fill } from 'react-icons/ri';
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
import { ServerSection } from '/@/renderer/features/servers/components/server-section';
@ -16,6 +17,7 @@ interface ServerListItemProps {
}
export const ServerListItem = ({ server }: ServerListItemProps) => {
const { t } = useTranslation();
const [edit, editHandlers] = useDisclosure(false);
const [savedPassword, setSavedPassword] = useState('');
const { deleteServer } = useAuthStoreActions();
@ -68,8 +70,18 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
<Stack>
<Group noWrap>
<Stack>
<Text>URL</Text>
<Text>Username</Text>
<Text>
{t('form.addServer.input', {
context: 'url',
postProcess: 'sentenceCase',
})}
</Text>
<Text>
{t('form.addServer.input', {
context: 'username',
postProcess: 'sentenceCase',
})}
</Text>
</Stack>
<Stack>
<Text>{server.url}</Text>
@ -83,7 +95,9 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
variant="subtle"
onClick={() => handleEdit()}
>
Edit
{t('common.edit', {
postProcess: 'sentenceCase',
})}
</Button>
</Group>
</Stack>
@ -95,7 +109,9 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
timeoutProps={{ callback: handleDeleteServer, duration: 1000 }}
variant="subtle"
>
Remove server
{t('action.removeServer', {
postProcess: 'sentenceCase',
})}
</TimeoutButton>
</Stack>
);

View File

@ -69,14 +69,14 @@ export const AppMenu = () => {
/>
),
size: 'sm',
title: `Update session for "${server.name}"`,
title: server.name,
});
};
const handleManageServersModal = () => {
openModal({
children: <ServerList />,
title: 'Manage Servers',
title: t('page.appMenu.manageServers', { postProcess: 'sentenceCase' }),
});
};

View File

@ -3,6 +3,7 @@ import isElectron from 'is-electron';
import { Navigate, Outlet } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { getIsCredentialRequired } from '/@/renderer/features/action-required/routes/action-required-route';
const localSettings = isElectron() ? window.electron.localSettings : null;
@ -17,9 +18,11 @@ export const AppOutlet = () => {
return true;
};
const isCredentialRequired = getIsCredentialRequired(currentServer);
const isServerRequired = !currentServer;
const actions = [isServerRequired, isMpvRequired()];
const actions = [isServerRequired, isCredentialRequired, isMpvRequired()];
const isActionRequired = actions.some((c) => c);
return isActionRequired;

View File

@ -31,7 +31,15 @@ export const useAuthStore = create<AuthSlice>()(
actions: {
addServer: (args) => {
set((state) => {
if (state.serverList[args.id]) {
return;
}
state.serverList[args.id] = args;
if (!state.currentServer) {
state.currentServer = args;
}
});
},
deleteServer: (id) => {

View File

@ -61,15 +61,16 @@ export enum ServerType {
}
export type ServerListItem = {
credential: string;
credential?: string;
id: string;
name: string;
ndCredential?: string;
savePassword?: boolean;
static?: boolean;
type: ServerType;
url: string;
userId: string | null;
username: string;
userId?: string | null;
username?: string;
};
export enum PlayerStatus {