diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index a34267207..42f6d6113 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -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 = () => { - + ); }; diff --git a/src/renderer/is-updated-dialog.tsx b/src/renderer/is-updated-dialog.tsx deleted file mode 100644 index 5c43f32ae..000000000 --- a/src/renderer/is-updated-dialog.tsx +++ /dev/null @@ -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 ( - - - {t('common.newVersion', { postProcess: 'sentenceCase', version })} - - - - - - - ); -} diff --git a/src/renderer/release-notes-modal.module.css b/src/renderer/release-notes-modal.module.css new file mode 100644 index 000000000..c72a264bb --- /dev/null +++ b/src/renderer/release-notes-modal.module.css @@ -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); +} diff --git a/src/renderer/release-notes-modal.tsx b/src/renderer/release-notes-modal.tsx new file mode 100644 index 000000000..59a4e44d9 --- /dev/null +++ b/src/renderer/release-notes-modal.tsx @@ -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 ( +
+ + + {t('common.loading', { postProcess: 'sentenceCase' })} + +
+ ); + } + + if (isError || !releaseData) { + return ( + + {t('common.newVersion', { postProcess: 'sentenceCase', version })} + + {t('error.genericError', { postProcess: 'sentenceCase' })} + + + + + + + ); + } + + return ( + + +
+ + + + + + + ); +}; + +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: , + size: 'xl', + styles: { + body: { + padding: 'var(--mantine-spacing-xl)', + }, + }, + title: t('common.newVersion', { + postProcess: 'sentenceCase', + version, + }) as string, + }); + } + }, [handleDismiss, value, version, t]); + + return null; +};