's'
This commit is contained in:
@@ -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()],
|
||||
|
||||
9
electron/pnpm-lock.yaml
generated
9
electron/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
83
electron/src/main/userAgreementGate.ts
Normal file
83
electron/src/main/userAgreementGate.ts
Normal file
@@ -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<void> {
|
||||
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'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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<LoginItem | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [err, setErr] = useState<string | null>(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 (
|
||||
<section className="card app-host-card" aria-labelledby="app-host-title">
|
||||
<h2 className="card__title" id="app-host-title">
|
||||
客户端与系统集成
|
||||
</h2>
|
||||
<p className="card__desc">此区域仅在 Electron 桌面客户端可用。</p>
|
||||
</section>
|
||||
<>
|
||||
<section className="card app-host-card" aria-labelledby="app-host-title">
|
||||
<h2 className="card__title" id="app-host-title">
|
||||
客户端与系统集成
|
||||
</h2>
|
||||
<p className="card__desc">此区域仅在 Electron 桌面客户端可用。</p>
|
||||
<button type="button" className="user-agreement-inline-link" onClick={() => setAgreementOpen(true)}>
|
||||
用户协议
|
||||
</button>
|
||||
</section>
|
||||
<UserAgreementDialog open={agreementOpen} onClose={() => setAgreementOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,6 +91,10 @@ export default function AppHostControls() {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="user-agreement-inline-link" onClick={() => setAgreementOpen(true)}>
|
||||
用户协议
|
||||
</button>
|
||||
<UserAgreementDialog open={agreementOpen} onClose={() => setAgreementOpen(false)} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
87
electron/src/renderer/src/UserAgreementDialog.tsx
Normal file
87
electron/src/renderer/src/UserAgreementDialog.tsx
Normal file
@@ -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(
|
||||
<h3 key={i} className="user-agreement-dialog__h2">
|
||||
{line.slice(3)}
|
||||
</h3>,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (line.startsWith('* ')) {
|
||||
out.push(
|
||||
<p key={i} className="user-agreement-dialog__li">
|
||||
• {line.slice(2)}
|
||||
</p>,
|
||||
)
|
||||
return
|
||||
}
|
||||
if (line.trim() === '') {
|
||||
out.push(<div key={i} className="user-agreement-dialog__gap" />)
|
||||
return
|
||||
}
|
||||
out.push(
|
||||
<p key={i} className="user-agreement-dialog__p">
|
||||
{line}
|
||||
</p>,
|
||||
)
|
||||
})
|
||||
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 (
|
||||
<div
|
||||
className="user-agreement-backdrop"
|
||||
role="presentation"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="user-agreement-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="user-agreement-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 id="user-agreement-title" className="user-agreement-sheet__title">
|
||||
用户使用协议
|
||||
</h2>
|
||||
<div className="user-agreement-sheet__scroll">{renderAgreementBody(USER_AGREEMENT_MARKDOWN)}</div>
|
||||
<div className="user-agreement-sheet__footer">
|
||||
<button type="button" className="btn btn-primary" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
4
electron/src/renderer/src/vite-env.d.ts
vendored
4
electron/src/renderer/src/vite-env.d.ts
vendored
@@ -49,6 +49,10 @@ interface Window {
|
||||
get?: () => Promise<string | null>
|
||||
set?: (value: string) => Promise<void>
|
||||
}
|
||||
userAgreement?: {
|
||||
accept: () => void
|
||||
refuse: () => void
|
||||
}
|
||||
relayConfig?: {
|
||||
syncConfig?: () => Promise<{ ok: true }>
|
||||
importFromConfig?: () => Promise<{ ok: true }>
|
||||
|
||||
100
electron/src/shared/userAgreement.ts
Normal file
100
electron/src/shared/userAgreement.ts
Normal file
@@ -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, '>')
|
||||
.replace(/"/g, '"')
|
||||
|
||||
const lines = USER_AGREEMENT_MARKDOWN.split('\n')
|
||||
const body: string[] = []
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('# ')) {
|
||||
body.push(`<h1>${esc(line.slice(2))}</h1>`)
|
||||
} else if (line.startsWith('## ')) {
|
||||
body.push(`<h2>${esc(line.slice(3))}</h2>`)
|
||||
} else if (line.startsWith('* ')) {
|
||||
body.push(`<p class="li">• ${esc(line.slice(2))}</p>`)
|
||||
} else if (line.trim() === '') {
|
||||
body.push('<div class="sp"></div>')
|
||||
} else {
|
||||
body.push(`<p>${esc(line)}</p>`)
|
||||
}
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head>
|
||||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>用户使用协议</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;font-size:13px;line-height:1.55;color:#e8e6f2;background:#14121a;display:flex;flex-direction:column;height:100vh}
|
||||
h1{font-size:17px;font-weight:700;margin:0 0 12px;color:#f2f0fa}
|
||||
h2{font-size:14px;font-weight:600;margin:14px 0 8px;color:#e8e6f2}
|
||||
p{margin:0 0 8px;color:#a39bb8}
|
||||
p.li{padding-left:4px}
|
||||
.sp{height:6px}
|
||||
.scroll{flex:1;overflow:auto;padding:16px 18px 12px}
|
||||
.footer{flex-shrink:0;display:flex;gap:10px;padding:12px 16px 16px;border-top:1px solid rgba(124,92,255,.25);background:#1a1722}
|
||||
button{flex:1;padding:11px 14px;border-radius:8px;border:1px solid rgba(124,92,255,.35);font-size:14px;font-weight:600;cursor:pointer}
|
||||
.primary{background:linear-gradient(135deg,#5c4dff,#3d7eff);color:#fff;border:none}
|
||||
.ghost{background:transparent;color:#c4bdd9}
|
||||
</style></head><body>
|
||||
<div class="scroll">${body.join('')}</div>
|
||||
<div class="footer">
|
||||
<button type="button" class="ghost" id="refuse">不同意并退出</button>
|
||||
<button type="button" class="primary" id="accept">同意并继续</button>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('accept').addEventListener('click',function(){
|
||||
if(window.userAgreement) window.userAgreement.accept();
|
||||
});
|
||||
document.getElementById('refuse').addEventListener('click',function(){
|
||||
if(window.userAgreement) window.userAgreement.refuse();
|
||||
});
|
||||
</script>
|
||||
</body></html>`
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"include": [
|
||||
"electron.vite.config.ts",
|
||||
"src/shared/**/*.ts",
|
||||
"src/main/**/*.ts",
|
||||
"src/preload/**/*.ts",
|
||||
"src/renderer/**/*.ts",
|
||||
|
||||
Reference in New Issue
Block a user