Merge pull request #286 from appergb/feat/settings-about-merge-export-log

feat(settings): 合并内外两个'关于' + 一键导出错误日志
This commit is contained in:
TRIP authored and GitHub committed 2026-05-06 14:23:57 +08:00
commit eaf1dbed08
14 files changed
+262 -79

No files matched your search

+13 -3
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"i18next": "^26.0.8",
@@ -1117,9 +1118,9 @@
]
},
"node_modules/@tauri-apps/api": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -1352,6 +1353,15 @@
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
"integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0"
}
},
"node_modules/@tauri-apps/plugin-shell": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
+1
View File
@@ -13,6 +13,7 @@
"dependencies": {
"@tauri-apps/api": "^2.1.1",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"i18next": "^26.0.8",
+79 -12
View File
@@ -99,7 +99,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -110,7 +110,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -1168,7 +1168,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1409,7 +1409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2242,7 +2242,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
"windows-core 0.61.2",
]
[[package]]
@@ -3445,6 +3445,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-dialog",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
@@ -3524,7 +3525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4439,6 +4440,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2 0.6.2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -4508,7 +4533,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4565,7 +4590,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5055,7 +5080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -5448,6 +5473,48 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation 0.3.2",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.5"
@@ -5629,7 +5696,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6115,7 +6182,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6604,7 +6671,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
+1
View File
@@ -24,6 +24,7 @@ tauri-plugin-shell = "2"
tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
@@ -886,6 +886,20 @@ pub fn local_asr_set_keep_loaded_secs(
coord.prefs().set(prefs).map_err(|e| e.to_string())
}
/// 把当前会话的 openless.log 复制到用户选择的位置(前端用 plugin-dialog 拿 target_path)。
/// 路径来自 lib::log_dir_path() —— mac: ~/Library/Logs/OpenLess/openless.log,
/// windows: %LOCALAPPDATA%\OpenLess\Logs\openless.log。
#[tauri::command]
pub fn export_error_log(target_path: String) -> Result<(), String> {
let src = crate::log_dir_path().join("openless.log");
if !src.exists() {
return Err(format!("日志文件不存在:{}", src.display()));
}
std::fs::copy(&src, std::path::Path::new(&target_path))
.map(|_| ())
.map_err(|e| format!("复制日志失败:{}", e))
}
// ─────────────────────────── unused but exported (silences dead_code) ───────────────────────────
#[allow(dead_code)]
+3 -1
View File
@@ -62,6 +62,7 @@ pub fn run() {
}))
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
// 跨平台开机自启:mac 写 LaunchAgent plist,linux 写 ~/.config/autostart/*.desktop,
// windows 写 HKCU\Software\Microsoft\Windows\CurrentVersion\Run。前端 toggle 直接
// 调插件 isEnabled / enable / disable,不维持本地 prefs,让 OS 当唯一真相。issue #194。
@@ -244,6 +245,7 @@ pub fn run() {
commands::local_asr_release_engine,
commands::local_asr_preload,
commands::local_asr_set_keep_loaded_secs,
commands::export_error_log,
restart_app,
])
.build(tauri::generate_context!())
@@ -420,7 +422,7 @@ fn init_file_logger() {
let _ = CombinedLogger::init(loggers);
}
fn log_dir_path() -> std::path::PathBuf {
pub fn log_dir_path() -> std::path::PathBuf {
#[cfg(target_os = "macos")]
{
if let Ok(home) = std::env::var("HOME") {
@@ -4,13 +4,13 @@
// 开机自启)已从此弹窗移除,避免 "看似可点实际无效" 的负面体感。
// 待 backend 就位后再补回(参见 issue #69)。
import { useEffect, useState, type CSSProperties } from 'react';
import { useEffect, useRef, useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon } from './Icon';
import { AboutUpdateControl, Settings as SettingsContent, type SettingsSectionId } from '../pages/Settings';
import { Row } from './ui/Row';
import { readFontScale, setFontScale, type FontScaleId } from '../lib/fontScale';
import { openExternal } from '../lib/ipc';
import { exportErrorLog, openExternal } from '../lib/ipc';
import {
FOLLOW_SYSTEM,
getLocalePreference,
@@ -270,6 +270,41 @@ function PersonalizeSection() {
function AboutMini() {
const { t } = useTranslation();
const [qqCopied, setQqCopied] = useState(false);
const qqCopiedRef = useRef<number | null>(null);
const [exportStatus, setExportStatus] = useState<'idle' | 'busy' | 'ok' | 'err'>('idle');
const [exportMessage, setExportMessage] = useState<string>('');
useEffect(() => () => {
if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current);
}, []);
const copyQq = () => {
navigator.clipboard?.writeText('1078960553');
setQqCopied(true);
if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current);
qqCopiedRef.current = window.setTimeout(() => setQqCopied(false), 1500);
};
const onExportLog = async () => {
setExportStatus('busy');
setExportMessage('');
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const target = await exportErrorLog(`openless-${ts}.log`);
if (target == null) {
setExportStatus('idle');
return;
}
setExportStatus('ok');
setExportMessage(target);
window.setTimeout(() => setExportStatus('idle'), 4000);
} catch (err) {
setExportStatus('err');
setExportMessage(err instanceof Error ? err.message : String(err));
}
};
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 16 }}>
@@ -279,6 +314,14 @@ function AboutMini() {
<AboutUpdateControl tagline={t('modal.about.tagline')} />
</div>
</div>
<Row label={t('modal.about.source')}>
<button
style={btnGhost}
onClick={() => openExternal('https://github.com/appergb/openless')}
>
GitHub
</button>
</Row>
<Row label={t('modal.about.docs')}>
<button
style={btnGhost}
@@ -295,6 +338,40 @@ function AboutMini() {
{t('modal.about.feedbackBtn')}
</button>
</Row>
<Row label={t('modal.about.qq')} desc={t('modal.about.qqDesc')}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<kbd style={{
padding: '4px 10px', fontSize: 12, fontFamily: 'var(--ol-font-mono)',
borderRadius: 6, background: 'var(--ol-surface-2)',
border: '0.5px solid var(--ol-line-strong)',
boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
color: 'var(--ol-ink-2)',
}}>1078960553</kbd>
<button onClick={copyQq} title={t('modal.about.copyQq')} style={btnGhost}>
<Icon name="copy" size={14} />
</button>
{qqCopied && <span style={{ fontSize: 11, color: 'var(--ol-ok)', whiteSpace: 'nowrap' }}>{t('common.copied')}</span>}
</div>
</Row>
<Row label={t('modal.about.exportErrorLog')} desc={t('modal.about.exportErrorLogDesc')}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<button style={btnGhost} onClick={onExportLog} disabled={exportStatus === 'busy'}>
{exportStatus === 'busy' ? t('modal.about.exporting') : t('modal.about.exportErrorLogBtn')}
</button>
{exportStatus === 'ok' && (
<span style={{ fontSize: 11, color: 'var(--ol-ok)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 220 }}
title={exportMessage}>
{t('modal.about.exportSuccess')}
</span>
)}
{exportStatus === 'err' && (
<span style={{ fontSize: 11, color: 'var(--ol-err)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 220 }}
title={exportMessage}>
{t('modal.about.exportFailed')}
</span>
)}
</div>
</Row>
<Row label={t('modal.about.privacy')} desc={t('modal.about.privacyDesc')}>
<span style={{ fontSize: 11, padding: '3px 8px', borderRadius: 999, background: 'var(--ol-blue-soft)', color: 'var(--ol-blue)', fontWeight: 500 }}>{t('modal.about.localFirst')}</span>
</Row>
+10
View File
@@ -495,6 +495,16 @@ export const en: typeof zhCN = {
docsBtn: 'openless.app/docs ↗',
feedback: 'Feedback channel',
feedbackBtn: 'GitHub Issues ↗',
source: 'Source',
qq: 'Community QQ Group',
qqDesc: 'Search the group number on QQ to join, or scan the QR code.',
copyQq: 'Copy group number',
exportErrorLog: 'Export error log',
exportErrorLogDesc: 'Save the current session log to disk for debugging or sending us feedback.',
exportErrorLogBtn: 'Export',
exporting: 'Exporting…',
exportSuccess: 'Saved',
exportFailed: 'Export failed',
privacy: 'Privacy',
privacyDesc: 'All transcripts stay on this device. Cloud APIs are used only for real-time calls.',
localFirst: 'Local-first',
+10
View File
@@ -497,6 +497,16 @@ export const ja: typeof zhCN = {
docsBtn: 'openless.app/docs ↗',
feedback: 'フィードバックチャネル',
feedbackBtn: 'GitHub Issues ↗',
source: 'ソース',
qq: 'コミュニティ QQ グループ',
qqDesc: 'QQ でグループ番号を検索して参加するか、QR コードをスキャンしてください。',
copyQq: 'グループ番号をコピー',
exportErrorLog: 'エラーログをエクスポート',
exportErrorLogDesc: '現在のセッションの実行ログをローカルに保存。問題の調査やフィードバック送付にお使いください。',
exportErrorLogBtn: 'エクスポート',
exporting: 'エクスポート中…',
exportSuccess: '保存しました',
exportFailed: 'エクスポート失敗',
privacy: 'プライバシー',
privacyDesc: 'すべての認識結果はローカルにのみ保存されます。クラウド API はリアルタイム呼び出しにのみ使用。',
localFirst: 'ローカル優先',
+10
View File
@@ -497,6 +497,16 @@ export const ko: typeof zhCN = {
docsBtn: 'openless.app/docs ↗',
feedback: '피드백 채널',
feedbackBtn: 'GitHub Issues ↗',
source: '소스',
qq: '커뮤니티 QQ 그룹',
qqDesc: 'QQ 에서 그룹 번호를 검색해 가입하거나 QR 코드로 입장하세요.',
copyQq: '그룹 번호 복사',
exportErrorLog: '오류 로그 내보내기',
exportErrorLogDesc: '현재 세션의 실행 로그를 로컬에 저장합니다. 문제 조사나 피드백 전송에 사용하세요.',
exportErrorLogBtn: '내보내기',
exporting: '내보내는 중…',
exportSuccess: '저장됨',
exportFailed: '내보내기 실패',
privacy: '프라이버시',
privacyDesc: '모든 인식 결과는 로컬에만 저장됩니다. 클라우드 API 는 실시간 호출에만 사용됩니다.',
localFirst: '로컬 우선',
+10
View File
@@ -493,6 +493,16 @@ export const zhCN = {
docsBtn: 'openless.app/docs ↗',
feedback: '反馈渠道',
feedbackBtn: 'GitHub Issues ↗',
source: '源码',
qq: '社区 QQ 群',
qqDesc: '使用 QQ 搜索群号加入,或扫码进群。',
copyQq: '复制群号',
exportErrorLog: '导出错误日志',
exportErrorLogDesc: '把当前会话的运行日志保存到本地,便于排查问题或反馈给我们。',
exportErrorLogBtn: '导出',
exporting: '导出中…',
exportSuccess: '已保存',
exportFailed: '导出失败',
privacy: '隐私',
privacyDesc: '所有识别结果只保存在本机,云端 API 仅用于实时调用。',
localFirst: '本地优先',
+10
View File
@@ -495,6 +495,16 @@ export const zhTW: typeof zhCN = {
docsBtn: 'openless.app/docs ↗',
feedback: '反饋渠道',
feedbackBtn: 'GitHub Issues ↗',
source: '原始碼',
qq: '社群 QQ 群',
qqDesc: '使用 QQ 搜尋群號加入,或掃碼進群。',
copyQq: '複製群號',
exportErrorLog: '匯出錯誤日誌',
exportErrorLogDesc: '把當前會話的執行日誌儲存到本地,便於排查問題或反饋給我們。',
exportErrorLogBtn: '匯出',
exporting: '匯出中…',
exportSuccess: '已儲存',
exportFailed: '匯出失敗',
privacy: '隱私',
privacyDesc: '所有識別結果只保存在本機,雲端 API 僅用於實時調用。',
localFirst: '本地優先',
+18
View File
@@ -320,4 +320,22 @@ export async function openExternal(url: string): Promise<void> {
await open(url);
}
/**
* 让用户选 save 路径并把当前会话日志(openless.log)复制过去。
* 浏览器开发模式下走 mock 不实际写盘。返回最终 save 的绝对路径,取消选择则返回 null。
*/
export async function exportErrorLog(suggestedFileName: string): Promise<string | null> {
if (!isTauri) {
return `~/Downloads/${suggestedFileName}`;
}
const { save } = await import('@tauri-apps/plugin-dialog');
const target = await save({
defaultPath: suggestedFileName,
filters: [{ name: 'Log', extensions: ['log', 'txt'] }],
});
if (!target) return null;
await invokeOrMock<void>('export_error_log', { targetPath: target }, () => undefined);
return target;
}
export { isTauri };
+4 -61
View File
@@ -60,9 +60,10 @@ interface SettingsProps {
initialSection?: SettingsSectionId;
}
export type SettingsSectionId = 'recording' | 'providers' | 'shortcuts' | 'permissions' | 'language' | 'about';
// "关于" tab 已移除(内容并入外层 SettingsModal 的 About 页,避免设置内外重复入口)。
export type SettingsSectionId = 'recording' | 'providers' | 'shortcuts' | 'permissions' | 'language';
const SECTION_ORDER: SettingsSectionId[] = ['recording', 'providers', 'shortcuts', 'permissions', 'language', 'about'];
const SECTION_ORDER: SettingsSectionId[] = ['recording', 'providers', 'shortcuts', 'permissions', 'language'];
async function autostartIsEnabled(): Promise<boolean> {
const { invoke } = await import('@tauri-apps/api/core');
@@ -140,7 +141,6 @@ export function Settings({ embedded = false, initialSection = 'recording' }: Set
{section === 'shortcuts' && <ShortcutsSection />}
{section === 'permissions' && <PermissionsSection />}
{section === 'language' && <LanguageSection />}
{section === 'about' && <AboutSection />}
</div>
</div>
</>
@@ -1144,64 +1144,7 @@ function LanguageSection() {
);
}
function AboutSection() {
const { t } = useTranslation();
const [qqCopied, setQqCopied] = useState(false);
const qqCopiedRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current);
};
}, []);
const copyQq = () => {
navigator.clipboard?.writeText('1078960553');
setQqCopied(true);
if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current);
qqCopiedRef.current = window.setTimeout(() => setQqCopied(false), 1500);
};
return (
<Card>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 18 }}>
<div
style={{
width: 52, height: 52, borderRadius: 12,
background: 'linear-gradient(135deg, #0a0a0b 0%, #2563eb 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 22, fontWeight: 700, letterSpacing: '-0.02em',
}}
>OL</div>
<div>
<div style={{ fontSize: 16, fontWeight: 600 }}>OpenLess</div>
<AboutUpdateControl tagline={t('settings.about.tagline')} />
</div>
</div>
<SettingRow label={t('settings.about.source')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless')}>GitHub</Btn></SettingRow>
<SettingRow label={t('settings.about.docs')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless#readme')}>README</Btn></SettingRow>
<SettingRow label={t('settings.about.feedback')}><Btn variant="ghost" size="sm" icon="link" onClick={() => openExternal('https://github.com/appergb/openless/issues')}>GitHub Issues</Btn></SettingRow>
<SettingRow label={t('settings.about.qq')} desc={t('settings.about.qqDesc')}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<kbd style={{
padding: '4px 10px', fontSize: 12, fontFamily: 'var(--ol-font-mono)',
borderRadius: 6, background: 'var(--ol-surface-2)',
border: '0.5px solid var(--ol-line-strong)',
boxShadow: '0 1px 0 rgba(0,0,0,0.04)',
color: 'var(--ol-ink-2)',
}}>1078960553</kbd>
<button onClick={copyQq} title={t('settings.about.copyQq')} style={iconBtnStyle}>
<Icon name="copy" size={14} />
</button>
{qqCopied && <span style={{ fontSize: 11, color: 'var(--ol-ok)', whiteSpace: 'nowrap' }}>{t('common.copied')}</span>}
</div>
</SettingRow>
<SettingRow label={t('settings.about.privacy')} desc={t('settings.about.privacyDesc')}>
<Pill tone="default">{t('settings.about.localFirst')}</Pill>
</SettingRow>
</Card>
);
}
// AboutSection 已移除:内容并入 SettingsModal 的 AboutMini,避免设置内外两个"关于"重复入口。
export function AboutUpdateControl({ tagline }: { tagline: string }) {
const { t } = useTranslation();