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: [ plugins: [
new webpack.EnvironmentPlugin({ new webpack.EnvironmentPlugin({
NODE_ENV: 'production', 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)", "deletePlaylist": "delete $t(entity.playlist_one)",
"deselectAll": "deselect all", "deselectAll": "deselect all",
"editPlaylist": "edit $t(entity.playlist_one)", "editPlaylist": "edit $t(entity.playlist_one)",
"signIn": "sign in",
"goToPage": "go to page", "goToPage": "go to page",
"moveToBottom": "move to bottom", "moveToBottom": "move to bottom",
"moveToTop": "move to top", "moveToTop": "move to top",
@ -14,6 +15,7 @@
"removeFromFavorites": "remove from $t(entity.favorite_other)", "removeFromFavorites": "remove from $t(entity.favorite_other)",
"removeFromPlaylist": "remove from $t(entity.playlist_one)", "removeFromPlaylist": "remove from $t(entity.playlist_one)",
"removeFromQueue": "remove from queue", "removeFromQueue": "remove from queue",
"removeServer": "remove server",
"setRating": "set rating", "setRating": "set rating",
"toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor", "toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor",
"viewPlaylists": "view $t(entity.playlist_other)" "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 { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { ModuleRegistry } from '@ag-grid-community/core'; import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model'; 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 { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists'; import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store'; import {
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; 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 '@ag-grid-community/styles/ag-grid.css';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'; import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
@ -48,6 +59,57 @@ export const App = () => {
const { clearQueue, restoreQueue } = useQueueControls(); const { clearQueue, restoreQueue } = useQueueControls();
const remoteSettings = useRemoteSettings(); const remoteSettings = useRemoteSettings();
const textStyleRef = useRef<HTMLStyleElement>(); 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(); useDiscordRpc();
useEffect(() => { 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 { 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 = () => { export const ServerCredentialRequired = () => {
const { t } = useTranslation();
const currentServer = useCurrentServer(); 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 ( return (
<> <>
<Text> <Button
The selected server &apos;{currentServer?.name}&apos; requires an additional login leftIcon={<RiKeyFill />}
to access. variant="filled"
</Text> onClick={handleCredentialsModal}
<Text> >
Add your credentials in the &apos;manage servers&apos; menu or switch to a different {t('action.signIn', { postProcess: 'titleCase' })}
server. </Button>
</Text> <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 { RiMenuFill } from 'react-icons/ri';
import { Button, DropdownMenu, Text } from '/@/renderer/components'; import { Button, DropdownMenu, Text } from '/@/renderer/components';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu'; import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
export const ServerRequired = () => { export const ServerRequired = () => {
const { t } = useTranslation();
return ( return (
<> <>
<Text>No server selected.</Text> <Text>No server selected.</Text>
@ -12,7 +15,7 @@ export const ServerRequired = () => {
leftIcon={<RiMenuFill />} leftIcon={<RiMenuFill />}
variant="filled" variant="filled"
> >
Open menu {t('common.menu', { postProcess: 'titleCase' })}
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>

View File

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

View File

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

View File

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

View File

@ -69,14 +69,14 @@ export const AppMenu = () => {
/> />
), ),
size: 'sm', size: 'sm',
title: `Update session for "${server.name}"`, title: server.name,
}); });
}; };
const handleManageServersModal = () => { const handleManageServersModal = () => {
openModal({ openModal({
children: <ServerList />, 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 { Navigate, Outlet } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { getIsCredentialRequired } from '/@/renderer/features/action-required/routes/action-required-route';
const localSettings = isElectron() ? window.electron.localSettings : null; const localSettings = isElectron() ? window.electron.localSettings : null;
@ -17,9 +18,11 @@ export const AppOutlet = () => {
return true; return true;
}; };
const isCredentialRequired = getIsCredentialRequired(currentServer);
const isServerRequired = !currentServer; const isServerRequired = !currentServer;
const actions = [isServerRequired, isMpvRequired()]; const actions = [isServerRequired, isCredentialRequired, isMpvRequired()];
const isActionRequired = actions.some((c) => c); const isActionRequired = actions.some((c) => c);
return isActionRequired; return isActionRequired;

View File

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

View File

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