mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-17 00:44:23 +02:00
add in-app release notes
This commit is contained in:
@@ -12,7 +12,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
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 { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
|
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
|
||||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||||
@@ -89,7 +89,7 @@ export const App = () => {
|
|||||||
<AppRouter />
|
<AppRouter />
|
||||||
</PlayerProvider>
|
</PlayerProvider>
|
||||||
</WebAudioContext.Provider>
|
</WebAudioContext.Provider>
|
||||||
<IsUpdatedDialog />
|
<ReleaseNotesModal />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user