diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dea19e6..ccf9883 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,7 @@ jobs: arch: universal artifact_name: preview-macos-universal artifact_suffix: macos-universal + app_path: dist/mac-universal/Hush.app - runner: windows-latest platform: windows arch: x64 @@ -93,7 +94,11 @@ jobs: - name: Verify native module (macOS) if: matrix.platform == 'macos' - run: node scripts/verify-native-module.mjs darwin ${{ matrix.arch }} + run: node scripts/verify-native-module.mjs darwin ${{ matrix.arch }} ${{ matrix.app_path }} + + - name: Verify macOS signing + if: matrix.platform == 'macos' + run: npm run verify:macos-signing -- ${{ matrix.app_path }} - name: Verify native module (Windows) if: matrix.platform == 'windows' diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 8a151ec..91284ef 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -74,7 +74,10 @@ jobs: run: npx electron-builder --mac --universal --publish never - name: Verify native module - run: node scripts/verify-native-module.mjs darwin universal + run: node scripts/verify-native-module.mjs darwin universal dist/mac-universal/Hush.app + + - name: Verify macOS signing + run: npm run verify:macos-signing -- dist/mac-universal/Hush.app - name: Inspect macOS build metadata shell: bash diff --git a/build/dmg-background.png b/build/dmg-background.png new file mode 100644 index 0000000..f241291 Binary files /dev/null and b/build/dmg-background.png differ diff --git a/build/dmg-background@2x.png b/build/dmg-background@2x.png new file mode 100644 index 0000000..cdbbcb3 Binary files /dev/null and b/build/dmg-background@2x.png differ diff --git a/build/dmg-readme.txt b/build/dmg-readme.txt new file mode 100644 index 0000000..e123645 --- /dev/null +++ b/build/dmg-readme.txt @@ -0,0 +1,21 @@ +Hush for macOS + +English +------- +Move Hush.app to the Applications folder. + +If macOS says the app cannot be opened or is damaged, run: + +sudo xattr -rd com.apple.quarantine /Applications/Hush.app + +Then open Hush again from the Applications folder. + +简体中文 +-------- +将 Hush.app 移动到“应用程序”文件夹。 + +如果 macOS 提示应用无法打开或已损坏,请运行: + +sudo xattr -rd com.apple.quarantine /Applications/Hush.app + +然后从“应用程序”文件夹重新打开 Hush。 diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist index 55f37a6..9a279dc 100644 --- a/build/entitlements.mac.plist +++ b/build/entitlements.mac.plist @@ -6,5 +6,7 @@ com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.cs.disable-library-validation + diff --git a/electron-builder.yml b/electron-builder.yml index e0b7027..5d677e6 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -39,10 +39,30 @@ mac: - dmg - zip artifactName: ${name}-macos-${arch}-${version}.${ext} + entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist notarize: false dmg: artifactName: ${name}-macos-${arch}-${version}.${ext} + background: build/dmg-background.png + iconSize: 80 + iconTextSize: 12 + window: + width: 540 + height: 380 + contents: + - x: 132 + y: 154 + type: file + - x: 408 + y: 154 + type: link + path: /Applications + - x: 270 + y: 280 + type: file + path: build/dmg-readme.txt + name: Read Me.txt linux: target: - AppImage diff --git a/package.json b/package.json index fd63a36..21304f0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", + "build:dmg-background": "electron scripts/render-dmg-background.mjs", "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win --x64", @@ -25,7 +26,8 @@ "build:mac:x64": "npm run build && electron-builder --mac --x64", "build:mac:universal": "npm run build && electron-builder --mac --universal", "build:linux": "npm run build && electron-builder --linux", - "build:publish": "npm run build && electron-builder --publish always" + "build:publish": "npm run build && electron-builder --publish always", + "verify:macos-signing": "node scripts/verify-macos-signing.mjs" }, "dependencies": { "@electron-toolkit/preload": "^3.0.2", diff --git a/resources/dreamcreator.png b/resources/dreamcreator.png new file mode 100644 index 0000000..32e719c Binary files /dev/null and b/resources/dreamcreator.png differ diff --git a/resources/xiadown.png b/resources/xiadown.png new file mode 100644 index 0000000..b13557a Binary files /dev/null and b/resources/xiadown.png differ diff --git a/scripts/render-dmg-background.mjs b/scripts/render-dmg-background.mjs new file mode 100644 index 0000000..0dda27e --- /dev/null +++ b/scripts/render-dmg-background.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { app, BrowserWindow } from 'electron' + +const WIDTH = 540 +const HEIGHT = 380 +const OUTPUT_DIR = join(process.cwd(), 'build') + +app.commandLine.appendSwitch('disable-gpu') +app.commandLine.appendSwitch('force-device-scale-factor', '1') + +app + .whenReady() + .then(async () => { + await mkdir(OUTPUT_DIR, { recursive: true }) + await renderBackground() + }) + .then(() => app.quit()) + .catch((error) => { + console.error(error) + app.exit(1) + }) + +async function renderBackground() { + const window = new BrowserWindow({ + width: WIDTH, + height: HEIGHT, + show: false, + frame: false, + transparent: false, + resizable: false, + webPreferences: { + offscreen: true, + sandbox: true + } + }) + + await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(createHtml())}`) + await window.webContents.executeJavaScript('document.fonts.ready') + + const image = await window.webContents.capturePage() + const imageSize = image.getSize() + const retinaImage = + imageSize.width >= WIDTH * 2 && imageSize.height >= HEIGHT * 2 + ? image + : image.resize({ width: WIDTH * 2, height: HEIGHT * 2, quality: 'best' }) + + await writeFile( + join(OUTPUT_DIR, 'dmg-background.png'), + retinaImage.resize({ width: WIDTH, height: HEIGHT, quality: 'best' }).toPNG() + ) + await writeFile(join(OUTPUT_DIR, 'dmg-background@2x.png'), retinaImage.toPNG()) + window.destroy() +} + +function createHtml() { + return ` + + + + + + +
+
Install Hush
+
Drag Hush into Applications
+
+
+
+
Move Hush.app to the Applications folder.
+
+ +` +} diff --git a/scripts/verify-macos-signing.mjs b/scripts/verify-macos-signing.mjs new file mode 100644 index 0000000..991c51b --- /dev/null +++ b/scripts/verify-macos-signing.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { existsSync } from 'node:fs' +import { isAbsolute, join } from 'node:path' +import { spawnSync } from 'node:child_process' + +const [, , appPathArg = 'dist/mac-universal/Hush.app'] = process.argv +const appPath = isAbsolute(appPathArg) ? appPathArg : join(process.cwd(), appPathArg) + +if (process.platform !== 'darwin') { + console.error('macOS signing verification can only run on darwin') + process.exit(2) +} + +if (!existsSync(appPath)) { + console.error(`App bundle not found: ${appPath}`) + process.exit(1) +} + +run('codesign', ['--verify', '--deep', '--strict', '--verbose=4', appPath], { capture: true }) + +const entitlements = run('codesign', ['-d', '--entitlements', ':-', appPath], { capture: true }) +const requiredEntitlements = [ + 'com.apple.security.cs.allow-jit', + 'com.apple.security.cs.allow-unsigned-executable-memory', + 'com.apple.security.cs.disable-library-validation' +] + +for (const entitlement of requiredEntitlements) { + if (!entitlements.includes(`${entitlement}`)) { + console.error(`Missing required entitlement: ${entitlement}`) + process.exit(1) + } +} + +const infoPlistPath = join(appPath, 'Contents', 'Info.plist') +const executableName = run( + 'plutil', + ['-extract', 'CFBundleExecutable', 'raw', '-o', '-', infoPlistPath], + { + capture: true + } +).trim() +const executablePath = join(appPath, 'Contents', 'MacOS', executableName) +const electronVersion = run( + executablePath, + ['-e', 'process.stdout.write(process.versions.electron || "")'], + { + capture: true, + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } + } +).trim() + +if (!electronVersion) { + console.error('Electron dyld smoke test did not return an Electron version') + process.exit(1) +} + +console.log(`macOS signing verification passed for ${appPathArg} (Electron ${electronVersion})`) + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: 'utf8', + env: options.env ?? process.env + }) + + if (result.status !== 0) { + if (result.stdout) process.stdout.write(result.stdout) + if (result.stderr) process.stderr.write(result.stderr) + console.error(`${command} ${args.join(' ')} failed`) + process.exit(result.status ?? 1) + } + + if (options.capture) { + return result.stdout + } + + if (result.stdout) process.stdout.write(result.stdout) + if (result.stderr) process.stderr.write(result.stderr) + return result.stdout +} diff --git a/src/main/index.ts b/src/main/index.ts index 38fe248..2a61431 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -76,6 +76,9 @@ const VIDEO_PLAYER_FRAME_MARGIN_BOTTOM = 10 const VIDEO_PLAYER_ASPECT_RATIO = 16 / 9 const VIDEO_PLAYER_MIN_CONTENT_HEIGHT = 260 const TRAY_ICON_SIZE = process.platform === 'darwin' ? 18 : 16 +const MENU_ICON_SIZE = 16 +const SETTINGS_MENU_ICON_DATA_URL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA10lEQVR42t1X0Q3EIAjtKI7SERzBUdykoziCIziCI3h+cIkxnqJH+mxJXppoC0+ggMfxYDkzEj0h4oiAQxFIBW4XXRHQ0gZiRsgwxZrKsLSeGgi0r4pvDK3HWQKlEU+K4w/DNSK97ytyU2KYxrgwK2HwQsb9ah5YIQJ2xbiaiDknJ5T06S+qgF9c0l4IC8p6pMM/RaY+eU96ntCjxuIY8Rw1npOhw7X0cBNKgkCzf8A9AM+Brf6CLeoAvBLCewG8G8LnAfhEBJ8J4VPx9veCLW5G8LvhO+QDSTx+3HSPDdAAAAAASUVORK5CYII=' const TRAY_SINGLE_CLICK_DELAY_MS = 120 const WINDOWS_TITLE_BAR_OVERLAY_COLOR = 'rgba(0, 0, 0, 0)' const ALLOWED_EXTERNAL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']) @@ -113,6 +116,7 @@ let shouldShowTrayPreviewWhenReady = false let pendingTrayPreviewBounds: Electron.Rectangle | undefined let traySingleClickTimer: ReturnType | null = null const windowBoundsSaveTimers = new Map<'main' | 'settings', ReturnType>() +let settingsMenuIcon: NativeImage | undefined function configureAppIdentity(): void { app.setName(APP_DISPLAY_NAME) @@ -977,6 +981,7 @@ function createTrayMenu(settings: AppSettings): Menu { }, { label: text.tray.settings, + icon: getSettingsMenuIcon(), click: () => showSettingsWindow('general') }, { type: 'separator' }, @@ -995,6 +1000,33 @@ function createTrayMenu(settings: AppSettings): Menu { ]) } +function getSettingsMenuIcon(): NativeImage | undefined { + if (settingsMenuIcon) { + return settingsMenuIcon + } + + const systemIcon = + process.platform === 'darwin' ? nativeImage.createFromNamedImage('gearshape') : undefined + const source = + systemIcon && !systemIcon.isEmpty() + ? systemIcon + : nativeImage.createFromDataURL(SETTINGS_MENU_ICON_DATA_URL) + + if (source.isEmpty()) { + return undefined + } + + settingsMenuIcon = source.resize({ + width: MENU_ICON_SIZE, + height: MENU_ICON_SIZE, + quality: 'best' + }) + if (process.platform === 'darwin') { + settingsMenuIcon.setTemplateImage(true) + } + return settingsMenuIcon +} + function createTrayIcon(): NativeImage { const imagePath = process.platform === 'darwin' @@ -1767,6 +1799,7 @@ function createApplicationMenu(settings: AppSettings): Menu { { label: menu.settings, accelerator: 'CommandOrControl+,', + icon: getSettingsMenuIcon(), click: () => showSettingsWindow('general') } ] diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 847dc59..2634823 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -56,6 +56,8 @@ import { } from './styles/theme' const appIconUrl = new URL('../../../resources/icon.png', import.meta.url).href +const dreamCreatorIconUrl = new URL('../../../resources/dreamcreator.png', import.meta.url).href +const xiaDownIconUrl = new URL('../../../resources/xiadown.png', import.meta.url).href interface LatestVersionMeta { label: string @@ -805,6 +807,7 @@ function AboutTab(props: { onCheckUpdates: () => void onInstallUpdate: () => void }): JSX.Element { + const [releaseNotesOpen, setReleaseNotesOpen] = useState(false) const { appInfo, text, @@ -822,6 +825,35 @@ function AboutTab(props: { const updateProgress = formatUpdateProgress(updateInfo) const updateReady = updateInfo?.status === 'downloaded' const updateInProgress = updateInfo?.status === 'downloading' + const releaseNotes = updateInfo?.releaseNotes?.trim() ?? '' + const dreamApps = [ + { + name: text.about.dreamCreator, + description: text.about.dreamCreatorDescription, + url: 'https://dreamcreator.dreamapp.cc/', + iconUrl: dreamCreatorIconUrl + }, + { + name: text.about.xiaDown, + description: text.about.xiaDownDescription, + url: 'https://xiadown.dreamapp.cc/', + iconUrl: xiaDownIconUrl + } + ] + + useEffect(() => { + if (!releaseNotesOpen) { + return + } + + const closeOnEscape = (event: KeyboardEvent): void => { + if (event.key === 'Escape') { + setReleaseNotesOpen(false) + } + } + window.addEventListener('keydown', closeOnEscape) + return () => window.removeEventListener('keydown', closeOnEscape) + }, [releaseNotesOpen]) return (
@@ -843,9 +875,14 @@ function AboutTab(props: { - - {updateInfo?.releaseNotes || text.about.noReleaseNotes} - + @@ -934,10 +971,296 @@ function AboutTab(props: { + +
+
{text.about.dreamApp}
+ + {dreamApps.map((app, index) => ( +
0 ? 'dream-app-item has-border' : 'dream-app-item'} + key={app.name} + > + +
+
{app.name}
+
{app.description}
+
+ +
+ ))} +
+
+ + {releaseNotesOpen ? ( +
setReleaseNotesOpen(false)} + > +
event.stopPropagation()} + > +
+

{text.about.releaseNotes}

+ +
+
+ +
+
+ +
+
+
+ ) : null} +
+ ) +} + +type MarkdownBlock = + | { type: 'code'; content: string } + | { type: 'heading'; level: number; content: string } + | { type: 'list'; ordered: boolean; items: string[] } + | { type: 'paragraph'; content: string } + | { type: 'quote'; content: string } + | { type: 'rule' } + +function MarkdownContent(props: { markdown: string }): JSX.Element { + const blocks = parseMarkdownBlocks(props.markdown) + + return ( +
+ {blocks.map((block, index) => { + const key = `markdown-block-${index}` + if (block.type === 'heading') { + const HeadingTag = `h${Math.min(block.level, 4)}` as keyof JSX.IntrinsicElements + return {renderMarkdownInline(block.content, key)} + } + if (block.type === 'list') { + const ListTag = block.ordered ? 'ol' : 'ul' + return ( + + {block.items.map((item, itemIndex) => ( +
  • + {renderMarkdownInline(item, `${key}-${itemIndex}`)} +
  • + ))} +
    + ) + } + if (block.type === 'code') { + return ( +
    +              {block.content}
    +            
    + ) + } + if (block.type === 'quote') { + return
    {renderMarkdownInline(block.content, key)}
    + } + if (block.type === 'rule') { + return
    + } + return

    {renderMarkdownInline(block.content, key)}

    + })}
    ) } +function parseMarkdownBlocks(markdown: string): MarkdownBlock[] { + const lines = markdown.replace(/\r\n/g, '\n').split('\n') + const blocks: MarkdownBlock[] = [] + let index = 0 + + while (index < lines.length) { + const line = lines[index] + const trimmed = line.trim() + + if (!trimmed) { + index += 1 + continue + } + + if (trimmed.startsWith('```')) { + const codeLines: string[] = [] + index += 1 + while (index < lines.length && !lines[index].trim().startsWith('```')) { + codeLines.push(lines[index]) + index += 1 + } + blocks.push({ type: 'code', content: codeLines.join('\n') }) + index += index < lines.length ? 1 : 0 + continue + } + + const headingMatch = /^(#{1,4})\s+(.+)$/.exec(trimmed) + if (headingMatch) { + blocks.push({ + type: 'heading', + level: headingMatch[1].length, + content: headingMatch[2].trim() + }) + index += 1 + continue + } + + if (/^[-*_]{3,}$/.test(trimmed)) { + blocks.push({ type: 'rule' }) + index += 1 + continue + } + + const quoteMatch = /^>\s?(.+)$/.exec(trimmed) + if (quoteMatch) { + const quoteLines = [quoteMatch[1].trim()] + index += 1 + while (index < lines.length) { + const nextQuoteMatch = /^>\s?(.+)$/.exec(lines[index].trim()) + if (!nextQuoteMatch) { + break + } + quoteLines.push(nextQuoteMatch[1].trim()) + index += 1 + } + blocks.push({ type: 'quote', content: quoteLines.join(' ') }) + continue + } + + const listMatch = /^((?:[-*+])|(?:\d+[.)]))\s+(.+)$/.exec(trimmed) + if (listMatch) { + const ordered = /^\d/.test(listMatch[1]) + const items = [listMatch[2].trim()] + index += 1 + while (index < lines.length) { + const nextListMatch = /^((?:[-*+])|(?:\d+[.)]))\s+(.+)$/.exec(lines[index].trim()) + if (!nextListMatch || /^\d/.test(nextListMatch[1]) !== ordered) { + break + } + items.push(nextListMatch[2].trim()) + index += 1 + } + blocks.push({ type: 'list', ordered, items }) + continue + } + + const paragraphLines = [trimmed] + index += 1 + while (index < lines.length) { + const nextTrimmed = lines[index].trim() + if ( + !nextTrimmed || + nextTrimmed.startsWith('```') || + /^(#{1,4})\s+/.test(nextTrimmed) || + /^((?:[-*+])|(?:\d+[.)]))\s+/.test(nextTrimmed) || + /^>\s?(.+)$/.test(nextTrimmed) || + /^[-*_]{3,}$/.test(nextTrimmed) + ) { + break + } + paragraphLines.push(nextTrimmed) + index += 1 + } + blocks.push({ type: 'paragraph', content: paragraphLines.join(' ') }) + } + + return blocks +} + +function renderMarkdownInline(text: string, keyPrefix: string): ReactNode[] { + const nodes: ReactNode[] = [] + const tokenPattern = /(\[[^\]]+\]\([^)]+\)|`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_)/g + let lastIndex = 0 + let match: RegExpExecArray | null + + while ((match = tokenPattern.exec(text))) { + if (match.index > lastIndex) { + nodes.push(text.slice(lastIndex, match.index)) + } + + const token = match[0] + const key = `${keyPrefix}-${match.index}` + const linkMatch = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token) + if (linkMatch) { + const href = normalizeReleaseNotesHref(linkMatch[2]) + nodes.push( + href ? ( + { + event.preventDefault() + void window.api.openExternal(href) + }} + > + {renderMarkdownInline(linkMatch[1], `${key}-label`)} + + ) : ( + linkMatch[1] + ) + ) + } else if (token.startsWith('`')) { + nodes.push({token.slice(1, -1)}) + } else if (token.startsWith('**') || token.startsWith('__')) { + nodes.push( + {renderMarkdownInline(token.slice(2, -2), `${key}-strong`)} + ) + } else { + nodes.push({renderMarkdownInline(token.slice(1, -1), `${key}-em`)}) + } + + lastIndex = tokenPattern.lastIndex + } + + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)) + } + + return nodes +} + +function normalizeReleaseNotesHref(href: string): string { + try { + const parsed = new URL(href.trim()) + if ( + parsed.protocol === 'https:' || + parsed.protocol === 'http:' || + parsed.protocol === 'mailto:' + ) { + return parsed.toString() + } + } catch { + return '' + } + return '' +} + function SettingsCard(props: { children: ReactNode; contentClassName?: string }): JSX.Element { return (
    {props.children}
    diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index e9337c2..28f7ec8 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -3420,6 +3420,86 @@ button:disabled { color: var(--app-muted-fg); } +.dream-app-section { + display: grid; + gap: 8px; +} + +.dream-app-title { + padding: 0 14px; + color: var(--app-fg); + font-size: 13px; + font-weight: 800; +} + +.dream-app-card { + display: grid; +} + +.dream-app-item { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + min-height: 64px; + padding: 11px 14px; +} + +.dream-app-item.has-border { + border-top: 1px solid color-mix(in srgb, var(--app-fg) 8%, transparent); +} + +.dream-app-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + min-width: 42px; + height: 42px; + border-radius: var(--app-radius-lg); + overflow: hidden; + background: color-mix(in srgb, var(--app-fg) 6%, transparent); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 24%), + 0 8px 18px rgb(0 0 0 / 13%); +} + +.dream-app-icon img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.dream-app-copy { + display: grid; + flex: 1 1 auto; + gap: 4px; + min-width: 0; +} + +.dream-app-name { + overflow: hidden; + color: var(--app-fg); + font-size: 13px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dream-app-description { + display: -webkit-box; + overflow: hidden; + color: var(--app-muted-fg); + font-size: 12px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.dream-app-link { + flex: 0 0 auto; +} + .dialog-backdrop { position: fixed; inset: 0; @@ -3445,6 +3525,12 @@ button:disabled { backdrop-filter: blur(28px) saturate(1.2); } +.release-notes-dialog { + grid-template-rows: auto minmax(0, 1fr) auto; + width: min(560px, 100%); + max-height: min(460px, calc(100vh - 120px)); +} + .dialog-header, .dialog-actions { display: flex; @@ -3515,6 +3601,117 @@ button:disabled { margin-right: auto; } +.release-notes-trigger { + max-width: 100%; +} + +.release-notes-body { + min-height: 0; + overflow: auto; + padding: 14px 16px 16px; +} + +.markdown-content { + display: grid; + gap: 10px; + color: var(--app-fg); + font-size: 13px; + line-height: 1.55; +} + +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4, +.markdown-content p, +.markdown-content ul, +.markdown-content ol, +.markdown-content blockquote, +.markdown-content pre { + margin: 0; +} + +.markdown-content h1 { + font-size: 18px; + font-weight: 850; + line-height: 1.25; +} + +.markdown-content h2 { + margin-top: 4px; + font-size: 15px; + font-weight: 820; + line-height: 1.3; +} + +.markdown-content h3, +.markdown-content h4 { + margin-top: 2px; + font-size: 13px; + font-weight: 800; +} + +.markdown-content p, +.markdown-content li, +.markdown-content blockquote { + color: color-mix(in srgb, var(--app-fg) 86%, var(--app-muted-fg)); +} + +.markdown-content ul, +.markdown-content ol { + display: grid; + gap: 6px; + padding-left: 20px; +} + +.markdown-content blockquote { + padding: 8px 10px; + border-left: 3px solid color-mix(in srgb, var(--app-primary) 55%, transparent); + border-radius: var(--app-radius-md); + background: color-mix(in srgb, var(--app-fg) 5%, transparent); +} + +.markdown-content code { + border-radius: 6px; + background: color-mix(in srgb, var(--app-fg) 8%, transparent); + color: var(--app-fg); + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; + font-size: 0.92em; + padding: 2px 5px; +} + +.markdown-content pre { + overflow: auto; + border-radius: var(--app-radius-lg); + background: color-mix(in srgb, var(--app-fg) 8%, transparent); + padding: 10px; +} + +.markdown-content pre code { + display: block; + padding: 0; + background: transparent; + white-space: pre; +} + +.markdown-content a { + color: var(--app-primary); + font-weight: 720; + text-decoration: none; +} + +.markdown-content a:hover { + text-decoration: underline; +} + +.markdown-content hr { + width: 100%; + height: 1px; + margin: 2px 0; + border: 0; + background: color-mix(in srgb, var(--app-fg) 10%, transparent); +} + .spin { animation: spin 900ms linear infinite; } diff --git a/src/shared/i18n/locales/en.ts b/src/shared/i18n/locales/en.ts index cc4e482..f0e00ee 100644 --- a/src/shared/i18n/locales/en.ts +++ b/src/shared/i18n/locales/en.ts @@ -126,6 +126,7 @@ export const en = { downloadProgress: 'Download', updateReady: 'Ready to install', releaseNotes: 'Release Notes', + viewReleaseNotes: 'View Release Notes', noReleaseNotes: 'No release notes', craftedBy: 'Crafted By', contact: 'Contact', @@ -133,7 +134,12 @@ export const en = { email: 'Email', website: 'Website', github: 'GitHub', - sendFeedback: 'Send Feedback' + sendFeedback: 'Send Feedback', + dreamApp: 'DreamApp', + dreamCreator: 'DreamCreator / 追创作', + dreamCreatorDescription: 'An AI assistant built for creators', + xiaDown: 'XiaDown / 下蛋', + xiaDownDescription: 'A video download tool with online music support' }, live: { audioMode: 'Audio Mode', diff --git a/src/shared/i18n/locales/zh-CN.ts b/src/shared/i18n/locales/zh-CN.ts index 5ee3c28..bbfb84d 100644 --- a/src/shared/i18n/locales/zh-CN.ts +++ b/src/shared/i18n/locales/zh-CN.ts @@ -126,6 +126,7 @@ export const zhCN = { downloadProgress: '下载进度', updateReady: '可重启安装', releaseNotes: '发布说明', + viewReleaseNotes: '查看发布说明', noReleaseNotes: '暂无发布说明', craftedBy: '作者', contact: '联系', @@ -133,7 +134,12 @@ export const zhCN = { email: '邮件', website: '网站', github: 'GitHub', - sendFeedback: '提交反馈' + sendFeedback: '提交反馈', + dreamApp: 'DreamApp', + dreamCreator: 'DreamCreator / 追创作', + dreamCreatorDescription: '一款为创作者打造的 AI 助手', + xiaDown: 'XiaDown / 下蛋', + xiaDownDescription: '一款支持在线音乐的视频下载工具' }, live: { audioMode: '音频模式', diff --git a/src/shared/i18n/types.ts b/src/shared/i18n/types.ts index e1a6813..4a1d495 100644 --- a/src/shared/i18n/types.ts +++ b/src/shared/i18n/types.ts @@ -115,6 +115,7 @@ export interface TextBundle { downloadProgress: string updateReady: string releaseNotes: string + viewReleaseNotes: string noReleaseNotes: string craftedBy: string contact: string @@ -123,6 +124,11 @@ export interface TextBundle { website: string github: string sendFeedback: string + dreamApp: string + dreamCreator: string + dreamCreatorDescription: string + xiaDown: string + xiaDownDescription: string } live: { audioMode: string