-
Notifications
You must be signed in to change notification settings - Fork 0
Stage existing UI and assets for C# rewrite #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| <title>SleepySource Chat Overlay</title> | ||
| <style> | ||
| html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;font-family:"Segoe UI",system-ui,sans-serif}*{box-sizing:border-box} | ||
| #canvas{position:relative;width:100vw;height:100vh;overflow:hidden;background:transparent} | ||
| #chatBox{position:absolute;display:flex;overflow:hidden} | ||
| #messages{width:100%;display:flex;overflow:hidden} | ||
| .message{display:flex;align-items:center;min-width:0;word-break:break-word;overflow-wrap:anywhere;animation-fill-mode:both;border:none;box-shadow:0 5px 18px rgba(0,0,0,.10);backdrop-filter:blur(2px)} | ||
| .message.compact{border-color:transparent;box-shadow:none;backdrop-filter:none} | ||
| .avatar{flex:none;border-radius:50%;object-fit:cover}.content{min-width:0;flex:1}.line{display:block;min-width:0;line-height:1.34}.badge-group{display:inline-flex;align-items:center;gap:.18em;margin-right:.34em;vertical-align:-.15em}.badge-wrap{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.badge-img{display:block;object-fit:contain;max-width:2.35em}.badge-fallback{display:none;align-items:center;justify-content:center;border-radius:4px;padding:1px 4px;font-size:.54em;line-height:1.25;font-weight:850;background:rgba(255,255,255,.15);color:#fff;text-transform:uppercase;letter-spacing:.02em}.name{font-weight:800;white-space:nowrap}.separator{font-weight:700;margin-right:.28em}.timestamp{opacity:.58;font-size:.68em;margin-right:.38em}.text{white-space:pre-wrap}.emote{display:inline-block;vertical-align:middle;object-fit:contain;margin:0 .08em;transform:translateY(-.04em)} | ||
| @keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}@keyframes slideLeft{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}@keyframes slideRight{from{opacity:0;transform:translateX(-16px)}to{opacity:1;transform:none}}@keyframes popIn{from{opacity:0;transform:scale(.72)}to{opacity:1;transform:scale(1)}}@keyframes zoomIn{from{opacity:0;transform:scale(.88)}to{opacity:1;transform:scale(1)}}@keyframes bounceIn{0%{opacity:0;transform:translateY(20px) scale(.94)}72%{opacity:1;transform:translateY(-4px) scale(1.02)}100%{transform:none}}@keyframes blurIn{from{opacity:0;filter:blur(10px)}to{opacity:1;filter:blur(0)}}@keyframes flipIn{from{opacity:0;transform:perspective(600px) rotateX(-70deg)}to{opacity:1;transform:none}} | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="canvas"><div id="chatBox"><div id="messages"></div></div></div> | ||
| <script> | ||
| let state=null,lastUpdated=0,last7Key='',last7Attempt=0,emotes=new Map(),subBadges=[],badgeChannel='',lastRenderSettings=''; | ||
| const esc=s=>String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); | ||
| function cssRGBA(hex,alpha){const m=/^#([0-9a-f]{6})$/i.exec(hex||'');if(!m)return `rgba(7,17,31,${alpha})`;const n=parseInt(m[1],16);return `rgba(${n>>16},${(n>>8)&255},${n&255},${alpha})`} | ||
| const chatLoadedFonts=new Map();async function loadCustomFonts(){try{const r=await fetch('/api/state',{cache:'no-store'});if(!r.ok)return;const app=await r.json();for(const font of app.fonts||[]){if(!font?.family||!font?.url||chatLoadedFonts.get(font.family)===font.url)continue;try{const face=new FontFace(font.family,'url("'+font.url.replaceAll('"','%22')+'")');await face.load();document.fonts.add(face);chatLoadedFonts.set(font.family,font.url)}catch(_){}}}catch(_){}} | ||
| function collect7TVEmotes(payload,out=[]){if(!payload||typeof payload!=='object')return out;if(Array.isArray(payload.emotes))out.push(...payload.emotes);if(payload.emote_set&&Array.isArray(payload.emote_set.emotes))out.push(...payload.emote_set.emotes);for(const [k,v] of Object.entries(payload)){if(k==='emotes'||k==='emote_set')continue;if(v&&typeof v==='object')collect7TVEmotes(v,out)}return out} | ||
| function emoteURL(e){const id=e?.id||e?.data?.id;return id?`/api/chat/7tv-image?id=${encodeURIComponent(id)}`:''} | ||
| async function load7TV(s){if(!s.seventv_enabled){emotes.clear();last7Key='';return}const key=[s.kick_channel,s.seventv_emote_set_id,state?.broadcaster_user_id||'',state?.auth_ready?'auth':''].join('|');if(!key.replaceAll('|',''))return;const now=Date.now();if(key===last7Key&&(emotes.size||now-last7Attempt<10000))return;last7Key=key;last7Attempt=now;try{const q=new URLSearchParams();if(s.kick_channel)q.set('kick_channel',s.kick_channel);if(s.seventv_emote_set_id)q.set('emote_set_id',s.seventv_emote_set_id);const r=await fetch('/api/chat/7tv?'+q,{cache:'no-store'});if(!r.ok)return;const data=await r.json();const next=new Map();const globals=collect7TVEmotes(data.global||{}),channel=collect7TVEmotes(data.channel||data);for(const e of [...globals,...channel]){const name=e?.name||e?.data?.name,id=e?.id||e?.data?.id,u=emoteURL(e);if(name&&id&&u)next.set(String(name),{id,url:u})}emotes=next;render(true)}catch(_){}} | ||
| async function loadBadgeCatalog(s){const channel=(s.kick_channel||state?.connected_channel||'').trim();if(!channel){subBadges=[];badgeChannel='';return}if(channel.toLowerCase()===badgeChannel.toLowerCase()&&subBadges.length)return;try{const r=await fetch('/api/chat/badges?channel='+encodeURIComponent(channel),{cache:'no-store'});if(!r.ok)return;const data=await r.json();subBadges=(data.subscriber_badges||[]).filter(b=>Number(b.months)>0&&b.url).sort((a,b)=>Number(a.months)-Number(b.months));badgeChannel=channel}catch(_){}} | ||
| function normalizeBadgeType(v){v=String(v||'').trim().toLowerCase().replace(/[. -]+/g,'_');const m={owner:'broadcaster',channel_owner:'broadcaster',channel_host:'broadcaster',mod:'moderator',sub:'subscriber',subgifter:'sub_gifter',gifter:'sub_gifter',kick_staff:'staff'};return m[v]||v} | ||
| function legacyBadgeType(v){const x=String(v||'').toLowerCase();if(x.includes('broadcaster')||x.includes('channel host')||x==='bc')return'broadcaster';if(x.includes('moderator')||x==='mod')return'moderator';if(x==='vip'||x.includes('vip'))return'vip';if(x==='og'||x.includes(' og'))return'og';if(x.includes('founder'))return'founder';if(x.includes('sub gifter')||x.includes('subgifter'))return'sub_gifter';if(x.includes('subscriber')||x==='sub')return'subscriber';if(x.includes('verified'))return'verified';if(x.includes('staff'))return'staff';return normalizeBadgeType(v)} | ||
| function badgeLabel(type){return({broadcaster:'BC',moderator:'MOD',vip:'VIP',og:'OG',founder:'FND',subscriber:'SUB',sub_gifter:'SG',verified:'VER',staff:'STAFF',sidekick:'SIDE'})[type]||String(type||'').slice(0,5).toUpperCase()} | ||
| function messageBadges(m){const src=Array.isArray(m.badge_details)&&m.badge_details.length?m.badge_details:(m.badges||[]).map(x=>({text:String(x),type:legacyBadgeType(x),count:0}));const seen=new Set();return src.map(b=>({text:b.text||b.type||'',type:normalizeBadgeType(b.type||legacyBadgeType(b.text)),count:Number(b.count||0)})).filter(b=>b.type&&!seen.has(b.type)&&seen.add(b.type)).sort((a,b)=>{const order={broadcaster:1,moderator:2,vip:3,og:4,founder:5,verified:6,staff:7,subscriber:8,sub_gifter:9,sidekick:10};return(order[a.type]||99)-(order[b.type]||99)})} | ||
| function subscriberBadgeURL(months){months=Number(months||0);let chosen=null;for(const b of subBadges){if(Number(b.months)<=Math.max(1,months))chosen=b;else break}return chosen?.url?`/api/chat/badge-image?url=${encodeURIComponent(chosen.url)}`:''} | ||
| function badgeImageURL(b){if(b.type==='subscriber'){const u=subscriberBadgeURL(b.count);if(u)return u}return `/api/chat/badge-image?role=${encodeURIComponent(b.type)}&count=${encodeURIComponent(b.count||0)}`} | ||
| function renderBadges(m,size){return messageBadges(m).map(b=>`<span class="badge-wrap" title="${esc(b.text||b.type)}"><img class="badge-img" alt="${esc(b.text||b.type)}" src="${esc(badgeImageURL(b))}" style="height:${size}px;width:${size}px"><span class="badge-fallback">${esc(badgeLabel(b.type))}</span></span>`).join('')} | ||
| function render7TVToken(token,size){const direct=emotes.get(token);if(direct)return `<img class="emote seventv-emote" alt="${esc(token)}" title="${esc(token)}" src="${esc(direct.url)}" style="width:${size}px;height:${size}px">`;const m=/^([^\p{L}\p{N}_]*)([\s\S]*?)([^\p{L}\p{N}_]*)$/u.exec(token);if(m&&m[2]){const e=emotes.get(m[2]);if(e)return esc(m[1])+`<img class="emote seventv-emote" alt="${esc(m[2])}" title="${esc(m[2])}" src="${esc(e.url)}" style="width:${size}px;height:${size}px">`+esc(m[3])}return esc(token)} | ||
| function renderPlainText(text,size){return String(text??'').split(/(\s+)/).map(p=>/^[\s]+$/.test(p)?p:render7TVToken(p,size)).join('')} | ||
| function renderText(text,size){const raw=String(text??'');const re=/\[emote:(\d{1,20}):([^\]\r\n]{1,100})\]/g;let out='',last=0,m;while((m=re.exec(raw))){out+=renderPlainText(raw.slice(last,m.index),size);const id=m[1],name=m[2];out+=`<img class="emote kick-emote" alt="${esc(name)}" title="${esc(name)}" src="/api/chat/kick-emote?id=${encodeURIComponent(id)}" style="width:${size}px;height:${size}px">`;last=re.lastIndex}out+=renderPlainText(raw.slice(last),size);return out} | ||
| function installBadgeFallbacks(root){root.querySelectorAll('.badge-img').forEach(img=>img.addEventListener('error',()=>{img.style.display='none';const fb=img.nextElementSibling;if(fb)fb.style.display='inline-flex'},{once:true}))} | ||
| function animationName(v){return({fade:'fadeIn','slide-up':'slideUp','slide-left':'slideLeft','slide-right':'slideRight',pop:'popIn',zoom:'zoomIn',bounce:'bounceIn',blur:'blurIn',flip:'flipIn',none:'none'})[v]||'slideUp'} | ||
| function animationEasing(v){return v==='snappy'?'cubic-bezier(.2,.9,.2,1)':v==='spring'?'cubic-bezier(.18,1.35,.35,1)':v==='smooth'?'cubic-bezier(.2,.8,.2,1)':v||'ease'} | ||
| function messageMarkup(m,s,animate){const anim=animate?animationName(s.animation):'none',badges=s.show_badges?`<span class="badge-group">${renderBadges(m,s.badge_size||20)}</span>`:'',avatar=s.show_avatars&&m.avatar_url?`<img class="avatar" src="/api/chat/avatar?url=${encodeURIComponent(m.avatar_url)}" style="width:${Math.max(24,s.font_size+6)}px;height:${Math.max(24,s.font_size+6)}px;margin-right:${Math.max(6,Math.round(s.message_gap*.7))}px">`:'',ts=s.show_timestamps?`<span class="timestamp">${new Date(m.created_at||Date.now()).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</span>`:'',nameColor=s.use_kick_username_color!==false?(m.color||s.username_color):s.username_color,pad=s.compact_mode?'3px 5px':'7px 9px',shadow=s.text_shadow?'0 1px 2px rgba(0,0,0,.78)':'none',rowBorder=(s.message_border_width||0)>0?`${s.message_border_width}px solid ${s.message_border_color||'#2F78B7'}`:'none',animation=anim==='none'?'none':`${anim} ${s.animation_ms}ms ${animationEasing(s.animation_easing)} both`;return `<div class="message ${s.compact_mode?'compact':''}" data-id="${esc(m.id)}" style="font-size:${s.font_size}px;color:${s.message_color};padding:${pad};border-radius:${s.message_radius??9}px;border:${rowBorder};background:${cssRGBA(s.message_background_color||'#07111F',s.message_background_transparent?0:(s.message_background_opacity||0)/100)};text-shadow:${shadow};animation:${animation}">${avatar}<div class="content"><div class="line">${ts}${badges}<span class="name" style="font-size:${s.username_size}px;font-weight:${s.username_weight||800};color:${esc(nameColor)}">${esc(m.username)}</span><span class="separator">:</span><span class="text">${renderText(m.text,s.emote_size)}</span></div></div></div>`} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not interpolate
Build the message DOM with 🧰 Tools🪛 ast-grep (0.45.1)[warning] 42-42: Avoid assigning untrusted data to innerHTML/outerHTML or document.write (inner-outer-html) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| function render(force=false){if(!state)return;const s=state.settings,box=document.getElementById('chatBox'),list=document.getElementById('messages');document.body.style.fontFamily=s.font_family||'Segoe UI';box.style.left=s.box_x+'px';box.style.top=s.box_y+'px';box.style.width=s.box_width+'px';box.style.height=s.box_height+'px';box.style.padding=s.padding+'px';box.style.borderRadius=s.radius+'px';box.style.border='none';box.style.background=cssRGBA(s.background_color,s.box_background_transparent?0:(s.background_opacity||0)/100);box.style.backdropFilter=(s.box_blur||0)>0?`blur(${s.box_blur}px)`:'none';list.style.flexDirection=s.direction==='top-down'?'column':'column-reverse';list.style.gap=s.message_gap+'px';const all=[...(state.messages||[])],msgs=(s.direction==='top-down'?all.slice(-s.max_messages):all.slice(-s.max_messages).reverse()),settingsSig=JSON.stringify(s),settingsChanged=settingsSig!==lastRenderSettings,existing=new Map();if(!force&&!settingsChanged){for(const node of list.children){if(node.dataset?.id)existing.set(node.dataset.id,node)}}const nodes=[];for(const m of msgs){const id=String(m.id??'');let node=existing.get(id);if(node){existing.delete(id);nodes.push(node);continue}const template=document.createElement('template');template.innerHTML=messageMarkup(m,s,!force&&!settingsChanged).trim();node=template.content.firstElementChild;if(node)nodes.push(node)}list.replaceChildren(...nodes);lastRenderSettings=settingsSig;installBadgeFallbacks(list)} | ||
| async function poll(){try{const r=await fetch('/api/chat/state',{cache:'no-store'});if(r.ok){const next=await r.json();state=next;if(next.settings?.font_family&&!chatLoadedFonts.has(next.settings.font_family))await loadCustomFonts();await Promise.all([load7TV(next.settings),loadBadgeCatalog(next.settings)]);if(next.updated_at!==lastUpdated){lastUpdated=next.updated_at;render()}}}catch(_){}setTimeout(poll,220)} | ||
| loadCustomFonts();setInterval(loadCustomFonts,30000);setInterval(()=>{if(state?.settings?.seventv_enabled){last7Key='';load7TV(state.settings).then(()=>render(true))}if(state?.settings){badgeChannel='';loadBadgeCatalog(state.settings).then(()=>render(true))}},60000);poll(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add timeouts to overlay resource requests.
If
/api/chat/state,/api/chat/7tv, or/api/chat/badgesstalls,poll()waits indefinitely. Line 44 schedules the next poll only after all awaited requests complete. The chat overlay then stops receiving updates until the page reloads.Use an
AbortControllertimeout for each fetch. Treat an aborted request as a failed poll so the existing retry schedule continues.Proposed timeout helper
Also applies to: 44-45
🤖 Prompt for AI Agents