This commit is contained in:
eric
2026-03-25 04:52:15 -05:00
parent 17ba5a83d1
commit 9f8cd2252f
11 changed files with 758 additions and 394 deletions

View File

@@ -6,13 +6,14 @@ import { app, BrowserWindow, ipcMain, shell } from 'electron'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
const VALID_NAV = new Set(['desk', 'record', 'network', 'proxy', 'system', 'wechat']) const VALID_NAV = new Set(['desk', 'record', 'network', 'remote', 'system', 'wechat'])
export function parseNavFromArgv(argv: string[]): string | null { export function parseNavFromArgv(argv: string[]): string | null {
for (const a of argv) { for (const a of argv) {
const m = /^--nav=(.+)$/.exec(a) const m = /^--nav=(.+)$/.exec(a)
if (!m) continue if (!m) continue
const id = m[1].trim() const id = m[1].trim()
if (id === 'proxy') return 'network'
if (VALID_NAV.has(id)) return id if (VALID_NAV.has(id)) return id
} }
return null return null
@@ -97,7 +98,7 @@ export function applyAppHostBootstrap(getBackendRoot: () => string): void {
program: process.execPath, program: process.execPath,
arguments: '--nav=network', arguments: '--nav=network',
title: '网络', title: '网络',
description: '网络与远程绑定', description: '网络测速与录制专用代理',
iconPath: process.execPath, iconPath: process.execPath,
iconIndex: 0, iconIndex: 0,
}, },

View File

@@ -42,6 +42,7 @@ import {
sendNavToWindow, sendNavToWindow,
setAppUserModelIdEarly, setAppUserModelIdEarly,
} from './appHost' } from './appHost'
import { setMainBrowserWindow } from './mainWindowRef'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@@ -399,6 +400,7 @@ async function createWindow(): Promise<void> {
nodeIntegration: false, nodeIntegration: false,
}, },
}) })
setMainBrowserWindow(mainWindow)
mainWindow.once('ready-to-show', () => mainWindow?.show()) mainWindow.once('ready-to-show', () => mainWindow?.show())
mainWindow.webContents.once('did-finish-load', () => { mainWindow.webContents.once('did-finish-load', () => {
appendClientLog('info', 'main', '渲染界面已加载') appendClientLog('info', 'main', '渲染界面已加载')
@@ -452,6 +454,7 @@ async function createWindow(): Promise<void> {
} }
mainWindow.on('closed', () => { mainWindow.on('closed', () => {
setMainBrowserWindow(null)
mainWindow = null mainWindow = null
}) })
} }

View File

@@ -0,0 +1,11 @@
import { BrowserWindow } from 'electron'
let mainWindowRef: BrowserWindow | null = null
export function setMainBrowserWindow(win: BrowserWindow | null): void {
mainWindowRef = win
}
export function getMainBrowserWindow(): BrowserWindow | null {
return mainWindowRef
}

View File

@@ -3,6 +3,7 @@
* 并可检测页面文本是否包含串流密钥后缀以便与本地配置对应。 * 并可检测页面文本是否包含串流密钥后缀以便与本地配置对应。
*/ */
import { BrowserWindow, app, dialog } from 'electron' import { BrowserWindow, app, dialog } from 'electron'
import { getMainBrowserWindow } from './mainWindowRef'
import { import {
getYoutubeStudioBrowserMode, getYoutubeStudioBrowserMode,
launchYoutubeStudioExternalChrome, launchYoutubeStudioExternalChrome,
@@ -102,7 +103,9 @@ export async function ensureYoutubeStudioWindow(streamKeySuffix: string | undefi
return return
} }
const parent = getMainBrowserWindow()
studioWin = new BrowserWindow({ studioWin = new BrowserWindow({
...(parent ? { parent } : {}),
width: 1100, width: 1100,
height: 720, height: 720,
minWidth: 800, minWidth: 800,

View File

@@ -443,6 +443,13 @@ body.ready {
color: var(--text-muted); color: var(--text-muted);
} }
.app-workspace__header-actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.app-workspace__scroll { .app-workspace__scroll {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
@@ -921,6 +928,11 @@ body.ready {
0 0 0 3px var(--accent-soft); 0 0 0 3px var(--accent-soft);
} }
.input-area--autosize {
min-height: 72px;
field-sizing: content;
}
.btn-row { .btn-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -1125,6 +1137,113 @@ body.ready {
box-shadow: var(--shadow-inset-top); box-shadow: var(--shadow-inset-top);
} }
.stream-key-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.stream-key-row .select-process {
flex: 0 0 auto;
min-width: 160px;
}
.stream-key-row__input {
flex: 1 1 220px;
min-width: 200px;
}
.yt-opt-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.yt-opt-item {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
.yt-opt-item .select-process {
width: 100%;
}
.live-log-detail {
display: flex;
flex-direction: column;
gap: 12px;
font-size: 13px;
}
.live-log-detail__row {
display: grid;
grid-template-columns: 140px 1fr;
gap: 10px;
align-items: start;
}
.live-log-detail__k {
color: var(--text-muted);
font-size: 12px;
}
.live-log-detail__v {
word-break: break-word;
}
.live-log-detail__row--top {
align-items: start;
}
.live-log-detail__v--block {
display: flex;
flex-direction: column;
gap: 6px;
}
.live-log-detail__preview-line {
word-break: break-all;
line-height: 1.45;
}
a.live-log-detail__link {
color: var(--accent);
text-decoration: underline;
cursor: pointer;
}
.live-log-detail__block {
display: flex;
flex-direction: column;
gap: 6px;
}
.live-log-detail__pre {
margin: 0;
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.5;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
background: var(--bg-input);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
}
.live-log-detail__pre--err {
color: #f0a8a8;
border-color: rgba(240, 100, 100, 0.35);
}
.log-wrap { .log-wrap {
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
@@ -1206,6 +1325,13 @@ body.ready {
flex-shrink: 0; flex-shrink: 0;
} }
.net-dash__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.net-dash__error { .net-dash__error {
margin: 0 0 12px; margin: 0 0 12px;
padding: 10px 12px; padding: 10px 12px;

File diff suppressed because it is too large Load Diff

View File

@@ -84,33 +84,6 @@ export default function AppHostControls() {
<span></span> <span></span>
</label> </label>
</div> </div>
<div className="app-host-block">
<div className="app-host-block__title"></div>
<div className="app-host-actions">
<button type="button" className="btn btn-secondary" onClick={() => void host.showAbout?.()}>
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
if (window.confirm('将重启整个客户端并重新加载本地服务,确定?')) void host.relaunch?.()
}}
>
</button>
<button type="button" className="btn btn-secondary" onClick={() => void host.openLogsFolder?.()}>
</button>
<button type="button" className="btn btn-secondary" onClick={() => void host.addRecentConfig?.()}>
使
</button>
</div>
<p className="app-host-hint">
Windows使 /
</p>
</div>
</div> </div>
</section> </section>
) )

View File

@@ -165,9 +165,23 @@ export default function NetworkDashboard() {
/ 256KB / 256KB
</p> </p>
</div> </div>
<button type="button" className="btn btn-secondary net-dash__refresh" onClick={() => void load()}> <div className="net-dash__actions">
<button type="button" className="btn btn-secondary net-dash__refresh" onClick={() => void load()}>
</button>
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
const open = window.recorderConsole?.openExternal
const url = 'https://nomadro.com/'
if (open) void open(url)
else window.open(url, '_blank', 'noopener,noreferrer')
}}
>
</button>
</div>
</div> </div>
{err && ( {err && (

View File

@@ -38,7 +38,7 @@ export default function ProxySettings() {
body: JSON.stringify(cfg), body: JSON.stringify(cfg),
}) })
if (!res.ok) throw new Error('保存失败') if (!res.ok) throw new Error('保存失败')
setMsg('已保存。重启录制任务后生效。') setMsg('已保存。重启直播任务后生效。')
} catch (e) { } catch (e) {
setMsg(e instanceof Error ? e.message : String(e)) setMsg(e instanceof Error ? e.message : String(e))
} finally { } finally {

View File

@@ -168,7 +168,7 @@ export default function RemoteBinding() {
if (!rb?.getStatus) { if (!rb?.getStatus) {
return ( return (
<section className="card card--remote"> <section className="card card--remote">
<h2 className="card__title"></h2> <h2 className="card__title"></h2>
<p className="card__desc">{msg || '不可用'}</p> <p className="card__desc">{msg || '不可用'}</p>
</section> </section>
) )
@@ -179,16 +179,8 @@ export default function RemoteBinding() {
return ( return (
<section className="card card--remote" aria-labelledby="remote-binding-title"> <section className="card card--remote" aria-labelledby="remote-binding-title">
<h2 className="card__title" id="remote-binding-title"> <h2 className="card__title" id="remote-binding-title">
</h2> </h2>
<p className="card__desc">
Windows {' '}
<a href="https://github.com/EasyTier/EasyTier" target="_blank" rel="noreferrer" className="remote-link">
EasyTier
</a>{' '}
App 使 EasyTier JSON {' '}
<code className="remote-code">easytier</code> 0.0.0.0 API
</p>
{status?.envHostOverride && ( {status?.envHostOverride && (
<p className="remote-warn" role="status"> <p className="remote-warn" role="status">

View File

@@ -17,6 +17,29 @@ export function parseYoutubeStreamKeySuffix(iniContent: string): string {
return '' return ''
} }
/** 从地址列表中提取完整抖音直播间 URL供预览接口 */
export function extractDouyinLiveUrls(urlIni: string): string[] {
const out: string[] = []
for (const line of urlIni.split(/\r?\n/)) {
const t = line.trim()
if (!t || t.startsWith('#')) continue
if (!/douyin\.com|v\.douyin\.com/i.test(t)) continue
const full = t.match(/(https?:\/\/live\.douyin\.com\/[a-zA-Z0-9_-]+)/i)
if (full) {
out.push(full[1].replace(/^http:\/\//i, 'https://'))
continue
}
const rel = t.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i)
if (rel) {
out.push(`https://live.douyin.com/${rel[1]}`)
continue
}
const n = t.match(/(\d{8,})/)
if (n) out.push(`https://live.douyin.com/${n[1]}`)
}
return [...new Set(out)].slice(0, 16)
}
/** URL_config.ini 文本中提取抖音直播间线索(房间号或短链片段) */ /** URL_config.ini 文本中提取抖音直播间线索(房间号或短链片段) */
export function extractDouyinRoomHints(urlIni: string): string[] { export function extractDouyinRoomHints(urlIni: string): string[] {
const out: string[] = [] const out: string[] = []