From 029b46db1a06d5c57664695d3514698fdac5e4c4 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 26 Mar 2026 00:48:03 -0500 Subject: [PATCH] 's' --- electron/electron.vite.config.ts | 11 ++ electron/pnpm-lock.yaml | 9 -- electron/src/main/index.ts | 7 ++ electron/src/main/userAgreementGate.ts | 83 +++++++++++++++ electron/src/preload/index.ts | 6 ++ electron/src/renderer/src/App.css | 88 +++++++++++++++ electron/src/renderer/src/AppHostControls.tsx | 25 +++-- .../src/renderer/src/UserAgreementDialog.tsx | 87 +++++++++++++++ electron/src/renderer/src/vite-env.d.ts | 4 + electron/src/shared/userAgreement.ts | 100 ++++++++++++++++++ electron/tsconfig.json | 1 + 11 files changed, 406 insertions(+), 15 deletions(-) create mode 100644 electron/src/main/userAgreementGate.ts create mode 100644 electron/src/renderer/src/UserAgreementDialog.tsx create mode 100644 electron/src/shared/userAgreement.ts diff --git a/electron/electron.vite.config.ts b/electron/electron.vite.config.ts index 3b4ab22..d147d82 100644 --- a/electron/electron.vite.config.ts +++ b/electron/electron.vite.config.ts @@ -4,9 +4,19 @@ import { resolve } from 'path' export default defineConfig({ main: { + resolve: { + alias: { + '@shared': resolve('src/shared'), + }, + }, plugins: [externalizeDepsPlugin()], }, preload: { + resolve: { + alias: { + '@shared': resolve('src/shared'), + }, + }, plugins: [externalizeDepsPlugin()], build: { rollupOptions: { @@ -21,6 +31,7 @@ export default defineConfig({ resolve: { alias: { '@renderer': resolve('src/renderer/src'), + '@shared': resolve('src/shared'), }, }, plugins: [react()], diff --git a/electron/pnpm-lock.yaml b/electron/pnpm-lock.yaml index ebf011d..03de5ae 100644 --- a/electron/pnpm-lock.yaml +++ b/electron/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - adm-zip: - specifier: ^0.5.16 - version: 0.5.16 d3: specifier: ^7.9.0 version: 7.9.0 @@ -709,10 +706,6 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} - engines: {node: '>=12.0'} - agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2950,8 +2943,6 @@ snapshots: abbrev@1.1.1: {} - adm-zip@0.5.16: {} - agent-base@6.0.2: dependencies: debug: 4.4.3 diff --git a/electron/src/main/index.ts b/electron/src/main/index.ts index 8e31a87..8d50c32 100644 --- a/electron/src/main/index.ts +++ b/electron/src/main/index.ts @@ -30,6 +30,7 @@ import { setAppUserModelIdEarly, } from './appHost' import { setMainBrowserWindow } from './mainWindowRef' +import { ensureUserAgreementAccepted } from './userAgreementGate' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -645,6 +646,12 @@ if (!gotLock) { appendClientLog('info', 'main', '本地服务已连接,准备打开界面') + try { + await ensureUserAgreementAccepted(resolvePreload) + } catch { + return + } + await createWindow() app.on('activate', () => { diff --git a/electron/src/main/userAgreementGate.ts b/electron/src/main/userAgreementGate.ts new file mode 100644 index 0000000..295236a --- /dev/null +++ b/electron/src/main/userAgreementGate.ts @@ -0,0 +1,83 @@ +import { app, BrowserWindow, ipcMain } from 'electron' +import fs from 'fs' +import path from 'path' + +import { agreementHtmlDocument } from '../shared/userAgreement' + +const AGREEMENT_FILE = 'user-agreement-v1.json' + +export function userAgreementAcceptedPath(): string { + return path.join(app.getPath('userData'), AGREEMENT_FILE) +} + +export function hasUserAgreementAccepted(): boolean { + try { + return fs.existsSync(userAgreementAcceptedPath()) + } catch { + return false + } +} + +function markUserAgreementAccepted(): void { + fs.writeFileSync( + userAgreementAcceptedPath(), + JSON.stringify({ v: 1, acceptedAt: Date.now() }, null, 2), + 'utf-8', + ) +} + +/** + * 首次启动:在打开主窗口前展示协议;拒绝或关闭未同意则退出应用。 + */ +export function ensureUserAgreementAccepted(resolvePreload: () => string): Promise { + if (hasUserAgreementAccepted()) return Promise.resolve() + + return new Promise((resolve, reject) => { + const win = new BrowserWindow({ + width: 520, + height: 680, + show: false, + resizable: true, + minimizable: false, + maximizable: false, + fullscreenable: false, + autoHideMenuBar: true, + title: '用户使用协议', + webPreferences: { + preload: resolvePreload(), + contextIsolation: true, + nodeIntegration: false, + }, + }) + + const html = agreementHtmlDocument() + win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`) + + const onAccept = (): void => { + ipcMain.removeListener('user-agreement:refuse', onRefuse) + markUserAgreementAccepted() + win.close() + resolve() + } + + const onRefuse = (): void => { + ipcMain.removeListener('user-agreement:accept', onAccept) + app.quit() + reject(new Error('user refused agreement')) + } + + ipcMain.once('user-agreement:accept', onAccept) + ipcMain.once('user-agreement:refuse', onRefuse) + + win.once('ready-to-show', () => win.show()) + + win.on('closed', () => { + ipcMain.removeListener('user-agreement:accept', onAccept) + ipcMain.removeListener('user-agreement:refuse', onRefuse) + if (!hasUserAgreementAccepted()) { + app.quit() + reject(new Error('agreement window closed')) + } + }) + }) +} diff --git a/electron/src/preload/index.ts b/electron/src/preload/index.ts index cf9b2ca..9e9f5f6 100644 --- a/electron/src/preload/index.ts +++ b/electron/src/preload/index.ts @@ -36,6 +36,12 @@ contextBridge.exposeInMainWorld('appTheme', { set: (value: string) => ipcRenderer.invoke('app:set-setting', 'theme', value), }) +/** 首次启动协议窗(主进程内嵌页) */ +contextBridge.exposeInMainWorld('userAgreement', { + accept: () => ipcRenderer.send('user-agreement:accept'), + refuse: () => ipcRenderer.send('user-agreement:refuse'), +}) + contextBridge.exposeInMainWorld('relayConfig', { syncConfig: () => ipcRenderer.invoke('relay:sync-config'), importFromConfig: () => ipcRenderer.invoke('relay:import-from-config'), diff --git a/electron/src/renderer/src/App.css b/electron/src/renderer/src/App.css index 741b3bd..03921c8 100644 --- a/electron/src/renderer/src/App.css +++ b/electron/src/renderer/src/App.css @@ -2939,3 +2939,91 @@ button.net-badge { .app-host-metrics { font-size: 11px; } + +.user-agreement-inline-link { + display: inline-block; + margin-top: 0.65rem; + padding: 0; + border: none; + background: none; + font: inherit; + font-size: 12px; + color: var(--accent, #7c5cff); + text-decoration: underline; + cursor: pointer; +} + +.user-agreement-inline-link:hover { + opacity: 0.9; +} + +.user-agreement-backdrop { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.55); +} + +.user-agreement-sheet { + max-width: 520px; + width: 100%; + max-height: min(88vh, 720px); + display: flex; + flex-direction: column; + border-radius: var(--radius-lg, 12px); + border: 1px solid var(--border-card); + background: var(--bg-card); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); +} + +.user-agreement-sheet__title { + margin: 0; + padding: 0.85rem 1rem 0.5rem; + font-size: 1.05rem; + font-weight: 700; + text-align: center; + color: var(--text); +} + +.user-agreement-sheet__scroll { + flex: 1; + overflow: auto; + padding: 0 1rem 0.5rem; + font-size: 13px; + line-height: 1.55; + color: var(--text-muted); +} + +.user-agreement-dialog__h2 { + margin: 0.75rem 0 0.4rem; + font-size: 0.95rem; + font-weight: 600; + color: var(--text); +} + +.user-agreement-dialog__p { + margin: 0 0 0.35rem; +} + +.user-agreement-dialog__li { + margin: 0 0 0.35rem; + padding-left: 0.25rem; +} + +.user-agreement-dialog__gap { + height: 0.35rem; +} + +.user-agreement-sheet__footer { + flex-shrink: 0; + padding: 0.65rem 1rem 1rem; + border-top: 1px solid var(--border-subtle); +} + +.user-agreement-sheet__footer .btn { + width: 100%; +} diff --git a/electron/src/renderer/src/AppHostControls.tsx b/electron/src/renderer/src/AppHostControls.tsx index 4bd883f..303bb86 100644 --- a/electron/src/renderer/src/AppHostControls.tsx +++ b/electron/src/renderer/src/AppHostControls.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState } from 'react' +import UserAgreementDialog from './UserAgreementDialog' + type LoginItem = { openAtLogin?: boolean executableWillLaunchAtLogin?: boolean @@ -10,6 +12,7 @@ export default function AppHostControls() { const [login, setLogin] = useState(null) const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) + const [agreementOpen, setAgreementOpen] = useState(false) const load = useCallback(async () => { if (!host?.getLoginItemSettings) return @@ -30,12 +33,18 @@ export default function AppHostControls() { if (!host?.getPaths) { return ( -
-

- 客户端与系统集成 -

-

此区域仅在 Electron 桌面客户端可用。

-
+ <> +
+

+ 客户端与系统集成 +

+

此区域仅在 Electron 桌面客户端可用。

+ +
+ setAgreementOpen(false)} /> + ) } @@ -82,6 +91,10 @@ export default function AppHostControls() { + + setAgreementOpen(false)} /> ) } diff --git a/electron/src/renderer/src/UserAgreementDialog.tsx b/electron/src/renderer/src/UserAgreementDialog.tsx new file mode 100644 index 0000000..14de0a5 --- /dev/null +++ b/electron/src/renderer/src/UserAgreementDialog.tsx @@ -0,0 +1,87 @@ +import type { ReactNode } from 'react' +import { useCallback, useEffect } from 'react' + +import { USER_AGREEMENT_MARKDOWN } from '@shared/userAgreement' + +function renderAgreementBody(md: string) { + const lines = md.replace(/^# 用户使用协议\n+/, '').split('\n') + const out: ReactNode[] = [] + lines.forEach((line, i) => { + if (line.startsWith('## ')) { + out.push( +

+ {line.slice(3)} +

, + ) + return + } + if (line.startsWith('* ')) { + out.push( +

+ • {line.slice(2)} +

, + ) + return + } + if (line.trim() === '') { + out.push(
) + return + } + out.push( +

+ {line} +

, + ) + }) + return out +} + +type Props = { + open: boolean + onClose: () => void +} + +export default function UserAgreementDialog({ open, onClose }: Props) { + const onKey = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + }, + [onClose], + ) + + useEffect(() => { + if (!open) return + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [open, onKey]) + + if (!open) return null + + return ( +
{ + if (e.target === e.currentTarget) onClose() + }} + > +
e.stopPropagation()} + > +

+ 用户使用协议 +

+
{renderAgreementBody(USER_AGREEMENT_MARKDOWN)}
+
+ +
+
+
+ ) +} diff --git a/electron/src/renderer/src/vite-env.d.ts b/electron/src/renderer/src/vite-env.d.ts index cd0575d..c190c77 100644 --- a/electron/src/renderer/src/vite-env.d.ts +++ b/electron/src/renderer/src/vite-env.d.ts @@ -49,6 +49,10 @@ interface Window { get?: () => Promise set?: (value: string) => Promise } + userAgreement?: { + accept: () => void + refuse: () => void + } relayConfig?: { syncConfig?: () => Promise<{ ok: true }> importFromConfig?: () => Promise<{ ok: true }> diff --git a/electron/src/shared/userAgreement.ts b/electron/src/shared/userAgreement.ts new file mode 100644 index 0000000..68e5220 --- /dev/null +++ b/electron/src/shared/userAgreement.ts @@ -0,0 +1,100 @@ +/** 用户使用协议(桌面端首次启动弹窗与系统页「用户协议」共用) */ +export const USER_AGREEMENT_MARKDOWN = `# 用户使用协议 + +欢迎使用本软件(以下简称「本软件」)。在使用本软件前,请您仔细阅读并理解本协议。 + +## 1. 软件性质说明 + +本软件为通用的视频处理与推流工具,支持用户对自有或合法授权的视频源进行处理与分发。本软件不提供任何第三方平台内容的解析、抓取或分发服务。 + +## 2. 用户责任 + +用户在使用本软件过程中,应确保: + +* 所使用的视频源具有合法使用权或授权; +* 不侵犯任何第三方的著作权、邻接权或其他合法权益; +* 不违反相关法律法规及平台规则。 + +由用户上传、处理、推流的所有内容,均由用户自行承担全部法律责任。 + +## 3. 禁止行为 + +用户不得利用本软件从事以下行为: + +* 未经授权抓取、转播、传播第三方平台内容; +* 侵犯他人版权、肖像权、隐私权; +* 从事违法违规内容传播。 + +## 4. 责任限制 + +本软件仅提供技术工具服务,不参与用户内容的获取、编辑或分发过程。 +因用户行为导致的任何法律责任或纠纷,均由用户自行承担,与本软件无关。 + +## 5. 权限与服务 + +部分功能属于付费服务(如 Pro/VIP),用户购买后仅获得使用权限,不涉及软件所有权转移。 + +## 6. 协议变更 + +本软件有权根据业务发展对本协议进行调整,并通过软件或网站进行公告。 + +如您不同意本协议,请立即停止使用本软件。 +` + +/** 供主进程 data: URL 内嵌窗口使用(依赖 preload 暴露 window.userAgreement) */ +export function agreementHtmlDocument(): string { + const esc = (s: string) => + s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + + const lines = USER_AGREEMENT_MARKDOWN.split('\n') + const body: string[] = [] + for (const line of lines) { + if (line.startsWith('# ')) { + body.push(`

${esc(line.slice(2))}

`) + } else if (line.startsWith('## ')) { + body.push(`

${esc(line.slice(3))}

`) + } else if (line.startsWith('* ')) { + body.push(`

• ${esc(line.slice(2))}

`) + } else if (line.trim() === '') { + body.push('
') + } else { + body.push(`

${esc(line)}

`) + } + } + + return ` + + +用户使用协议 + +
${body.join('')}
+ + +` +} diff --git a/electron/tsconfig.json b/electron/tsconfig.json index 9db05ad..ccea286 100644 --- a/electron/tsconfig.json +++ b/electron/tsconfig.json @@ -12,6 +12,7 @@ }, "include": [ "electron.vite.config.ts", + "src/shared/**/*.ts", "src/main/**/*.ts", "src/preload/**/*.ts", "src/renderer/**/*.ts",