Skip to content

Commit

Permalink
add svg and undo&redo
Browse files Browse the repository at this point in the history
  • Loading branch information
langonginc committed May 12, 2024
1 parent 26f587b commit 2272757
Show file tree
Hide file tree
Showing 20 changed files with 788 additions and 74 deletions.
2 changes: 1 addition & 1 deletion src/components/app-root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { Button, Flex } from '@chakra-ui/react';
import WindowHeader from './window-header';
import WindowHeader from './header/window-header';
import { RmgPage, RmgErrorBoundary, RmgThemeProvider, RmgWindow } from '@railmapgen/rmg-components';
import { useTranslation } from 'react-i18next';
import { useRootDispatch, useRootSelector } from '../redux';
Expand Down
45 changes: 45 additions & 0 deletions src/components/header/about-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Flex,
Image,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Text,
} from '@chakra-ui/react';
import rmgRuntime from '@railmapgen/rmg-runtime';
import { useTranslation } from 'react-i18next';

const AboutModal = (props: { isOpen: boolean; onClose: () => void }) => {
const { isOpen, onClose } = props;
const { t } = useTranslation();
const appVersion = rmgRuntime.getAppVersion();

return (
<Modal isOpen={isOpen} onClose={onClose} size="xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('header.about.title')}</ModalHeader>
<ModalCloseButton />

<ModalBody paddingBottom={10}>
<Flex direction="row">
<Image boxSize="128px" src={import.meta.env.BASE_URL + '/logo192.png'} />
<Flex direction="column" width="100%" alignItems="center" justifyContent="center">
<Text fontSize="xl" as="b">
{t('RMP Style Generator')}
</Text>
<Text>{appVersion}</Text>
<Text />
<Text fontSize="sm">{t('header.about.railmapgen')}</Text>
</Flex>
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
};

export default AboutModal;

Check warning on line 45 in src/components/header/about-modal.tsx

View workflow job for this annotation

GitHub Actions / build

Insert `⏎`
84 changes: 84 additions & 0 deletions src/components/header/import-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SvgsAttrs, SvgsType } from '../../constants/svgs';
import svgs from '../svgs/svgs';
import { Id, SvgsElem } from '../../constants/constants';
import { nanoid, roundToNearestN } from '../../util/helper';
import { addSvg } from '../../redux/param/param-slice';
import { useRootDispatch } from '../../redux';

export const ImportFromSvg = (props: { isOpen: boolean; onClose: () => void }) => {
const { isOpen, onClose } = props;
const { t } = useTranslation();
const dispatch = useRootDispatch();

const [svgString, setSvgString] = React.useState('');
const field: RmgFieldsField[] = [
{
label: 'SVG',
type: 'textarea',
value: '',
onChange: v => setSvgString(v),
},
];

const handleImport = () => {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');
for (const svgTag of Object.values(SvgsType)) {
const elems = svgDoc.getElementsByTagName(svgTag);
for (let i = 0; i < elems.length; i++) {
const elem = elems[i];
const id: Id = `id_${nanoid(10)}`;
const x = elem.getAttribute(svgTag === 'circle' ? 'cx' : 'x') ?? '0';
const y = elem.getAttribute(svgTag === 'circle' ? 'cy' : 'y') ?? '0';
const attr = svgs[svgTag].inputFromSvg(elem);
console.log(attr);
const newElem: SvgsElem<SvgsAttrs[keyof SvgsAttrs]> = {
id,
type: svgTag,
isCore: false,
x,
y,
attrs: attr,
};
dispatch(addSvg(newElem));
}
}
onClose();
};

return (
<Modal isOpen={isOpen} onClose={onClose} size="xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>Import from SVG</ModalHeader>
<ModalCloseButton />

<ModalBody paddingBottom={10}>
<RmgFields fields={field} minW="full" />
</ModalBody>

<ModalFooter>
<Button colorScheme="blue" variant="outline" mr="1" onClick={onClose}>
{t('cancel')}
</Button>
<Button colorScheme="red" mr="1" onClick={handleImport}>
{t('apply')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

Check warning on line 84 in src/components/header/import-modal.tsx

View workflow job for this annotation

GitHub Actions / build

Insert `;⏎`
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { render } from '../test-utils';
import { render } from '../../test-utils';
import { screen } from '@testing-library/react';
import WindowHeader from './window-header';

describe('WindowHeader', () => {
it('Can render window header', () => {
render(<WindowHeader />);

expect(screen.getByRole('heading').textContent).toContain('Seed Project');
expect(screen.getByRole('heading').textContent).toContain('RMP Style Generator');
});
});
83 changes: 83 additions & 0 deletions src/components/header/window-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Heading, HStack, IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { RmgEnvBadge, RmgWindowHeader } from '@railmapgen/rmg-components';
import rmgRuntime from '@railmapgen/rmg-runtime';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MdHelp, MdRedo, MdSave, MdUndo, MdUpload } from 'react-icons/md';
import AboutModal from './about-modal';
import { ZoomPopover } from './zoom-popover';
import { ImportFromSvg } from './import-modal';
import { useRootDispatch, useRootSelector } from '../../redux';
import { setParam } from '../../redux/param/param-slice';
import { backupParam, backupRedo, backupRemove, backupUndo } from '../../redux/runtime/runtime-slice';

export default function WindowHeader() {
const { t } = useTranslation();
const dispatch = useRootDispatch();
const { history, undo_history } = useRootSelector(store => store.runtime);
const param = useRootSelector(store => store.param);


Check warning on line 20 in src/components/header/window-header.tsx

View workflow job for this annotation

GitHub Actions / build

Delete `⏎`
const environment = rmgRuntime.getEnv();
const appVersion = rmgRuntime.getAppVersion();

const [openAbout, setOpenAbout] = React.useState(false);
const [openImportSvg, setOpenImportSvg] = React.useState(false);

return (
<RmgWindowHeader>
<Heading as="h4" size="md">
{t('RMP Style Generator')}
</Heading>
<RmgEnvBadge environment={environment} version={appVersion} />

<HStack ml="auto">
<IconButton
size="sm"
variant="ghost"
aria-label="Undo"
icon={<MdUndo />}
isDisabled={history.length === 0}
onClick={() => {
dispatch(backupUndo(param));
dispatch(setParam(history[history.length - 1]));
dispatch(backupRemove());
}}
/>
<IconButton
size="sm"
variant="ghost"
aria-label="Redo"
icon={<MdRedo />}
isDisabled={undo_history.length === 0}
onClick={() => {
console.log(undo_history);
dispatch(backupParam(param));
dispatch(setParam(undo_history[undo_history.length - 1]));
dispatch(backupRedo());
console.log(param);
}}
/>
<ZoomPopover />
<Menu id="download">
<MenuButton as={IconButton} size="sm" variant="ghost" icon={<MdUpload />} />
<MenuList>
<MenuItem icon={<MdSave />} onClick={() => setOpenImportSvg(true)}>
Import from SVG
</MenuItem>
</MenuList>
</Menu>
<IconButton
size="sm"
variant="ghost"
aria-label={t('Help')}
title={t('Help')}
icon={<MdHelp />}
onClick={() => setOpenAbout(true)}
/>
</HStack>
<AboutModal isOpen={openAbout} onClose={() => setOpenAbout(false)} />
<ImportFromSvg isOpen={openImportSvg} onClose={() => setOpenImportSvg(false)} />
</RmgWindowHeader>
);
}
51 changes: 51 additions & 0 deletions src/components/header/zoom-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@chakra-ui/react';
import { MdZoomOut, MdZoomIn } from 'react-icons/md';
import { RmgFields, RmgFieldsField } from '@railmapgen/rmg-components';
import { useRootSelector, useRootDispatch } from '../../redux';
import { setSvgViewBoxZoom } from '../../redux/runtime/runtime-slice';

/**
* A zoom control displayed in popover component.
* This will greatly decrease the width of the header in mobile device.
*/
export const ZoomPopover = () => {
const [isOpen, setIsOpen] = React.useState(false);

const { svgViewBoxZoom } = useRootSelector(state => state.runtime);
const dispatch = useRootDispatch();

const fields: RmgFieldsField[] = [
{
type: 'slider',
label: '',
value: 400 - svgViewBoxZoom,
min: 10,
max: 390,
step: 1,
onChange: value => dispatch(setSvgViewBoxZoom(400 - value)),
leftIcon: <MdZoomOut />,
rightIcon: <MdZoomIn />,
minW: 160,
},
];

return (
<Popover isOpen={isOpen} onOpen={() => setIsOpen(true)} onClose={() => setIsOpen(false)}>
<PopoverTrigger>
<IconButton
aria-label="zoom"
variant="ghost"
size="sm"
icon={<MdZoomIn />}
onClick={() => setIsOpen(!isOpen)}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<RmgFields fields={fields} noLabel />
</PopoverBody>
</PopoverContent>
</Popover>
);
};
30 changes: 23 additions & 7 deletions src/components/panel/details-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
setComponentValue,
} from '../../redux/param/param-slice';
import { ComponentsType, ComponentsTypeOptions } from '../../constants/components';
import { openPaletteAppClip } from '../../redux/runtime/runtime-slice';
import { backupParam, openPaletteAppClip } from '../../redux/runtime/runtime-slice';
import { nanoid } from '../../util/helper';
import ColourUtil from './colour-util';

Expand All @@ -48,6 +48,7 @@ export function DetailsComponents() {
};

const handleAddNewComponent = () => {
dispatch(backupParam(param));
dispatch(
addComponent({
id: nanoid(),
Expand All @@ -61,6 +62,7 @@ export function DetailsComponents() {
const handleMove = (index: number, d: number) => {
const dest = index + d;
if (dest >= 0 && dest < param.components.length) {
dispatch(backupParam(param));
dispatch(
setComponents(
param.components
Expand All @@ -83,22 +85,29 @@ export function DetailsComponents() {
label: 'Label',
type: 'input',
value: label,
onChange: v =>
dispatch(setComponentValue({ index: index, value: { ...c, label: v.replaceAll(' ', '') } })),
onChange: v => {
dispatch(backupParam(param));
dispatch(setComponentValue({ index: index, value: { ...c, label: v.replaceAll(' ', '') } }));
},
},
{
label: 'Type',
type: 'select',
options: ComponentsTypeOptions,
value: type,
onChange: v =>
dispatch(setComponentValue({ index: index, value: { ...c, type: v as ComponentsType } })),
onChange: v => {
dispatch(backupParam(param));
dispatch(setComponentValue({ index: index, value: { ...c, type: v as ComponentsType } }));
},
},
{
label: 'Default value',
type: 'input',
value: defaultValue,
onChange: v => dispatch(setComponentValue({ index: index, value: { ...c, defaultValue: v } })),
onChange: v => {
dispatch(backupParam(param));
dispatch(setComponentValue({ index: index, value: { ...c, defaultValue: v } }));
},
},
{
label: '',
Expand All @@ -112,7 +121,13 @@ export function DetailsComponents() {
<Button size="md" onClick={() => handleMove(index, 1)}>
<MdArrowDownward />
</Button>
<Button size="md" onClick={() => dispatch(deleteComponent(index))}>
<Button
size="md"
onClick={() => {
dispatch(backupParam(param));
dispatch(deleteComponent(index));
}}
>
<MdClose />
</Button>
</>
Expand All @@ -138,6 +153,7 @@ export function DetailsComponents() {
const [isThemeRequested, setIsThemeRequested] = React.useState(false);
React.useEffect(() => {
if (isThemeRequested && output) {
dispatch(backupParam(param));
dispatch(setColor({ ...param.color!, defaultValue: output }));
setIsThemeRequested(false);
}
Expand Down
Loading

0 comments on commit 2272757

Please sign in to comment.