mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 20:40:21 +02:00
add global music folder selector
This commit is contained in:
@@ -1,76 +1,77 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import isElectron from 'is-electron';
|
||||
import { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import packageJson from '../../../../../package.json';
|
||||
|
||||
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
|
||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||
import { ServerSelectorItems } from '/@/renderer/features/sidebar/components/server-selector-items';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
useAppStore,
|
||||
useAppStoreActions,
|
||||
useAuthStoreActions,
|
||||
useCurrentServer,
|
||||
useServerList,
|
||||
useSidebarStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useAppStore, useAppStoreActions, useSidebarStore } from '/@/renderer/store';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerListItemWithCredential, ServerType } from '/@/shared/types/domain-types';
|
||||
|
||||
const browser = isElectron() ? window.api.browser : null;
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
interface BaseMenuItem {
|
||||
id: string;
|
||||
type: 'conditional-group' | 'conditional-item' | 'custom' | 'divider' | 'item';
|
||||
}
|
||||
|
||||
interface ConditionalGroupItem extends BaseMenuItem {
|
||||
condition: boolean;
|
||||
items: MenuItem[];
|
||||
type: 'conditional-group';
|
||||
}
|
||||
|
||||
interface ConditionalItem extends BaseMenuItem {
|
||||
condition: boolean;
|
||||
item: Omit<MenuItem, 'id' | 'type'>;
|
||||
type: 'conditional-item';
|
||||
}
|
||||
|
||||
interface CustomItem extends BaseMenuItem {
|
||||
component: ReactNode;
|
||||
type: 'custom';
|
||||
}
|
||||
|
||||
interface DividerItem extends BaseMenuItem {
|
||||
type: 'divider';
|
||||
}
|
||||
|
||||
type MenuItem = ConditionalGroupItem | ConditionalItem | CustomItem | DividerItem | RegularMenuItem;
|
||||
|
||||
interface RegularMenuItem extends BaseMenuItem {
|
||||
component?: 'a' | typeof Link;
|
||||
href?: string;
|
||||
icon?: keyof typeof import('/@/shared/components/icon/icon').AppIcon;
|
||||
iconColor?:
|
||||
| 'contrast'
|
||||
| 'default'
|
||||
| 'error'
|
||||
| 'info'
|
||||
| 'inherit'
|
||||
| 'muted'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warn';
|
||||
label: string;
|
||||
leftSection?: ReactNode;
|
||||
onClick?: () => void;
|
||||
rightSection?: ReactNode;
|
||||
target?: string;
|
||||
to?: string;
|
||||
type: 'item';
|
||||
}
|
||||
|
||||
export const AppMenu = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const currentServer = useCurrentServer();
|
||||
const serverList = useServerList();
|
||||
const { setCurrentServer } = useAuthStoreActions();
|
||||
const { collapsed } = useSidebarStore();
|
||||
const { privateMode } = useAppStore();
|
||||
const { setPrivateMode, setSideBar } = useAppStoreActions();
|
||||
|
||||
const handleSetCurrentServer = (server: ServerListItemWithCredential) => {
|
||||
navigate(AppRoute.HOME);
|
||||
setCurrentServer(server);
|
||||
};
|
||||
|
||||
const handleCredentialsModal = async (server: ServerListItemWithCredential) => {
|
||||
let password: null | string = null;
|
||||
|
||||
try {
|
||||
if (localSettings && server.savePassword) {
|
||||
password = await localSettings.passwordGet(server.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
openModal({
|
||||
children: server && (
|
||||
<EditServerForm
|
||||
isUpdate
|
||||
onCancel={closeAllModals}
|
||||
password={password}
|
||||
server={server}
|
||||
/>
|
||||
),
|
||||
size: 'sm',
|
||||
title: `Update session for "${server.name}"`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleManageServersModal = () => {
|
||||
openModal({
|
||||
children: <ServerList />,
|
||||
title: t('page.manageServers.title', { postProcess: 'titleCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleBrowserDevTools = () => {
|
||||
browser?.devtools();
|
||||
};
|
||||
@@ -103,124 +104,184 @@ export const AppMenu = () => {
|
||||
browser?.quit();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="arrowLeftS" />}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
{t('page.appMenu.goBack', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="arrowRightS" />}
|
||||
onClick={() => navigate(1)}
|
||||
>
|
||||
{t('page.appMenu.goForward', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{collapsed ? (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="panelRightOpen" />}
|
||||
onClick={handleExpandSidebar}
|
||||
>
|
||||
{t('page.appMenu.expandSidebar', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="panelRightClose" />}
|
||||
onClick={handleCollapseSidebar}
|
||||
>
|
||||
{t('page.appMenu.collapseSidebar', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
component={Link}
|
||||
leftSection={<Icon icon="settings" />}
|
||||
to={AppRoute.SETTINGS}
|
||||
>
|
||||
{t('page.appMenu.settings', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={handleManageServersModal}
|
||||
>
|
||||
{t('page.appMenu.manageServers', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{privateMode ? (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon color="error" icon="lock" />}
|
||||
onClick={handlePrivateModeOff}
|
||||
>
|
||||
{t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
) : (
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="lockOpen" />}
|
||||
onClick={handlePrivateModeOn}
|
||||
>
|
||||
{t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('page.appMenu.selectServer', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
{Object.keys(serverList).map((serverId) => {
|
||||
const server = serverList[serverId];
|
||||
const isNavidromeExpired =
|
||||
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||
const isJellyfinExpired = server.type === ServerType.JELLYFIN && !server.credential;
|
||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
||||
const menuConfig: MenuItem[] = [
|
||||
{
|
||||
condition: privateMode,
|
||||
id: 'private-mode',
|
||||
item: {
|
||||
icon: 'lock',
|
||||
iconColor: 'error',
|
||||
label: t('page.appMenu.privateModeOff', { postProcess: 'sentenceCase' }),
|
||||
onClick: handlePrivateModeOff,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: !privateMode,
|
||||
id: 'private-mode',
|
||||
item: {
|
||||
icon: 'lockOpen',
|
||||
label: t('page.appMenu.privateModeOn', { postProcess: 'sentenceCase' }),
|
||||
onClick: handlePrivateModeOn,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
id: 'divider-1',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
condition: collapsed,
|
||||
id: 'navigation-group',
|
||||
items: [
|
||||
{
|
||||
icon: 'arrowLeftS',
|
||||
id: 'go-back',
|
||||
label: t('page.appMenu.goBack', { postProcess: 'sentenceCase' }),
|
||||
onClick: () => navigate(-1),
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
icon: 'arrowRightS',
|
||||
id: 'go-forward',
|
||||
label: t('page.appMenu.goForward', { postProcess: 'sentenceCase' }),
|
||||
onClick: () => navigate(1),
|
||||
type: 'item',
|
||||
},
|
||||
],
|
||||
type: 'conditional-group',
|
||||
},
|
||||
{
|
||||
condition: collapsed,
|
||||
id: 'sidebar-toggle',
|
||||
item: {
|
||||
icon: 'panelRightOpen',
|
||||
id: 'expand-sidebar',
|
||||
label: t('page.appMenu.expandSidebar', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleExpandSidebar,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: !collapsed,
|
||||
id: 'sidebar-toggle',
|
||||
item: {
|
||||
icon: 'panelRightClose',
|
||||
id: 'collapse-sidebar',
|
||||
label: t('page.appMenu.collapseSidebar', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleCollapseSidebar,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
id: 'divider-2',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: Link,
|
||||
icon: 'settings',
|
||||
id: 'settings',
|
||||
label: t('page.appMenu.settings', { postProcess: 'sentenceCase' }),
|
||||
to: AppRoute.SETTINGS,
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
id: 'divider-3',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: <ServerSelectorItems />,
|
||||
id: 'server-selector',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: 'divider-4',
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
component: 'a',
|
||||
href: 'https://github.com/jeffvli/feishin/releases',
|
||||
icon: 'brandGitHub',
|
||||
id: 'version',
|
||||
label: t('page.appMenu.version', {
|
||||
postProcess: 'sentenceCase',
|
||||
version: packageJson.version,
|
||||
}),
|
||||
rightSection: <Icon icon="externalLink" />,
|
||||
target: '_blank',
|
||||
type: 'item',
|
||||
},
|
||||
{
|
||||
condition: isElectron(),
|
||||
id: 'devtools',
|
||||
item: {
|
||||
icon: 'appWindow',
|
||||
id: 'open-devtools',
|
||||
label: t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleBrowserDevTools,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
condition: isElectron(),
|
||||
id: 'quit',
|
||||
item: {
|
||||
icon: 'x',
|
||||
id: 'quit-app',
|
||||
label: t('page.appMenu.quit', { postProcess: 'sentenceCase' }),
|
||||
onClick: handleQuit,
|
||||
type: 'item',
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
];
|
||||
|
||||
const renderMenuItem = (item: MenuItem): ReactNode => {
|
||||
switch (item.type) {
|
||||
case 'conditional-group':
|
||||
if (!item.condition) return null;
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
key={`server-${server.id}`}
|
||||
leftSection={
|
||||
isSessionExpired ? (
|
||||
<Icon fill="error" icon="lock" />
|
||||
) : (
|
||||
<Icon
|
||||
color={server.id === currentServer?.id ? 'primary' : undefined}
|
||||
icon="server"
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isSessionExpired) return handleSetCurrentServer(server);
|
||||
return handleCredentialsModal(server);
|
||||
}}
|
||||
>
|
||||
{server.name}
|
||||
</DropdownMenu.Item>
|
||||
<div key={item.id}>{item.items.map((subItem) => renderMenuItem(subItem))}</div>
|
||||
);
|
||||
})}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
component="a"
|
||||
href="https://github.com/jeffvli/feishin/releases"
|
||||
leftSection={<Icon icon="brandGitHub" />}
|
||||
rightSection={<Icon icon="externalLink" />}
|
||||
target="_blank"
|
||||
>
|
||||
{t('page.appMenu.version', {
|
||||
postProcess: 'sentenceCase',
|
||||
version: packageJson.version,
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
{isElectron() && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="appWindow" />}
|
||||
onClick={handleBrowserDevTools}
|
||||
>
|
||||
{t('page.appMenu.openBrowserDevtools', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item leftSection={<Icon icon="x" />} onClick={handleQuit}>
|
||||
{t('page.appMenu.quit', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
case 'conditional-item':
|
||||
if (!item.condition) return null;
|
||||
return renderMenuItem(item.item as MenuItem);
|
||||
|
||||
case 'custom':
|
||||
return <div key={item.id}>{item.component}</div>;
|
||||
|
||||
case 'divider':
|
||||
return <DropdownMenu.Divider key={item.id} />;
|
||||
|
||||
case 'item': {
|
||||
const leftSection =
|
||||
item.leftSection ||
|
||||
(item.icon && <Icon color={item.iconColor} icon={item.icon} />);
|
||||
|
||||
const props: any = {
|
||||
key: item.id,
|
||||
leftSection,
|
||||
...(item.rightSection && { rightSection: item.rightSection }),
|
||||
...(item.onClick && { onClick: item.onClick }),
|
||||
...(item.component && { component: item.component }),
|
||||
...(item.to && { to: item.to }),
|
||||
...(item.href && { href: item.href }),
|
||||
...(item.target && { target: item.target }),
|
||||
};
|
||||
|
||||
return <DropdownMenu.Item {...props}>{item.label}</DropdownMenu.Item>;
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <>{menuConfig.map((item) => renderMenuItem(item))}</>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user