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 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 {
for (const a of argv) {
const m = /^--nav=(.+)$/.exec(a)
if (!m) continue
const id = m[1].trim()
if (id === 'proxy') return 'network'
if (VALID_NAV.has(id)) return id
}
return null
@@ -97,7 +98,7 @@ export function applyAppHostBootstrap(getBackendRoot: () => string): void {
program: process.execPath,
arguments: '--nav=network',
title: '网络',
description: '网络与远程绑定',
description: '网络测速与录制专用代理',
iconPath: process.execPath,
iconIndex: 0,
},

View File

@@ -42,6 +42,7 @@ import {
sendNavToWindow,
setAppUserModelIdEarly,
} from './appHost'
import { setMainBrowserWindow } from './mainWindowRef'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -399,6 +400,7 @@ async function createWindow(): Promise<void> {
nodeIntegration: false,
},
})
setMainBrowserWindow(mainWindow)
mainWindow.once('ready-to-show', () => mainWindow?.show())
mainWindow.webContents.once('did-finish-load', () => {
appendClientLog('info', 'main', '渲染界面已加载')
@@ -452,6 +454,7 @@ async function createWindow(): Promise<void> {
}
mainWindow.on('closed', () => {
setMainBrowserWindow(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 { getMainBrowserWindow } from './mainWindowRef'
import {
getYoutubeStudioBrowserMode,
launchYoutubeStudioExternalChrome,
@@ -102,7 +103,9 @@ export async function ensureYoutubeStudioWindow(streamKeySuffix: string | undefi
return
}
const parent = getMainBrowserWindow()
studioWin = new BrowserWindow({
...(parent ? { parent } : {}),
width: 1100,
height: 720,
minWidth: 800,

View File

@@ -443,6 +443,13 @@ body.ready {
color: var(--text-muted);
}
.app-workspace__header-actions {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.app-workspace__scroll {
flex: 1;
overflow: auto;
@@ -921,6 +928,11 @@ body.ready {
0 0 0 3px var(--accent-soft);
}
.input-area--autosize {
min-height: 72px;
field-sizing: content;
}
.btn-row {
display: flex;
flex-wrap: wrap;
@@ -1125,6 +1137,113 @@ body.ready {
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 {
border-radius: var(--radius-md);
overflow: hidden;
@@ -1206,6 +1325,13 @@ body.ready {
flex-shrink: 0;
}
.net-dash__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.net-dash__error {
margin: 0 0 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>
</label>
</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>
</section>
)

View File

@@ -165,9 +165,23 @@ export default function NetworkDashboard() {
/ 256KB
</p>
</div>
<div className="net-dash__actions">
<button type="button" className="btn btn-secondary net-dash__refresh" onClick={() => void load()}>
</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>
{err && (

View File

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

View File

@@ -168,7 +168,7 @@ export default function RemoteBinding() {
if (!rb?.getStatus) {
return (
<section className="card card--remote">
<h2 className="card__title"></h2>
<h2 className="card__title"></h2>
<p className="card__desc">{msg || '不可用'}</p>
</section>
)
@@ -179,16 +179,8 @@ export default function RemoteBinding() {
return (
<section className="card card--remote" aria-labelledby="remote-binding-title">
<h2 className="card__title" id="remote-binding-title">
</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 && (
<p className="remote-warn" role="status">

View File

@@ -17,6 +17,29 @@ export function parseYoutubeStreamKeySuffix(iniContent: string): string {
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 文本中提取抖音直播间线索(房间号或短链片段) */
export function extractDouyinRoomHints(urlIni: string): string[] {
const out: string[] = []