Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions download/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="robots" content="noindex" />
<meta name="description" content="Download Rail Map Toolkit for Windows, macOS and Linux." />
<title>Download desktop app - Rail Map Toolkit</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="./src/index.tsx"></script>
</body>
</html>
110 changes: 110 additions & 0 deletions download/src/components/app-root.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
.page {
flex: 1;
min-height: 0;
overflow: auto;
padding: clamp(24px, 4vw, 64px);
padding-bottom: max(env(safe-area-inset-bottom), 32px);
}

.hero {
display: grid;
grid-template-areas:
'copy'
'preview'
'downloads';
grid-template-columns: minmax(0, 1fr);
gap: 32px;
align-content: start;
width: 100%;
max-width: 1000px;
margin: 0 auto;
}

.copy {
grid-area: copy;
text-align: center;
}

.title {
font-size: clamp(2rem, 3.6vw, 3.5rem);
line-height: 1.2;
text-wrap: balance;
letter-spacing: -0.025em;
}

.subtitle {
margin-top: 20px;
font-size: var(--mantine-font-size-lg);
line-height: 1.7;
color: var(--mantine-color-dimmed);
}

.preview {
grid-area: preview;
align-self: center;
min-width: 0;
overflow: hidden;

img {
display: block;
width: 100%;
height: auto;
}
}

.downloads {
grid-area: downloads;
min-width: 0;
width: 100%;
max-width: 560px;
justify-self: center;
}

.platform {
display: grid;
grid-template-columns: 110px minmax(0, 1fr);
align-items: center;
gap: 12px;
padding: 14px 0;
border-bottom: 1px solid var(--mantine-color-default-border);

&:last-of-type {
border-bottom: 0;
}
}

.platform-name {
display: flex;
align-items: center;
gap: 10px;

svg {
flex-shrink: 0;
}
}

.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.version {
margin-top: 12px;
}

@media (max-width: 48em) {
.page {
padding: 32px 20px max(env(safe-area-inset-bottom), 24px);
}

.hero {
gap: 28px;
max-width: 600px;
}

.subtitle {
font-size: var(--mantine-font-size-md);
margin-top: 16px;
}
}
52 changes: 52 additions & 0 deletions download/src/components/app-root.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import '@testing-library/jest-dom';
import { cleanup, render, screen, within } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from '../../../src/i18n/config';
import AppRoot from './app-root';

describe('Desktop download page', () => {
afterEach(() => {
cleanup();
vi.useRealTimers();
});

it.each([
{ date: '2026-10-24T12:00:00Z', tag: '20261001', version: '26.10.1' },
{ date: '2026-10-31T11:59:59Z', tag: '20261001', version: '26.10.1' },
{ date: '2026-10-31T12:00:00Z', tag: '20261101', version: '26.11.1' },
{ date: '2026-12-31T11:59:59Z', tag: '20261201', version: '26.12.1' },
{ date: '2026-12-31T12:00:00Z', tag: '20270101', version: '27.1.1' },
])('Uses the UTC+12 monthly release at $date', ({ date, tag, version }) => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(date));
render(
<I18nextProvider i18n={i18n}>
<AppRoot />
</I18nextProvider>
);

const base = `https://gh.monlor.com/https://github.com/railmapgen/railmapgen.github.io/releases/download/tauri-${tag}/Rail.Map.Toolkit_${version}_`;
const expectedLinks = [
{ name: 'Download Windows x64', file: 'x64-setup.exe' },
{ name: 'Download Windows ARM64', file: 'arm64-setup.exe' },
{ name: 'Download macOS Apple Silicon', file: 'aarch64.dmg' },
{ name: 'Download macOS Intel', file: 'x64.dmg' },
{ name: 'Download Linux x64 AppImage', file: 'amd64.AppImage' },
];
expect(screen.getAllByRole('link')).toHaveLength(expectedLinks.length);
for (const { name, file } of expectedLinks) {
expect(screen.getByRole('link', { name })).toHaveAttribute('href', base + file);
}
expect(screen.getByText(`Version ${version}`)).toBeVisible();

for (const platform of ['Android', 'iOS']) {
const row = within(screen.getByRole('region', { name: platform }));
expect(row.getByText('Coming soon')).toBeVisible();
expect(row.queryByRole('link')).not.toBeInTheDocument();
expect(row.queryByRole('button')).not.toBeInTheDocument();
}
expect(screen.getByRole('img')).toHaveAccessibleName(
'Rail Map Toolkit displaying a rail network map in Rail Map Painter'
);
});
});
109 changes: 109 additions & 0 deletions download/src/components/app-root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Button, Text, Title } from '@mantine/core';
import { RMMantineProvider, RMWindow, RMWindowHeader } from '@railmapgen/mantine-components';
import { useTranslation } from 'react-i18next';
import { FaAndroid, FaApple, FaLinux, FaWindows } from 'react-icons/fa';
import { MdDownload } from 'react-icons/md';
import classes from './app-root.module.css';

const desktopPlatforms = [
{
name: 'Windows',
Icon: FaWindows,
downloads: [
{ label: 'x64', suffix: 'x64-setup.exe' },
{ label: 'ARM64', suffix: 'arm64-setup.exe' },
],
},
{
name: 'macOS',
Icon: FaApple,
downloads: [
{ label: 'Apple Silicon', suffix: 'aarch64.dmg' },
{ label: 'Intel', suffix: 'x64.dmg' },
],
},
{
name: 'Linux',
Icon: FaLinux,
downloads: [{ label: 'x64 AppImage', suffix: 'amd64.AppImage' }],
},
];

export default function AppRoot() {
const { t } = useTranslation();
const now = new Date(Date.now() + 12 * 60 * 60 * 1000);
const year = now.getUTCFullYear();
const month = now.getUTCMonth() + 1;
const tag = `${year}${String(month).padStart(2, '0')}01`;
const version = `${String(year).slice(-2)}.${month}.1`;
const downloadBase = `https://gh.monlor.com/https://github.com/railmapgen/railmapgen.github.io/releases/download/tauri-${tag}/Rail.Map.Toolkit_${version}_`;

return (
<RMMantineProvider>
<RMWindow>
<RMWindowHeader>
<Title>{t('Download desktop app')}</Title>
</RMWindowHeader>
<main className={classes.page}>
<div className={classes.hero}>
<div className={classes.copy}>
<Title order={2} className={classes.title}>
{t('DownloadPage.title')}
</Title>
<Text className={classes.subtitle}>{t('DownloadPage.subtitle')}</Text>
</div>

<div className={classes.preview}>
<img src="/images/rail-map-toolkit.png" alt={t('DownloadPage.screenshot')} />
</div>

<div className={classes.downloads}>
{desktopPlatforms.map(({ name, Icon, downloads }) => (
<section key={name} aria-label={name} className={classes.platform}>
<div className={classes['platform-name']}>
<Icon aria-hidden size={22} />
<Text fw={600}>{name}</Text>
</div>
<div className={classes.buttons}>
{downloads.map(({ label, suffix }) => (
<Button
key={suffix}
component="a"
href={downloadBase + suffix}
target="_blank"
rel="noreferrer"
aria-label={`${t('Download')} ${name} ${label}`}
variant="light"
radius="md"
rightSection={<MdDownload aria-hidden size={18} />}
>
{label}
</Button>
))}
</div>
</section>
))}
{[
{ name: 'Android', Icon: FaAndroid },
{ name: 'iOS', Icon: FaApple },
].map(({ name, Icon }) => (
<section key={name} aria-label={name} className={classes.platform}>
<div className={classes['platform-name']}>
<Icon aria-hidden size={22} />
<Text fw={600}>{name}</Text>
</div>
<Text c="dimmed" size="sm">
{t('DownloadPage.comingSoon')}
</Text>
</section>
))}
<Text c="dimmed" size="xs" ta="right" className={classes.version}>
{t('DownloadPage.version')} {version}
</Text>
</div>
</div>
</main>
</RMWindow>
</RMMantineProvider>
);
}
18 changes: 18 additions & 0 deletions download/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import '../../src/index.css';
import rmgRuntime from '@railmapgen/rmg-runtime';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import i18n from '../../src/i18n/config';
import AppRoot from './components/app-root';

rmgRuntime.ready().then(() => {
createRoot(document.getElementById('root') as HTMLDivElement).render(
<StrictMode>
<I18nextProvider i18n={i18n}>
<AppRoot />
</I18nextProvider>
</StrictMode>
);
rmgRuntime.injectUITools();
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"dev": "vite --host",
"build": "tsc && vite build",
"test": "vitest run --coverage",
"lint": "eslint src contributors runtime-demo",
"lint:fix": "eslint src contributors runtime-demo --fix",
"lint": "eslint src contributors download runtime-demo",
"lint:fix": "eslint src contributors download runtime-demo --fix",
"preview": "vite preview --host"
},
"version": "3.2.43"
Expand Down
Binary file added public/images/rail-map-toolkit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/components/hook/use-open-temp-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import rmgRuntime from '@railmapgen/rmg-runtime';
import { useRootDispatch, useRootSelector } from '../../redux';
import { openTempApp, toggleMenu } from '../../redux/app/app-slice';
import useSmMediaQuery from './use-sm-media-query';

export default function useOpenTempApp() {
const dispatch = useRootDispatch();
const isShowMenu = useRootSelector(state => state.app.isShowMenu);
const smMediaQuery = useSmMediaQuery();

return (appId: string) => {
dispatch(openTempApp(appId));
if (!smMediaQuery && isShowMenu) {
dispatch(toggleMenu());
rmgRuntime.toggleNavMenu(false);
}
};
}
49 changes: 49 additions & 0 deletions src/components/menu/main-view/link-item-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NavLink, NavLinkProps } from '@mantine/core';
import rmgRuntime from '@railmapgen/rmg-runtime';
import { useTranslation } from 'react-i18next';
import { MdChevronRight, MdOpenInNew } from 'react-icons/md';
import { useRootSelector } from '../../../redux';
import { assetEnablement, tempAssets } from '../../../util/asset-enablements';
import { Events } from '../../../util/constants';
import useOpenTempApp from '../../hook/use-open-temp-app';
import classes from './app-item-button.module.css';

type LinkItemButtonProps = {
linkId: string;
};

export default function LinkItemButton({ linkId }: LinkItemButtonProps) {
const { t } = useTranslation();
const activeTab = useRootSelector(state => state.app.activeTab);
const openTempApp = useOpenTempApp();
const link = assetEnablement[linkId];
const isTempApp = Object.hasOwn(tempAssets, linkId);

const handleOpenLink = () => {
rmgRuntime.event(Events.OPEN_LINK, { id: linkId });
if (isTempApp) {
openTempApp(linkId);
}
};

const sharedProps: NavLinkProps = {
variant: 'filled',
label: t(link.name),
classNames: { root: classes.button, label: classes.label },
onClick: handleOpenLink,
};

if (isTempApp) {
return (
<NavLink
{...sharedProps}
component="button"
type="button"
active={activeTab === linkId}
rightSection={<MdChevronRight />}
/>
);
}

return <NavLink {...sharedProps} href={link.url} target="_blank" rightSection={<MdOpenInNew />} />;
}
Loading
Loading