diff --git a/src/App.svelte b/src/App.svelte index d681688..6f23d47 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -68,6 +68,17 @@ let secretDescription = ''; let secretValue = ''; let secretError = ''; + 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 selectedOverlayId = ''; + let overlayName = ''; + let overlaySlug = ''; + let overlayHtml = ''; + let overlayCss = ''; + let overlayJavascript = ''; + let overlayEnabled = true; + let overlayDisabledDesign = 'transparent'; + let overlayError = ''; + let overlayBusy = false; let oauthScopes: Record<'streamer' | 'bot', { connected: boolean; missing_scopes: string[]; reauthorization_required: boolean }> = { streamer: { connected: false, missing_scopes: [], reauthorization_required: false }, bot: { connected: false, missing_scopes: [], reauthorization_required: false }, @@ -122,6 +133,7 @@ await loadCollaboration(); await loadBotCommands(); await loadBotSecrets(); + await loadOverlays(); await loadStreamInfo(); connectRealtime(); if (!mustReplaceCredentials) password = ''; @@ -571,6 +583,92 @@ } } + async function loadOverlays() { + try { + const response = await fetch(`${serverAddress}/api/v1/overlays`, { headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('Overlays could not be loaded.'); + overlays = await response.json(); + if (!selectedOverlayId && overlays[0]) selectOverlay(overlays[0]); + overlayError = ''; + } catch (error) { + overlayError = error instanceof Error ? error.message : 'Overlays could not be loaded.'; + } + } + + function selectOverlay(overlay: (typeof overlays)[number]) { + selectedOverlayId = overlay.id; + overlayName = overlay.name; + overlaySlug = overlay.slug; + overlayHtml = overlay.draft_html; + overlayCss = overlay.draft_css; + overlayJavascript = overlay.draft_javascript; + overlayEnabled = overlay.enabled; + overlayDisabledDesign = overlay.disabled_design; + } + + async function createOverlay() { + selectedOverlayId = ''; + overlayName = 'New Overlay'; + overlaySlug = `overlay-${Date.now()}`; + overlayHtml = '
Hello ${username}
'; + overlayCss = '.alert { color: white; font: 32px sans-serif; }'; + overlayJavascript = ''; + overlayEnabled = true; + overlayDisabledDesign = 'transparent'; + } + + async function saveOverlay() { + overlayBusy = true; + overlayError = ''; + try { + const body = JSON.stringify({ name: overlayName, slug: overlaySlug, html: overlayHtml, css: overlayCss, javascript: overlayJavascript, disabled_design: overlayDisabledDesign }); + 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.'); + const saved = await response.json(); + await loadOverlays(); + selectOverlay({ ...saved, enabled: overlayEnabled, disabled_design: overlayDisabledDesign }); + } catch (error) { + overlayError = error instanceof Error ? error.message : 'The overlay draft could not be saved.'; + } finally { + overlayBusy = false; + } + } + + async function publishOverlay() { + if (!selectedOverlayId) return; + overlayBusy = true; + overlayError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/overlays/${selectedOverlayId}/publish`, { method: 'POST', headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('The overlay could not be published.'); + await loadOverlays(); + } catch (error) { + overlayError = error instanceof Error ? error.message : 'The overlay could not be published.'; + } finally { + overlayBusy = false; + } + } + + async function undoOverlay() { + if (!selectedOverlayId) return; + overlayBusy = true; + overlayError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/overlays/${selectedOverlayId}/undo`, { method: 'POST', headers: { authorization: `Bearer ${sessionToken}` } }); + if (!response.ok) throw new Error('There is no previous published overlay to restore.'); + await loadOverlays(); + } catch (error) { + overlayError = error instanceof Error ? error.message : 'The previous overlay could not be restored.'; + } finally { + overlayBusy = false; + } + } + + async function copyOverlayUrl() { + if (!overlaySlug) return; + await navigator.clipboard.writeText(`${serverAddress}/overlay/${overlaySlug}`).catch(() => (overlayError = 'Copy was unavailable; select the URL manually.')); + } + async function revokeOAuth(identity: 'streamer' | 'bot') { oauthBusy = true; oauthError = ''; @@ -685,6 +783,8 @@ {:else if activeView === 'Bot'}
BOT COMMANDS
PUBLIC COMMAND CATALOG
{#if botCommandError}
{botCommandError}
{:else if botCommands.length}
{#each botCommands as command}
!{command.name}{command.description}{#if command.tags.length}
{command.tags.join(' · ')}
{/if}
{command.permission_level.toUpperCase()}{#if command.cooldown_seconds}{command.cooldown_seconds}s / {command.cooldown_scope}{/if}
{/each}
{:else}
No enabled commandsCreate commands on the server to make them available here.
{/if}
SECRETS MANAGEMENT
Values are encrypted and cannot be read after saving.
{#if botSecrets.length}
{#each botSecrets as secret}
{secret.name}{secret.description || 'No description'}{#if secret.used_by.length}
USED BY: {secret.used_by.join(', ')}
{/if}
{/each}
{:else}
There are no secrets yet.
{/if}{#if secretError}
{secretError}
{/if}
+ {:else if activeView === 'Overlays'} +
OVERLAYS
{#if selectedOverlayId || overlayName}
LIVE PREVIEW
{#if overlayError}
{overlayError}
{/if}{:else}
Select or create an overlayDrafts autosave when explicitly saved; publishing controls the browser source.
{/if}
{:else}
STREAM PREVIEW⋮
{#if playerSrc}{:else}
STREAM OFFLINE
CONNECT TWITCH TO BEGIN
{/if}
diff --git a/src/styles.css b/src/styles.css index fe8f99c..fd578fc 100644 --- a/src/styles.css +++ b/src/styles.css @@ -576,6 +576,66 @@ label { .bot-secret-row span { display: block; } +.overlay-page { + min-height: 100%; +} +.overlay-toolbar, +.overlay-controls { + display: flex; + gap: 7px; +} +.overlay-layout { + display: grid; + grid-template-columns: 220px 1fr; + min-height: calc(100vh - 100px); +} +.overlay-list { + border-right: 1px solid var(--ui-divider); + padding: 10px; +} +.overlay-list-item { + display: grid; + width: 100%; + gap: 4px; + padding: 10px; + color: var(--ed-dim); + text-align: left; +} +.overlay-list-item.active, +.overlay-list-item:hover { + color: var(--ed-cyan); + background: var(--ui-surface-hi); +} +.overlay-list-item span { + font: 9px var(--font-data); +} +.overlay-editor { + display: grid; + align-content: start; + gap: 10px; + padding: 14px; +} +.overlay-fields, +.overlay-tabs { + display: grid; + gap: 7px; +} +.overlay-code { + min-height: 110px; + resize: vertical; + color: var(--ed-dim); + background: var(--ui-surface-low); + font: 12px/1.4 var(--font-data); +} +.overlay-preview-label { + margin-top: 4px; +} +.overlay-preview { + width: 100%; + min-height: 180px; + border: 1px solid var(--ui-divider); + background: #101217; +} .bot-secret-row span { margin-top: 3px; color: var(--ed-dim);