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 (
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