add in-app release notes

This commit is contained in:
jeffvli
2025-12-02 17:14:58 -08:00
parent cab16b0893
commit a35577444b
4 changed files with 282 additions and 57 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import i18n from '/@/i18n/i18n';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
import { ReleaseNotesModal } from './release-notes-modal';
import { AppRouter } from '/@/renderer/router/app-router';
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
@@ -89,7 +89,7 @@ export const App = () => {
<AppRouter />
</PlayerProvider>
</WebAudioContext.Provider>
<IsUpdatedDialog />
<ReleaseNotesModal />
</MantineProvider>
);
};
-55
View File
@@ -1,55 +0,0 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import packageJson from '../../package.json';
import { Button } from '/@/shared/components/button/button';
import { Dialog } from '/@/shared/components/dialog/dialog';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
export function IsUpdatedDialog() {
const { version } = packageJson;
const { t } = useTranslation();
const [value, setValue] = useLocalStorage({ key: 'version' });
const handleDismiss = useCallback(() => {
setValue(version);
}, [setValue, version]);
return (
<Dialog
opened={value !== version}
position={{ bottom: '5rem', right: '1rem' }}
styles={{
root: {
marginBottom: '50px',
right: '1rem',
},
}}
>
<Stack>
<Text>{t('common.newVersion', { postProcess: 'sentenceCase', version })}</Text>
<Group justify="flex-end" wrap="nowrap">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
onClick={handleDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
variant="filled"
>
{t('common.viewReleaseNotes', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={handleDismiss} variant="default">
{t('common.dismiss', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
</Dialog>
);
}
@@ -0,0 +1,86 @@
.markdown-content {
color: var(--mantine-color-text);
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: var(--mantine-spacing-md);
margin-bottom: var(--mantine-spacing-xs);
font-weight: 600;
line-height: 1.25;
}
.markdown-content h1 {
font-size: 2em;
}
.markdown-content h2 {
font-size: 1.5em;
}
.markdown-content h3 {
font-size: 1.25em;
}
.markdown-content p {
margin-top: 0;
margin-bottom: var(--mantine-spacing-sm);
}
.markdown-content ul,
.markdown-content ol {
padding-left: var(--mantine-spacing-xl);
margin-top: 0;
margin-bottom: var(--mantine-spacing-sm);
}
.markdown-content li {
margin-bottom: var(--mantine-spacing-xs);
}
.markdown-content code {
padding: 2px 6px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
background-color: var(--mantine-color-dark-6);
border-radius: var(--mantine-radius-sm);
}
.markdown-content pre {
padding: var(--mantine-spacing-md);
margin-top: var(--mantine-spacing-md);
margin-bottom: var(--mantine-spacing-md);
overflow-x: auto;
background-color: var(--mantine-color-dark-6);
border-radius: var(--mantine-radius-md);
}
.markdown-content pre code {
padding: 0;
background-color: transparent;
}
.markdown-content blockquote {
padding-left: var(--mantine-spacing-md);
margin-top: var(--mantine-spacing-md);
margin-bottom: var(--mantine-spacing-md);
border-left: 3px solid var(--mantine-color-gray-6);
}
.markdown-content a {
color: var(--mantine-color-blue-6);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content img {
max-width: 100%;
border-radius: var(--mantine-radius-md);
}
+194
View File
@@ -0,0 +1,194 @@
import { closeAllModals, openModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import DOMPurify from 'dompurify';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import packageJson from '../../package.json';
import styles from './release-notes-modal.module.css';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
interface ReleaseNotesContentProps {
onDismiss: () => void;
version: string;
}
const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => {
const { t } = useTranslation();
// Fetch release notes from GitHub API
const {
data: releaseData,
isError,
isLoading,
} = useQuery({
queryFn: async () => {
const response = await axios.get(
`https://api.github.com/repos/jeffvli/feishin/releases/tags/v${version}`,
);
return response.data;
},
queryKey: ['github-release', version],
retry: 2,
});
// Convert markdown to HTML using GitHub's markdown API
const { data: htmlContent, isLoading: isConverting } = useQuery({
enabled: !!releaseData?.body,
queryFn: async () => {
const response = await axios.post(
'https://api.github.com/markdown',
{
mode: 'gfm',
text: releaseData.body,
},
{
headers: {
'Content-Type': 'application/json',
},
responseType: 'text',
},
);
return response.data;
},
queryKey: ['github-markdown', releaseData?.body],
retry: 2,
});
const sanitizedHtml = useMemo(() => {
if (!htmlContent) return '';
return DOMPurify.sanitize(htmlContent, {
ALLOWED_ATTR: ['alt', 'href', 'src', 'title'],
ALLOWED_TAGS: [
'a',
'blockquote',
'br',
'code',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'img',
'li',
'ol',
'p',
'pre',
'strong',
'u',
'ul',
],
});
}, [htmlContent]);
if (isLoading || isConverting) {
return (
<Center h={400}>
<Stack align="center" gap="md">
<Spinner />
<Text size="sm">{t('common.loading', { postProcess: 'sentenceCase' })}</Text>
</Stack>
</Center>
);
}
if (isError || !releaseData) {
return (
<Stack gap="md">
<Text>{t('common.newVersion', { postProcess: 'sentenceCase', version })}</Text>
<Text c="dimmed" size="sm">
{t('error.genericError', { postProcess: 'sentenceCase' })}
</Text>
<Group justify="flex-end">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
variant="filled"
>
{t('common.viewReleaseNotes', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={onDismiss} variant="default">
{t('common.dismiss', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
);
}
return (
<Stack gap="md">
<ScrollArea
style={{
height: '500px',
}}
>
<div
className={styles.markdownContent}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
</ScrollArea>
<Group justify="flex-end">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/v${version}`}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
variant="subtle"
>
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={onDismiss} variant="filled">
{t('common.dismiss', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
);
};
export const ReleaseNotesModal = () => {
const { version } = packageJson;
const { t } = useTranslation();
const [value, setValue] = useLocalStorage({ key: 'version' });
const handleDismiss = useCallback(() => {
setValue(version);
closeAllModals();
}, [setValue, version]);
useEffect(() => {
if (value !== version) {
openModal({
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
size: 'xl',
styles: {
body: {
padding: 'var(--mantine-spacing-xl)',
},
},
title: t('common.newVersion', {
postProcess: 'sentenceCase',
version,
}) as string,
});
}
}, [handleDismiss, value, version, t]);
return null;
};