This commit is contained in:
Yvan
2026-09-23 17:42:55 +08:00
parent 0a8ecbe09b
commit 435b601273
42 changed files with 2845 additions and 154 deletions
+24
View File
@@ -0,0 +1,24 @@
export async function copyLiveText(value, {
clipboard = navigator.clipboard,
documentRef = document,
} = {}) {
const text = String(value)
if (clipboard?.writeText) {
try { await clipboard.writeText(text); return }
catch { /* Older or restricted browsers can still support a selection copy. */ }
}
const previousFocus = documentRef.activeElement
const input = documentRef.createElement('textarea')
input.value = text
input.readOnly = true
input.style.position = 'fixed'
input.style.left = '-9999px'
documentRef.body.appendChild(input)
try {
input.select()
if (!documentRef.execCommand('copy')) throw new Error('Clipboard copy failed')
} finally {
input.remove()
previousFocus?.focus?.({ preventScroll: true })
}
}
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { copyLiveText } from './clipboard.js'
function legacyDocument(success) {
const input = { style: {}, select() { this.selected = true }, remove() { this.removed = true } }
return {
input,
activeElement: { focus() {} },
createElement: () => input,
body: { appendChild() {} },
execCommand: () => success,
}
}
test('copies an absolute link through the modern clipboard', async () => {
let copied
await copyLiveText('https://live.example.com/#/studio/2', {
clipboard: { async writeText(value) { copied = value } },
documentRef: {},
})
assert.equal(copied, 'https://live.example.com/#/studio/2')
})
test('uses a selection fallback when clipboard access is denied', async () => {
const documentRef = legacyDocument(true)
await copyLiveText('viewer-link', {
clipboard: { async writeText() { throw new Error('denied') } },
documentRef,
})
assert.equal(documentRef.input.value, 'viewer-link')
assert.equal(documentRef.input.selected, true)
assert.equal(documentRef.input.removed, true)
})
test('does not report success when both browser copy mechanisms fail', async () => {
const documentRef = legacyDocument(false)
await assert.rejects(copyLiveText('viewer-link', { clipboard: null, documentRef }), /copy failed/)
assert.equal(documentRef.input.removed, true)
})
+21
View File
@@ -0,0 +1,21 @@
export function getRoomLinks(roomId, {
origin = window.location.origin,
clientBaseUrl = import.meta.env?.VITE_LIVE_CLIENT_URL,
development = import.meta.env?.DEV,
} = {}) {
const id = Number(roomId)
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Invalid live room ID')
const clientBase = String(clientBaseUrl || '').trim()
const url = new URL(clientBase || '/', origin)
if (!clientBase && development) url.port = '5173'
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
throw new Error('Invalid live client URL')
}
url.search = ''
if (!/\.html?$/i.test(url.pathname)) url.pathname = `${url.pathname.replace(/\/+$/, '')}/`
url.hash = `/live/${id}`
const viewer = url.toString()
url.hash = `/studio/${id}`
return { viewer, studio: url.toString() }
}
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { getRoomLinks } from './roomLinks.js'
test('production links target the PC routes, not the administration path', () => {
assert.deepEqual(getRoomLinks(2, { origin: 'https://live.example.com', development: false }), {
viewer: 'https://live.example.com/#/live/2',
studio: 'https://live.example.com/#/studio/2',
})
})
test('custom client origins and base paths discard unrelated query and hash state', () => {
assert.deepEqual(getRoomLinks('12', {
origin: 'https://admin.example.com',
clientBaseUrl: 'https://client.example.com/player?token=unused#/old',
}), {
viewer: 'https://client.example.com/player/#/live/12',
studio: 'https://client.example.com/player/#/studio/12',
})
})
test('development links use the PC dev server and support an explicit port override', () => {
assert.equal(getRoomLinks(1, { origin: 'http://localhost:8080', development: true }).viewer, 'http://localhost:5173/#/live/1')
assert.equal(getRoomLinks(1, {
origin: 'http://localhost:8080',
development: true,
clientBaseUrl: 'http://localhost:5174/',
}).studio, 'http://localhost:5174/#/studio/1')
})
test('client base supports a relative path and a static HTML entrypoint', () => {
assert.equal(getRoomLinks(3, {
origin: 'https://live.example.com',
clientBaseUrl: '/pc/index.html',
}).viewer, 'https://live.example.com/pc/index.html#/live/3')
})
test('invalid room IDs, executable schemes and URL credentials are rejected', () => {
for (const id of [0, -1, 1.2, '2/other', undefined]) {
assert.throws(() => getRoomLinks(id, { origin: 'https://live.example.com' }))
}
for (const clientBaseUrl of ['javascript:alert(1)', 'https://user:password@example.com/']) {
assert.throws(() => getRoomLinks(2, { origin: 'https://live.example.com', clientBaseUrl }))
}
})