Expose overlay alert playback settings
This commit is contained in:
+20
-7
@@ -192,7 +192,7 @@
|
|||||||
let pluginError = '';
|
let pluginError = '';
|
||||||
let pluginBusy = false;
|
let pluginBusy = false;
|
||||||
let pluginDropActive = false;
|
let pluginDropActive = false;
|
||||||
let overlays: Array<{ id: string; name: string; slug: string; draft_html: string; draft_css: string; draft_javascript: string; published: boolean; enabled: boolean; disabled_design: string }> = [];
|
let overlays: Array<{ id: string; name: string; slug: string; draft_html: string; draft_css: string; draft_javascript: string; published: boolean; enabled: boolean; disabled_design: string; alert_group_window_ms: number; alert_group_size: number; alert_display_ms: number }> = [];
|
||||||
let selectedOverlayId = '';
|
let selectedOverlayId = '';
|
||||||
let overlayName = '';
|
let overlayName = '';
|
||||||
let overlaySlug = '';
|
let overlaySlug = '';
|
||||||
@@ -201,11 +201,14 @@
|
|||||||
let overlayJavascript = '';
|
let overlayJavascript = '';
|
||||||
let overlayEnabled = true;
|
let overlayEnabled = true;
|
||||||
let overlayDisabledDesign = 'transparent';
|
let overlayDisabledDesign = 'transparent';
|
||||||
|
let overlayAlertGroupWindowMs = 200;
|
||||||
|
let overlayAlertGroupSize = 5;
|
||||||
|
let overlayAlertDisplayMs = 4000;
|
||||||
let overlayError = '';
|
let overlayError = '';
|
||||||
let overlayBusy = false;
|
let overlayBusy = false;
|
||||||
let overlaySavedSignature = '';
|
let overlaySavedSignature = '';
|
||||||
let overlayAutosaveAttemptedSignature = '';
|
let overlayAutosaveAttemptedSignature = '';
|
||||||
let pendingOverlayDraft: { overlayName: string; overlaySlug: string; overlayHtml: string; overlayCss: string; overlayJavascript: string; overlayEnabled: boolean; overlayDisabledDesign: string } | null = null;
|
let pendingOverlayDraft: { overlayName: string; overlaySlug: string; overlayHtml: string; overlayCss: string; overlayJavascript: string; overlayEnabled: boolean; overlayDisabledDesign: string; overlayAlertGroupWindowMs: number; overlayAlertGroupSize: number; overlayAlertDisplayMs: number } | null = null;
|
||||||
let overlayDiagnosticsOpen = false;
|
let overlayDiagnosticsOpen = false;
|
||||||
let overlayAutosaveTimer: number | undefined;
|
let overlayAutosaveTimer: number | undefined;
|
||||||
let oauthScopes: Record<'streamer' | 'bot', { connected: boolean; missing_scopes: string[]; reauthorization_required: boolean }> = {
|
let oauthScopes: Record<'streamer' | 'bot', { connected: boolean; missing_scopes: string[]; reauthorization_required: boolean }> = {
|
||||||
@@ -1197,13 +1200,16 @@
|
|||||||
overlayJavascript = overlay.draft_javascript;
|
overlayJavascript = overlay.draft_javascript;
|
||||||
overlayEnabled = overlay.enabled;
|
overlayEnabled = overlay.enabled;
|
||||||
overlayDisabledDesign = overlay.disabled_design;
|
overlayDisabledDesign = overlay.disabled_design;
|
||||||
|
overlayAlertGroupWindowMs = overlay.alert_group_window_ms ?? 200;
|
||||||
|
overlayAlertGroupSize = overlay.alert_group_size ?? 5;
|
||||||
|
overlayAlertDisplayMs = overlay.alert_display_ms ?? 4000;
|
||||||
overlaySavedSignature = overlayDraftSignature();
|
overlaySavedSignature = overlayDraftSignature();
|
||||||
overlayAutosaveAttemptedSignature = overlaySavedSignature;
|
overlayAutosaveAttemptedSignature = overlaySavedSignature;
|
||||||
restoreOverlayDraft(overlay.id);
|
restoreOverlayDraft(overlay.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function overlayDraftSignature() {
|
function overlayDraftSignature() {
|
||||||
return JSON.stringify({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign });
|
return JSON.stringify({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign, overlayAlertGroupWindowMs, overlayAlertGroupSize, overlayAlertDisplayMs });
|
||||||
}
|
}
|
||||||
|
|
||||||
function overlayVariableWarnings() {
|
function overlayVariableWarnings() {
|
||||||
@@ -1228,7 +1234,7 @@
|
|||||||
function cacheOverlayDraft() {
|
function cacheOverlayDraft() {
|
||||||
if (!selectedOverlayId) return;
|
if (!selectedOverlayId) return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(overlayDraftCacheKey(selectedOverlayId), JSON.stringify({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign }));
|
localStorage.setItem(overlayDraftCacheKey(selectedOverlayId), JSON.stringify({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign, overlayAlertGroupWindowMs, overlayAlertGroupSize, overlayAlertDisplayMs }));
|
||||||
} catch {
|
} catch {
|
||||||
// Local storage is an optional recovery aid; server persistence remains authoritative.
|
// Local storage is an optional recovery aid; server persistence remains authoritative.
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1242,7 @@
|
|||||||
|
|
||||||
function restoreOverlayDraft(id: string) {
|
function restoreOverlayDraft(id: string) {
|
||||||
try {
|
try {
|
||||||
const cached = JSON.parse(localStorage.getItem(overlayDraftCacheKey(id)) || 'null') as Partial<{ overlayName: string; overlaySlug: string; overlayHtml: string; overlayCss: string; overlayJavascript: string; overlayEnabled: boolean; overlayDisabledDesign: string }> | null;
|
const cached = JSON.parse(localStorage.getItem(overlayDraftCacheKey(id)) || 'null') as Partial<{ overlayName: string; overlaySlug: string; overlayHtml: string; overlayCss: string; overlayJavascript: string; overlayEnabled: boolean; overlayDisabledDesign: string; overlayAlertGroupWindowMs: number; overlayAlertGroupSize: number; overlayAlertDisplayMs: number }> | null;
|
||||||
if (!cached || typeof cached.overlayName !== 'string') return;
|
if (!cached || typeof cached.overlayName !== 'string') return;
|
||||||
const cachedSignature = JSON.stringify(cached);
|
const cachedSignature = JSON.stringify(cached);
|
||||||
if (cachedSignature !== overlayDraftSignature()) pendingOverlayDraft = {
|
if (cachedSignature !== overlayDraftSignature()) pendingOverlayDraft = {
|
||||||
@@ -1247,6 +1253,9 @@
|
|||||||
overlayJavascript: cached.overlayJavascript ?? overlayJavascript,
|
overlayJavascript: cached.overlayJavascript ?? overlayJavascript,
|
||||||
overlayEnabled: cached.overlayEnabled ?? overlayEnabled,
|
overlayEnabled: cached.overlayEnabled ?? overlayEnabled,
|
||||||
overlayDisabledDesign: cached.overlayDisabledDesign ?? overlayDisabledDesign,
|
overlayDisabledDesign: cached.overlayDisabledDesign ?? overlayDisabledDesign,
|
||||||
|
overlayAlertGroupWindowMs: cached.overlayAlertGroupWindowMs ?? overlayAlertGroupWindowMs,
|
||||||
|
overlayAlertGroupSize: cached.overlayAlertGroupSize ?? overlayAlertGroupSize,
|
||||||
|
overlayAlertDisplayMs: cached.overlayAlertDisplayMs ?? overlayAlertDisplayMs,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore malformed or unavailable local cache entries.
|
// Ignore malformed or unavailable local cache entries.
|
||||||
@@ -1267,11 +1276,14 @@
|
|||||||
overlayJavascript = "const alertBox = document.getElementById('trixie-follow-alert'); const alertName = document.getElementById('trixie-follow-name'); window.addEventListener('trixie-alert', (event) => { const detail = event.detail || {}; const payload = detail.payload || {}; if (detail.event_type !== 'follow') return; alertName.textContent = String(payload.user_name || payload.user_login || 'Someone'); alertBox.classList.add('visible'); clearTimeout(window.trixieAlertTimer); window.trixieAlertTimer = setTimeout(() => alertBox.classList.remove('visible'), 6000); });";
|
overlayJavascript = "const alertBox = document.getElementById('trixie-follow-alert'); const alertName = document.getElementById('trixie-follow-name'); window.addEventListener('trixie-alert', (event) => { const detail = event.detail || {}; const payload = detail.payload || {}; if (detail.event_type !== 'follow') return; alertName.textContent = String(payload.user_name || payload.user_login || 'Someone'); alertBox.classList.add('visible'); clearTimeout(window.trixieAlertTimer); window.trixieAlertTimer = setTimeout(() => alertBox.classList.remove('visible'), 6000); });";
|
||||||
overlayEnabled = true;
|
overlayEnabled = true;
|
||||||
overlayDisabledDesign = 'transparent';
|
overlayDisabledDesign = 'transparent';
|
||||||
|
overlayAlertGroupWindowMs = 200;
|
||||||
|
overlayAlertGroupSize = 5;
|
||||||
|
overlayAlertDisplayMs = 4000;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restorePendingOverlayDraft() {
|
function restorePendingOverlayDraft() {
|
||||||
if (!pendingOverlayDraft) return;
|
if (!pendingOverlayDraft) return;
|
||||||
({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign } = pendingOverlayDraft);
|
({ overlayName, overlaySlug, overlayHtml, overlayCss, overlayJavascript, overlayEnabled, overlayDisabledDesign, overlayAlertGroupWindowMs, overlayAlertGroupSize, overlayAlertDisplayMs } = pendingOverlayDraft);
|
||||||
pendingOverlayDraft = null;
|
pendingOverlayDraft = null;
|
||||||
overlaySavedSignature = '';
|
overlaySavedSignature = '';
|
||||||
overlayAutosaveAttemptedSignature = '';
|
overlayAutosaveAttemptedSignature = '';
|
||||||
@@ -1287,7 +1299,7 @@
|
|||||||
overlayBusy = true;
|
overlayBusy = true;
|
||||||
overlayError = '';
|
overlayError = '';
|
||||||
try {
|
try {
|
||||||
const body = JSON.stringify({ name: overlayName, slug: overlaySlug, html: overlayHtml, css: overlayCss, javascript: overlayJavascript, disabled_design: overlayDisabledDesign, enabled: overlayEnabled });
|
const body = JSON.stringify({ name: overlayName, slug: overlaySlug, html: overlayHtml, css: overlayCss, javascript: overlayJavascript, disabled_design: overlayDisabledDesign, enabled: overlayEnabled, alert_group_window_ms: Number(overlayAlertGroupWindowMs), alert_group_size: Number(overlayAlertGroupSize), alert_display_ms: Number(overlayAlertDisplayMs) });
|
||||||
const response = await fetch(selectedOverlayId ? `${serverAddress}/api/v1/overlays/${selectedOverlayId}` : `${serverAddress}/api/v1/overlays`, { method: selectedOverlayId ? 'PATCH' : 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, body });
|
const response = await fetch(selectedOverlayId ? `${serverAddress}/api/v1/overlays/${selectedOverlayId}` : `${serverAddress}/api/v1/overlays`, { method: selectedOverlayId ? 'PATCH' : 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, body });
|
||||||
if (!response.ok) throw new Error('The overlay draft could not be saved.');
|
if (!response.ok) throw new Error('The overlay draft could not be saved.');
|
||||||
const saved = await response.json();
|
const saved = await response.json();
|
||||||
@@ -1509,6 +1521,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if activeView === 'Overlays'}
|
{:else if activeView === 'Overlays'}
|
||||||
<section class="overlay-page panel"><div class="panel-header"><span>OVERLAYS</span><div class="overlay-toolbar"><button class="secondary" onclick={createOverlay}>NEW</button><button class="secondary" onclick={loadOverlays}>REFRESH</button></div></div><div class="overlay-layout"><aside class="overlay-list">{#each overlays as overlay}<button class:active={selectedOverlayId === overlay.id} class="overlay-list-item" onclick={() => selectOverlay(overlay)}><strong>{overlay.name}</strong><span>{overlay.published ? 'PUBLISHED' : 'DRAFT'}</span></button>{/each}{#if !overlays.length}<div class="empty-row">No overlays yet.</div>{/if}</aside><div class="overlay-editor">{#if selectedOverlayId || overlayName}<div class="overlay-fields"><input class="control" bind:value={overlayName} maxlength="100" placeholder="Overlay name" /><input class="control" bind:value={overlaySlug} maxlength="100" placeholder="stable-slug" /><label class="overlay-enabled"><input type="checkbox" bind:checked={overlayEnabled} /> ENABLED</label><input class="control" bind:value={overlayDisabledDesign} maxlength="10000" placeholder="Disabled design" /></div><div class="overlay-tabs"><label class="micro" for="overlay-html">HTML</label><textarea id="overlay-html" class="overlay-code" bind:value={overlayHtml} spellcheck="false"></textarea><label class="micro" for="overlay-css">CSS</label><textarea id="overlay-css" class="overlay-code" bind:value={overlayCss} spellcheck="false"></textarea><label class="micro" for="overlay-javascript">JAVASCRIPT</label><textarea id="overlay-javascript" class="overlay-code" bind:value={overlayJavascript} spellcheck="false"></textarea></div><div class="overlay-preview-label micro">LIVE PREVIEW</div><iframe class="overlay-preview" title="Overlay preview" sandbox="allow-scripts" srcdoc={`<style>${overlayCss}</style>${overlayEnabled ? overlayHtml : `<div>${overlayDisabledDesign}</div>`}\u003cscript\u003e${overlayJavascript}\u003c/script\u003e`}></iframe><div class="overlay-controls"><button class="primary" onclick={saveOverlay} disabled={overlayBusy || !overlayName || !overlaySlug}>SAVE DRAFT</button><button class="secondary" onclick={publishOverlay} disabled={overlayBusy || !selectedOverlayId}>PUBLISH</button><button class="secondary" onclick={undoOverlay} disabled={overlayBusy || !selectedOverlayId}>UNDO PUBLISHED</button><button class="secondary" onclick={copyOverlayUrl} disabled={!overlaySlug}>COPY OBS URL</button></div>{#if overlayError}<div class="auth-error">{overlayError}</div>{/if}{:else}<div class="empty-state"><strong>Select or create an overlay</strong><span>Drafts autosave when explicitly saved; publishing controls the browser source.</span></div>{/if}</div></div></section>
|
<section class="overlay-page panel"><div class="panel-header"><span>OVERLAYS</span><div class="overlay-toolbar"><button class="secondary" onclick={createOverlay}>NEW</button><button class="secondary" onclick={loadOverlays}>REFRESH</button></div></div><div class="overlay-layout"><aside class="overlay-list">{#each overlays as overlay}<button class:active={selectedOverlayId === overlay.id} class="overlay-list-item" onclick={() => selectOverlay(overlay)}><strong>{overlay.name}</strong><span>{overlay.published ? 'PUBLISHED' : 'DRAFT'}</span></button>{/each}{#if !overlays.length}<div class="empty-row">No overlays yet.</div>{/if}</aside><div class="overlay-editor">{#if selectedOverlayId || overlayName}<div class="overlay-fields"><input class="control" bind:value={overlayName} maxlength="100" placeholder="Overlay name" /><input class="control" bind:value={overlaySlug} maxlength="100" placeholder="stable-slug" /><label class="overlay-enabled"><input type="checkbox" bind:checked={overlayEnabled} /> ENABLED</label><input class="control" bind:value={overlayDisabledDesign} maxlength="10000" placeholder="Disabled design" /></div><div class="overlay-tabs"><label class="micro" for="overlay-html">HTML</label><textarea id="overlay-html" class="overlay-code" bind:value={overlayHtml} spellcheck="false"></textarea><label class="micro" for="overlay-css">CSS</label><textarea id="overlay-css" class="overlay-code" bind:value={overlayCss} spellcheck="false"></textarea><label class="micro" for="overlay-javascript">JAVASCRIPT</label><textarea id="overlay-javascript" class="overlay-code" bind:value={overlayJavascript} spellcheck="false"></textarea></div><div class="overlay-preview-label micro">LIVE PREVIEW</div><iframe class="overlay-preview" title="Overlay preview" sandbox="allow-scripts" srcdoc={`<style>${overlayCss}</style>${overlayEnabled ? overlayHtml : `<div>${overlayDisabledDesign}</div>`}\u003cscript\u003e${overlayJavascript}\u003c/script\u003e`}></iframe><div class="overlay-controls"><button class="primary" onclick={saveOverlay} disabled={overlayBusy || !overlayName || !overlaySlug}>SAVE DRAFT</button><button class="secondary" onclick={publishOverlay} disabled={overlayBusy || !selectedOverlayId}>PUBLISH</button><button class="secondary" onclick={undoOverlay} disabled={overlayBusy || !selectedOverlayId}>UNDO PUBLISHED</button><button class="secondary" onclick={copyOverlayUrl} disabled={!overlaySlug}>COPY OBS URL</button></div>{#if overlayError}<div class="auth-error">{overlayError}</div>{/if}{:else}<div class="empty-state"><strong>Select or create an overlay</strong><span>Drafts autosave when explicitly saved; publishing controls the browser source.</span></div>{/if}</div></div></section>
|
||||||
|
<section class="panel overlay-alert-settings-panel"><div class="panel-header"><span>ALERT PLAYBACK</span></div><div class="settings-body"><p class="settings-note">Configure grouping and playback for the selected browser-source overlay. Changes are saved with the next draft save.</p><div class="command-form-grid"><label class="micro" for="overlay-alert-window">GROUP WINDOW (MS)</label><input id="overlay-alert-window" class="control" type="number" min="0" max="5000" bind:value={overlayAlertGroupWindowMs} /><label class="micro" for="overlay-alert-size">MAX GROUP SIZE</label><input id="overlay-alert-size" class="control" type="number" min="1" max="20" bind:value={overlayAlertGroupSize} /><label class="micro" for="overlay-alert-display">DISPLAY TIME (MS)</label><input id="overlay-alert-display" class="control" type="number" min="500" max="60000" bind:value={overlayAlertDisplayMs} /></div></div></section>
|
||||||
{:else if activeView === 'Settings'}
|
{:else if activeView === 'Settings'}
|
||||||
<section class="settings-page panel"><div class="panel-header"><span>SERVER SETTINGS</span><button class="secondary" onclick={loadBackups}>REFRESH</button></div><div class="settings-body"><div class="micro">DATABASE BACKUPS</div><p class="settings-note">Daily backups use the server's local schedule. Restore replaces the current database and is permanently destructive.</p><button class="primary control-button" onclick={runBackup} disabled={backupBusy || restoreBusy}>{backupBusy ? 'RUNNING…' : 'RUN BACKUP NOW'}</button>{#if backupError}<div class="auth-error">{backupError}</div>{/if}{#if backups.length}<div class="backup-list">{#each backups as backup}<div class="backup-row"><span class="mono">{backup.filename}</span><span>{Math.round(backup.size_bytes / 1024)} KiB</span><span class="micro">{backup.modified_at ? new Date(backup.modified_at).toLocaleString() : 'UNKNOWN TIME'}</span><button class="danger" onclick={() => restoreBackup(backup.filename)} disabled={backupBusy || restoreBusy}>RESTORE</button></div>{/each}</div>{:else}<div class="empty-row">No backup archives are available.</div>{/if}</div></section>
|
<section class="settings-page panel"><div class="panel-header"><span>SERVER SETTINGS</span><button class="secondary" onclick={loadBackups}>REFRESH</button></div><div class="settings-body"><div class="micro">DATABASE BACKUPS</div><p class="settings-note">Daily backups use the server's local schedule. Restore replaces the current database and is permanently destructive.</p><button class="primary control-button" onclick={runBackup} disabled={backupBusy || restoreBusy}>{backupBusy ? 'RUNNING…' : 'RUN BACKUP NOW'}</button>{#if backupError}<div class="auth-error">{backupError}</div>{/if}{#if backups.length}<div class="backup-list">{#each backups as backup}<div class="backup-row"><span class="mono">{backup.filename}</span><span>{Math.round(backup.size_bytes / 1024)} KiB</span><span class="micro">{backup.modified_at ? new Date(backup.modified_at).toLocaleString() : 'UNKNOWN TIME'}</span><button class="danger" onclick={() => restoreBackup(backup.filename)} disabled={backupBusy || restoreBusy}>RESTORE</button></div>{/each}</div>{:else}<div class="empty-row">No backup archives are available.</div>{/if}</div></section>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user