Migrate to Mantine v8 and Design Changes (#961)

* mantine v8 migration

* various design changes and improvements
This commit is contained in:
Jeff 2025-06-24 00:04:36 -07:00 committed by GitHub
parent bea55d48a8
commit c1330d92b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
473 changed files with 12469 additions and 11607 deletions

View file

@ -1,4 +1,3 @@
import { Checkbox, Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks';
import { closeAllModals } from '@mantine/modals';
@ -8,23 +7,74 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { api } from '/@/renderer/api';
import { Button, PasswordInput, SegmentedControl, TextInput, toast } from '/@/renderer/components';
import JellyfinIcon from '/@/renderer/features/servers/assets/jellyfin.png';
import NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png';
import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png';
import { useAuthStoreActions } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { Group } from '/@/shared/components/group/group';
import { PasswordInput } from '/@/shared/components/password-input/password-input';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Stack } from '/@/shared/components/stack/stack';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast';
import { AuthenticationResponse } from '/@/shared/types/domain-types';
import { ServerType, toServerType } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : null;
const SERVER_TYPES = [
{ label: 'Jellyfin', value: ServerType.JELLYFIN },
{ label: 'Navidrome', value: ServerType.NAVIDROME },
{ label: 'Subsonic', value: ServerType.SUBSONIC },
];
interface AddServerFormProps {
onCancel: () => void;
onCancel: (() => void) | null;
}
function ServerIconWithLabel({ icon, label }: { icon: string; label: string }) {
return (
<Stack
align="center"
justify="center"
>
<img
height="50"
src={icon}
width="50"
/>
<Text>{label}</Text>
</Stack>
);
}
const SERVER_TYPES = [
{
label: (
<ServerIconWithLabel
icon={JellyfinIcon}
label="Jellyfin"
/>
),
value: ServerType.JELLYFIN,
},
{
label: (
<ServerIconWithLabel
icon={NavidromeIcon}
label="Navidrome"
/>
),
value: ServerType.NAVIDROME,
},
{
label: (
<ServerIconWithLabel
icon={SubsonicIcon}
label="OpenSubsonic"
/>
),
value: ServerType.SUBSONIC,
},
];
export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const { t } = useTranslation();
const focusTrapRef = useFocusTrap(true);
@ -40,7 +90,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
type:
(localSettings
? localSettings.env.SERVER_TYPE
: toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN,
: toServerType(window.SERVER_TYPE)) ?? ServerType.NAVIDROME,
url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://',
username: '',
},
@ -131,6 +181,8 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
<SegmentedControl
data={SERVER_TYPES}
disabled={Boolean(serverLock)}
p="md"
withItemsBorders={false}
{...form.getInputProps('type')}
/>
<Group grow>
@ -186,13 +238,18 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
{...form.getInputProps('legacyAuth', { type: 'checkbox' })}
/>
)}
<Group position="right">
<Button
onClick={onCancel}
variant="subtle"
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>
<Group
grow
justify="flex-end"
>
{onCancel && (
<Button
onClick={onCancel}
variant="subtle"
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>
)}
<Button
disabled={isSubmitDisabled}
loading={isLoading}

View file

@ -1,18 +1,24 @@
import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useFocusTrap } from '@mantine/hooks';
import { closeAllModals } from '@mantine/modals';
import isElectron from 'is-electron';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RiInformationLine } from 'react-icons/ri';
import i18n from '/@/i18n/i18n';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { Button, Checkbox, PasswordInput, TextInput, toast, Tooltip } from '/@/renderer/components';
import { queryClient } from '/@/renderer/lib/react-query';
import { useAuthStoreActions } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { PasswordInput } from '/@/shared/components/password-input/password-input';
import { Stack } from '/@/shared/components/stack/stack';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { toast } from '/@/shared/components/toast/toast';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { AuthenticationResponse, ServerListItem, ServerType } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
@ -27,9 +33,10 @@ interface EditServerFormProps {
const ModifiedFieldIndicator = () => {
return (
<Tooltip label={i18n.t('common.modified', { postProcess: 'titleCase' }) as string}>
<span>
<RiInformationLine color="red" />
</span>
<Icon
color="warn"
icon="info"
/>
</Tooltip>
);
};
@ -185,7 +192,7 @@ export const EditServerForm = ({ isUpdate, onCancel, password, server }: EditSer
})}
/>
)}
<Group position="right">
<Group justify="flex-end">
<Button
onClick={onCancel}
variant="subtle"

View file

@ -1,14 +1,17 @@
import { Divider, Group, Stack } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RiDeleteBin2Line, RiEdit2Fill } from 'react-icons/ri';
import { Button, Text, TimeoutButton } from '/@/renderer/components';
import { EditServerForm } from '/@/renderer/features/servers/components/edit-server-form';
import { ServerSection } from '/@/renderer/features/servers/components/server-section';
import { useAuthStoreActions } from '/@/renderer/store';
import { Button, TimeoutButton } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { Table } from '/@/shared/components/table/table';
import { ServerListItem as ServerItem } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
@ -54,17 +57,7 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
return (
<Stack>
<ServerSection
title={
<Group position="apart">
<Text>
{t('page.manageServers.serverDetails', {
postProcess: 'sentenceCase',
})}
</Text>
</Group>
}
>
<ServerSection title={null}>
{edit ? (
<EditServerForm
onCancel={() => editHandlers.toggle()}
@ -73,36 +66,41 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
/>
) : (
<Stack>
<Group noWrap>
<Stack>
<Text>
{t('page.manageServers.url', {
postProcess: 'sentenceCase',
})}
</Text>
<Text>
{t('page.manageServers.username', {
postProcess: 'sentenceCase',
})}
</Text>
</Stack>
<Stack>
<Text>{server.url}</Text>
<Text>{server.username}</Text>
</Stack>
</Group>
<Table
layout="fixed"
variant="vertical"
withTableBorder
>
<Table.Tbody>
<Table.Tr>
<Table.Th>
{t('page.manageServers.url', {
postProcess: 'sentenceCase',
})}
</Table.Th>
<Table.Td>{server.url}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Th>
{t('page.manageServers.username', {
postProcess: 'sentenceCase',
})}
</Table.Th>
<Table.Td>{server.username}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
<Group grow>
<Button
leftIcon={<RiEdit2Fill />}
leftSection={<Icon icon="edit" />}
onClick={() => handleEdit()}
tooltip={{
label: t('page.manageServers.editServerDetailsTooltip', {
postProcess: 'sentenceCase',
}),
}}
variant="subtle"
>
{t('common.edit')}
{t('common.edit', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
@ -110,9 +108,9 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
</ServerSection>
<Divider my="sm" />
<TimeoutButton
leftIcon={<RiDeleteBin2Line />}
leftSection={<Icon icon="delete" />}
timeoutProps={{ callback: handleDeleteServer, duration: 1000 }}
variant="subtle"
variant="state-error"
>
{t('page.manageServers.removeServer', { postProcess: 'sentenceCase' })}
</TimeoutButton>

View file

@ -1,16 +1,25 @@
import { Divider, Group, Stack } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { openContextModal } from '@mantine/modals';
import isElectron from 'is-electron';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { RiAddFill, RiServerFill } from 'react-icons/ri';
import { Accordion, Button, ContextModalVars, Switch, Text } from '/@/renderer/components';
import JellyfinLogo from '/@/renderer/features/servers/assets/jellyfin.png';
import NavidromeLogo from '/@/renderer/features/servers/assets/navidrome.png';
import OpenSubsonicLogo from '/@/renderer/features/servers/assets/opensubsonic.png';
import { AddServerForm } from '/@/renderer/features/servers/components/add-server-form';
import { ServerListItem } from '/@/renderer/features/servers/components/server-list-item';
import { useCurrentServer, useServerList } from '/@/renderer/store';
import { titleCase } from '/@/renderer/utils';
import { Accordion } from '/@/shared/components/accordion/accordion';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { ContextModalVars } from '/@/shared/components/modal/modal';
import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text';
import { ServerType } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null;
@ -59,27 +68,6 @@ export const ServerList = () => {
return (
<>
<Group
mb={10}
position="right"
sx={{
position: 'absolute',
right: 55,
transform: 'translateY(-3.5rem)',
zIndex: 2000,
}}
>
<Button
autoFocus
compact
leftIcon={<RiAddFill size={15} />}
onClick={handleAddServerModal}
size="sm"
variant="filled"
>
{t('form.addServer.title', { postProcess: 'titleCase' })}
</Button>
</Group>
<Stack>
<Accordion variant="separated">
{Object.keys(serverListQuery)?.map((serverId) => {
@ -89,10 +77,23 @@ export const ServerList = () => {
key={server.id}
value={server.name}
>
<Accordion.Control icon={<RiServerFill size={15} />}>
<Group position="apart">
<Text weight={server.id === currentServer?.id ? 800 : 400}>
{titleCase(server?.type)} - {server?.name}
<Accordion.Control>
<Group>
<img
src={
server.type === ServerType.NAVIDROME
? NavidromeLogo
: server.type === ServerType.JELLYFIN
? JellyfinLogo
: OpenSubsonicLogo
}
style={{
height: 'var(--theme-font-size-lg)',
width: 'var(--theme-font-size-lg)',
}}
/>
<Text fw={server.id === currentServer?.id ? 600 : 400}>
{server?.name}
</Text>
</Group>
</Accordion.Control>
@ -102,6 +103,18 @@ export const ServerList = () => {
</Accordion.Item>
);
})}
<Group
grow
pt="md"
>
<Button
autoFocus
leftSection={<Icon icon="add" />}
onClick={handleAddServerModal}
>
{t('form.addServer.title', { postProcess: 'titleCase' })}
</Button>
</Group>
</Accordion>
{isElectron() && (
<>

View file

@ -1,25 +1,17 @@
import React from 'react';
import styled from 'styled-components';
import React, { Fragment } from 'react';
import { Text } from '/@/renderer/components';
import { Text } from '/@/shared/components/text/text';
interface ServerSectionProps {
children: React.ReactNode;
title: React.ReactNode | string;
}
const Container = styled.div``;
const Section = styled.div`
padding: 1rem;
border: 1px dashed var(--generic-border-color);
`;
export const ServerSection = ({ children, title }: ServerSectionProps) => {
return (
<Container>
<Fragment>
{React.isValidElement(title) ? title : <Text>{title}</Text>}
<Section>{children}</Section>
</Container>
<div style={{ padding: '1rem' }}>{children}</div>
</Fragment>
);
};