's'
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
11
electron/src/main/mainWindowRef.ts
Normal file
11
electron/src/main/mainWindowRef.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -165,9 +165,23 @@ export default function NetworkDashboard() {
|
||||
下行 / 256KB 上行)。
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary net-dash__refresh" onClick={() => void load()}>
|
||||
重新检测
|
||||
</button>
|
||||
<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 && (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
Reference in New Issue
Block a user