diff --git a/src/App.svelte b/src/App.svelte index 7156117..858769d 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -16,6 +16,14 @@ let realtimeState = 'DISCONNECTED'; let realtimeSocket: WebSocket | undefined; let activityEvents: Array<{ event_id: string; event_type: string; occurred_at: string }> = []; + let showStreamEditor = false; + let streamBusy = false; + let streamError = ''; + let streamTitle = ''; + let streamGameId = ''; + let streamLanguage = 'en'; + let streamTags = ''; + let streamClassificationLabels = ''; let oauthLink = ''; let oauthIdentity: 'streamer' | 'bot' = 'streamer'; let oauthBusy = false; @@ -64,6 +72,7 @@ mustReplaceCredentials = result.must_replace_credentials; connectionState = 'CONNECTED'; await loadActivity(); + await loadStreamInfo(); connectRealtime(); if (!mustReplaceCredentials) password = ''; if (mustReplaceCredentials) newUsername = username; @@ -156,6 +165,47 @@ } } + async function loadStreamInfo() { + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/stream-info`, { + headers: { authorization: `Bearer ${sessionToken}` }, + }); + if (!response.ok) return; + const info: { title: string; game_id: string; language: string; tags: string[]; content_classification_labels: string[] } = await response.json(); + streamTitle = info.title; + streamGameId = info.game_id; + streamLanguage = info.language; + streamTags = info.tags.join(', '); + streamClassificationLabels = info.content_classification_labels.join(', '); + } catch { + // A missing Twitch streamer identity is shown when the editor is opened. + } + } + + async function saveStreamInfo() { + streamBusy = true; + streamError = ''; + try { + const response = await fetch(`${serverAddress}/api/v1/twitch/stream-info`, { + method: 'PATCH', + headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` }, + body: JSON.stringify({ + title: streamTitle, + game_id: streamGameId, + language: streamLanguage, + tags: streamTags.split(',').map((tag) => tag.trim()).filter(Boolean), + content_classification_labels: streamClassificationLabels.split(',').map((label) => label.trim()).filter(Boolean), + }), + }); + if (!response.ok) throw new Error('The server could not update stream information.'); + showStreamEditor = false; + } catch (error) { + streamError = error instanceof Error ? error.message : 'Stream information update failed.'; + } finally { + streamBusy = false; + } + } + async function createOAuthLink(identity: 'streamer' | 'bot') { oauthBusy = true; oauthError = ''; @@ -265,7 +315,7 @@ STREAM PREVIEW⋮STREAM OFFLINECONNECT TWITCH TO BEGIN ACTIVITY FEED⋮ALL ACTIVITYMODERATION{#if activityEvents.length}{#each activityEvents as event}{event.event_type}{new Date(event.occurred_at).toLocaleTimeString()}{/each}{:else}◌Nothing here yetFollows, subscriptions, raids, and server events will appear here.{/if} MY CHAT⋮⌁Chat is waitingConnect a streamer or bot account to begin.SEND - QUICK ACTIONS⋮EDIT STREAM INFOMODERATIONBOT COMMANDSOVERLAYS + QUICK ACTIONS⋮ (showStreamEditor = !showStreamEditor)}>EDIT STREAM INFOMODERATIONBOT COMMANDSOVERLAYS{#if showStreamEditor}TITLECATEGORY / GAME IDLANGUAGETAGSCLASSIFICATION LABELSSAVE STREAM INFO{#if streamError}{streamError}{/if}{/if} {/if} diff --git a/src/styles.css b/src/styles.css index 554643c..0984dc4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -468,6 +468,15 @@ label { color: var(--ed-dim); font: 10px var(--font-data); } +.stream-editor { + display: grid; + gap: 6px; + padding: 10px; + border-top: 1px solid var(--ui-divider); +} +.stream-editor label { + margin-top: 4px; +} .quick-action:hover, .quick-action.accent { border-color: var(--ui-line-focus);