Skip to content

Instantly share code, notes, and snippets.

@HoughIO
Created March 11, 2026 16:45
Show Gist options
  • Select an option

  • Save HoughIO/821b19a2ead7e812a3781df03dedd833 to your computer and use it in GitHub Desktop.

Select an option

Save HoughIO/821b19a2ead7e812a3781df03dedd833 to your computer and use it in GitHub Desktop.
Kevin & Todd Cam - Themes + Name Fix
#!/bin/bash
# Kevin & Todd Cam - Themes + Name Fix Update
# Run on Pi: bash upgrade-themes-v2.sh
echo "🐱 Upgrading with themes + name fix..."
# Update the index.html
sudo tee /opt/kittycam/web/index.html > /dev/null << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#0a0a0a" id="theme-color-meta">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Asshole Cam</title>
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="/static/icons/icon-192.png">
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
/* Todd Theme (Dark - Default) */
:root,[data-theme="todd"]{
--bg:#0a0a0a;--surface:rgba(255,255,255,0.03);--surface-hover:rgba(255,255,255,0.06);
--border:rgba(255,255,255,0.08);--text:#fff;--text-dim:rgba(255,255,255,0.5);
--accent:#ff6b6b;--accent-glow:rgba(255,107,107,0.3);--theme-color:#0a0a0a
}
/* Kevin Theme (Orange & Cream) */
[data-theme="kevin"]{
--bg:#fff8f0;--surface:rgba(255,140,50,0.08);--surface-hover:rgba(255,140,50,0.15);
--border:rgba(255,120,50,0.2);--text:#2a1810;--text-dim:rgba(42,24,16,0.6);
--accent:#ff8c42;--accent-glow:rgba(255,140,66,0.4);--theme-color:#fff8f0
}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;min-height:100vh;min-height:100dvh}
.tabs{position:fixed;bottom:0;left:0;right:0;display:flex;background:var(--bg);border-top:1px solid var(--border);padding-bottom:env(safe-area-inset-bottom);z-index:100}
.tab{flex:1;padding:12px;text-align:center;color:var(--text-dim);font-size:.75rem;cursor:pointer;border:none;background:none}
.tab.active{color:var(--accent)}
.tab-icon{font-size:1.4rem;display:block;margin-bottom:4px}
.page{display:none;min-height:100vh;padding-bottom:80px}
.page.active{display:block}
.stream-wrapper{height:calc(100vh - 80px);display:flex;align-items:center;justify-content:center;padding:env(safe-area-inset-top) 0 0 0;background:#000}
#stream{width:100%;height:100%;object-fit:contain;opacity:0;transition:opacity .3s}
#stream.loaded{opacity:1}
/* Header - Fixed for long names */
.header{position:fixed;top:0;left:0;right:0;padding:calc(env(safe-area-inset-top) + 8px) 12px 8px;display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;background:linear-gradient(to bottom,rgba(0,0,0,.85) 0%,rgba(0,0,0,.6) 70%,transparent 100%);z-index:10}
.title{font-size:.85rem;font-weight:500;display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0}
.title-text{overflow-wrap:break-word;word-break:break-word}
.live-dot{width:8px;height:8px;background:var(--accent);border-radius:50%;animation:pulse 2s infinite;flex-shrink:0}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.quality-pill{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:3px;backdrop-filter:blur(10px);flex-shrink:0}
.quality-btn{padding:5px 10px;border:none;background:transparent;color:var(--text-dim);font-size:.65rem;font-weight:500;text-transform:uppercase;border-radius:14px;cursor:pointer}
.quality-btn.active{background:var(--text);color:var(--bg)}
/* Rarity styles */
.rarity-badge{font-size:.55rem;padding:2px 5px;border-radius:6px;font-weight:bold;text-transform:uppercase;flex-shrink:0;white-space:nowrap}
.rarity-rare{background:#8847ff;color:#fff}
.rarity-ultra{background:#e4ae39;color:#000;animation:goldPulse 2s ease-in-out infinite}
@keyframes goldPulse{0%,100%{box-shadow:0 0 5px #e4ae39,0 0 10px #e4ae39}50%{box-shadow:0 0 15px #e4ae39,0 0 25px #e4ae39}}
.title-normal{color:var(--text)}
.title-rare{color:#8847ff;text-shadow:0 0 10px rgba(136,71,255,0.5)}
.title-ultra{background:linear-gradient(90deg,#e4ae39,#f7d774,#e4ae39);background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;animation:goldShimmer 2s linear infinite}
@keyframes goldShimmer{to{background-position:200% center}}
.page-header{padding:calc(env(safe-area-inset-top) + 16px) 16px 16px;font-size:1.5rem;font-weight:600}
.clips-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:0 8px}
.clip-card{aspect-ratio:16/9;background:var(--surface);border-radius:12px;overflow:hidden;position:relative;cursor:pointer}
.clip-card video{width:100%;height:100%;object-fit:cover}
.clip-time{position:absolute;bottom:8px;left:8px;background:rgba(0,0,0,.7);padding:4px 8px;border-radius:6px;font-size:.7rem;color:#fff}
.events-list{padding:0 16px}
.event-item{display:flex;align-items:center;gap:12px;padding:16px 0;border-bottom:1px solid var(--border)}
.event-icon{width:40px;height:40px;background:var(--surface);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2rem}
.event-info{flex:1}
.event-message{font-size:.9rem}
.event-time{font-size:.75rem;color:var(--text-dim);margin-top:2px}
.hidden{display:none!important}
.loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000;flex-direction:column;gap:16px}
.spinner{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--text);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.retry-btn{padding:12px 24px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer}
.empty-state{text-align:center;padding:60px 20px;color:var(--text-dim)}
.empty-icon{font-size:3rem;margin-bottom:12px}
/* Settings Panel */
.settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);z-index:200;opacity:0;visibility:hidden;transition:all .3s}
.settings-overlay.show{opacity:1;visibility:visible}
.settings-panel{position:fixed;bottom:0;left:0;right:0;background:var(--bg);border-top:1px solid var(--border);border-radius:20px 20px 0 0;padding:24px;padding-bottom:calc(env(safe-area-inset-bottom) + 24px);z-index:201;transform:translateY(100%);transition:transform .3s cubic-bezier(0.4,0,0.2,1)}
.settings-overlay.show .settings-panel{transform:translateY(0)}
.settings-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.settings-title{font-size:1.1rem;font-weight:600}
.settings-close{background:var(--surface);border:1px solid var(--border);color:var(--text);width:32px;height:32px;border-radius:50%;cursor:pointer;font-size:1rem}
.settings-label{font-size:.8rem;color:var(--text-dim);margin-bottom:10px;text-transform:uppercase;letter-spacing:.05em}
.theme-options{display:flex;gap:10px}
.theme-btn{flex:1;padding:14px 10px;border-radius:12px;border:2px solid var(--border);background:var(--surface);cursor:pointer;transition:all .2s;text-align:center}
.theme-btn:hover{border-color:var(--accent)}
.theme-btn.active{border-color:var(--accent);background:var(--accent-glow)}
.theme-icon{font-size:1.8rem;margin-bottom:6px}
.theme-name{font-size:.85rem;font-weight:500;color:var(--text)}
.theme-desc{font-size:.7rem;color:var(--text-dim);margin-top:2px}
</style>
</head>
<body>
<nav class="tabs">
<button class="tab active" data-page="live"><span class="tab-icon">📺</span>Live</button>
<button class="tab" data-page="clips"><span class="tab-icon">🎬</span>Clips</button>
<button class="tab" data-page="activity"><span class="tab-icon">🔔</span>Activity</button>
<button class="tab" id="tab-settings"><span class="tab-icon">⚙️</span>Settings</button>
</nav>
<div id="page-live" class="page active">
<div class="header">
<div class="title">
<div class="live-dot"></div>
<span class="title-text" id="cat-names">Loading...</span>
<span class="rarity-badge hidden" id="rarity-badge"></span>
</div>
<div class="quality-pill">
<button class="quality-btn active" id="btn-hd">HD</button>
<button class="quality-btn" id="btn-save">Lite</button>
</div>
</div>
<div class="stream-wrapper">
<div class="loading" id="loading">
<div class="spinner"></div>
<button class="retry-btn hidden" id="retry-btn">Retry</button>
</div>
<img id="stream" alt="">
</div>
</div>
<div id="page-clips" class="page">
<div class="page-header">Motion Clips</div>
<div class="clips-grid" id="clips-grid"></div>
<div class="empty-state hidden" id="clips-empty">
<div class="empty-icon">🎬</div>
<p>No clips yet</p>
</div>
</div>
<div id="page-activity" class="page">
<div class="page-header">Activity</div>
<div class="events-list" id="events-list"></div>
<div class="empty-state hidden" id="activity-empty">
<div class="empty-icon">🔔</div>
<p>No activity yet</p>
</div>
</div>
<!-- Settings Panel -->
<div class="settings-overlay" id="settings-overlay">
<div class="settings-panel">
<div class="settings-header">
<span class="settings-title">Settings</span>
<button class="settings-close" id="settings-close">✕</button>
</div>
<div class="settings-label">Theme</div>
<div class="theme-options">
<button class="theme-btn" data-theme="todd" id="theme-todd">
<div class="theme-icon">🐱</div>
<div class="theme-name">Todd</div>
<div class="theme-desc">Dark mode</div>
</button>
<button class="theme-btn" data-theme="kevin" id="theme-kevin">
<div class="theme-icon">🧡</div>
<div class="theme-name">Kevin</div>
<div class="theme-desc">Orange & cream</div>
</button>
</div>
</div>
</div>
<script>
// Random name generator with rarities
const KEVIN={normal:["Kevin","Kev","Kevers","Kevster","Keyvers","Saint Kevin","Brother Kevin","The Orange Kev","Big Kev","Little Kev","Captain Kevin","Professor Kev","Doctor Kevin","Kevinator","Kevvy","Kevvy Boy","Kevmeister","Kevlord","Kevski","Kevzilla","Kevinsaurus","Kevin McFluff","Snack Bandit Kev","Sleepy Kev","Biscuit Maker Kev","Chaos Kev","Gremlin Kev","The Kevening","Criminal Kev","Outlaw Kev","Kev the Menace","Kev the Destroyer","Kev the Great","Kev the Problem","Kev the Absolute Menace","Kevjamin","Kevothy","Kevbert","Kevnard","Kevathan","Kevtopher","Kevcules","Kevdini","Kevtron","Kevlopotamus","Kevquatch","Kevpocalypse","Kevageddon","Supreme Overkev","Kevdalf the Orange","Kevuman the White","Kevrond Peredhel","Kevagorn Son of Arathorn","Kevomir of Gondor","Kevgolas Greenleaf","Kevimli Son of Gloin","Kevwise the Brave","Kevrodo of the Shire","Kevrond Half-Elven","Kevion of Rivendell","Kevbrandir","Kevendur","Don Kevin Soprano","Kev \"The Claw\" Moltisanti","Kev \"Two-Toes\" Cifaretto","Kev \"The Collector\" Aprile","Kev \"The Pawshaker\" Spatafore","Kev \"The Quiet\" Gualtieri","Kev \"Nine Lives\" Bacala","Kev \"The Fixer\" Dante","Kev \"The Ledger\" Parisi","Kev \"The Informer\" Bonpensiero","Kev \"The Charmer\" La Cerva","Kev \"The Treasurer\" Barese","Kev \"The Neighbor\" Cusamano","Kev \"The Button Man\" Palmice","Kev \"The Collector\" Giunta","Kev \"The Boss\" Lupertazzi","Kev \"The Animal\" Blundetto","Kev \"The Runaway\" Petrulio","Kev \"The Wire\" Altieri","Kev \"The Associate\" DeAngelis","Kev \"The Contractor\" Scatino","Kev \"The Muscle\" Pontecorvo","Kev \"The Bookkeeper\" Melfi","Kev \"The Gambler\" Tannenbaum","Kev \"The Driver\" Walnuts","Kev \"The Couch Goblin\"","Kev \"The Treat Extortionist\"","Kev \"The Orange Menace\""],rare:["Asshole 1","Asshole Kev","Kev the Asshole","Dumbass Kevin","Bastard Kev","Kev the Little Bastard","Little Shit Kev","Kev the Shithead","Kev the Dumbass"],ultraRare:["Kevin the Cunt"]};
const TODD={normal:["Todd","Todders","Toddles","Todd-diddly-dumkins","Todd the Malevolent","Big Todd","Little Todd","The Grey Todd","Sleepy Todd","Snack Bandit Todd","Gremlin Todd","Chaos Todd","Biscuit Maker Todd","Criminal Todd","Outlaw Todd","Todd the Menace","Todd the Destroyer","Todd the Problem","Todd the Absolute Menace","The Toddening","Toddjamin","Toddothy","Toddbert","Toddnard","Toddathan","Toddcules","Toddtron","Toddquatch","Toddopotamus","Toddageddon","Todd McFluff","Todd the Couch Goblin","Todd the Treat Extortionist","Todd the Blanket Tyrant","Todd the Window Inspector","Todd the Carpet General","Todd the Sock Thief","Todd the Laundry Guardian","Todd the Naplord","Todd the Radiator King","Todd the Grey Wanderer","Todd the White Councilor","Todd of the Shire","Todd the Ring Sneak","Todd of Gondor","Todd the Mirkwood Stalker","Todd of Erebor","Todd the Rivendell Guest","Todd the Lothlórien Lurker","Todd the Lonely Mountain Bandit","Todd the Second Breakfast Inspector","Todd the Palantír Watcher","Todd the Grey Havens Drifter","Todd the Shire Troublemaker","Todd the Mordor Scout","Todd the Uruk Botherer","Todd the Fellowship Stowaway","Todd the Entish Observer","Todd the Bree Tavern Lurker","Todd the Rohan Stable Raider","Todd \"Velvet Paws\" Soprano","Todd \"Grey Shadow\" Moltisanti","Todd \"Loose Whiskers\" Cifaretto","Todd \"The Nap Boss\" Aprile","Todd \"Soft Claws\" Spatafore","Todd \"The Carpet King\" Gualtieri","Todd \"Whisker Face\" Bacala","Todd \"The Radiator Don\" Dante","Todd \"Side-Eye\" Parisi","Todd \"The Couch Fixer\" Bonpensiero","Todd \"Smooth Paws\" La Cerva","Todd \"Quiet Feet\" Barese","Todd \"Backyard Boss\" Cusamano","Todd \"Whisker Knife\" Palmice","Todd \"The Treat Collector\" Giunta","Todd \"The Grey Don\" Lupertazzi","Todd \"Nine Nap\" Blundetto","Todd \"Window Watcher\" Petrulio","Todd \"Silent Paw\" Altieri","Todd \"Porch Boss\" DeAngelis","Todd \"The Ledger Paw\" Scatino","Todd \"Heavy Step\" Pontecorvo","Todd \"The Doctor's Friend\" Melfi","Todd \"Card Shark\" Tannenbaum","Todd \"Walnut Paws\" Gualtieri","Todd the Grey Menace","Todd the Couch Inspector","Todd the Treat Bandit","Todd the Radiator Goblin","Todd the Blanket Invader"],rare:["Tardles","Asshole Todd","Todd the Asshole","Dumbass Todd","Bastard Todd","Todd the Little Bastard","Little Shit Todd","Todd the Shithead","Todd the Dumbass"],ultraRare:["Todd the Super-Cunt"]};
function pickName(obj){const w=[],n=[];obj.normal.forEach(x=>{n.push({name:x,rarity:'normal'});w.push(1)});obj.rare.forEach(x=>{n.push({name:x,rarity:'rare'});w.push(0.627)});obj.ultraRare.forEach(x=>{n.push({name:x,rarity:'ultra'});w.push(0.47)});const t=w.reduce((a,b)=>a+b,0);let r=Math.random()*t;for(let i=0;i<n.length;i++){r-=w[i];if(r<=0)return n[i]}return n[0]}
function setRandomNames(){const k=pickName(KEVIN),t=pickName(TODD);const titleEl=document.getElementById('cat-names');titleEl.textContent=k.name+' & '+t.name;const badge=document.getElementById('rarity-badge');titleEl.className='title-text';if(k.rarity==='ultra'||t.rarity==='ultra'){badge.textContent='★ CONTRABAND';badge.className='rarity-badge rarity-ultra';badge.classList.remove('hidden');titleEl.classList.add('title-ultra')}else if(k.rarity==='rare'||t.rarity==='rare'){badge.textContent='RESTRICTED';badge.className='rarity-badge rarity-rare';badge.classList.remove('hidden');titleEl.classList.add('title-rare')}else{badge.classList.add('hidden');titleEl.classList.add('title-normal')}}
// Theme handling
const THEME_KEY='kittycam_theme';
function applyTheme(theme){
document.documentElement.setAttribute('data-theme',theme);
document.getElementById('theme-color-meta').content=theme==='kevin'?'#fff8f0':'#0a0a0a';
localStorage.setItem(THEME_KEY,theme);
document.getElementById('theme-todd').classList.toggle('active',theme==='todd');
document.getElementById('theme-kevin').classList.toggle('active',theme==='kevin');
}
function loadTheme(){applyTheme(localStorage.getItem(THEME_KEY)||'todd')}
function openSettings(){document.getElementById('settings-overlay').classList.add('show')}
function closeSettings(){document.getElementById('settings-overlay').classList.remove('show')}
const STORAGE_KEY='kittycam_auth',MODE_KEY='kittycam_mode';
let isHD=true,snapshotInterval=null,streamRetries=0;
function getAuthKey(){const p=new URLSearchParams(location.search),k=p.get('key');if(k){localStorage.setItem(STORAGE_KEY,k);history.replaceState({},'',location.pathname);return k}return localStorage.getItem(STORAGE_KEY)}
async function validateAuth(k){try{return(await fetch('/auth?key='+k)).ok}catch{return!!localStorage.getItem(STORAGE_KEY)}}
function toast(m){console.log(m)}
function startStream(){const s=document.getElementById('stream'),l=document.getElementById('loading'),r=document.getElementById('retry-btn');s.classList.remove('loaded');l.classList.remove('hidden');r.classList.add('hidden');if(snapshotInterval){clearInterval(snapshotInterval);snapshotInterval=null}if(isHD){s.src='/stream?action=stream&t='+Date.now()}else{const u=()=>{s.src='/stream?action=snapshot&t='+Date.now()};u();snapshotInterval=setInterval(u,1500)}}
function setMode(m){isHD=m==='hd';localStorage.setItem(MODE_KEY,m);document.getElementById('btn-hd').classList.toggle('active',isHD);document.getElementById('btn-save').classList.toggle('active',!isHD);startStream()}
async function loadClips(){const g=document.getElementById('clips-grid'),e=document.getElementById('clips-empty');try{const r=await fetch('/clips/');if(!r.ok)throw new Error();const t=await r.text();const m=[...t.matchAll(/href="([^"]+\.mp4)"/g)].map(x=>x[1]).reverse();if(m.length===0){e.classList.remove('hidden');g.innerHTML='';return}e.classList.add('hidden');g.innerHTML=m.slice(0,20).map(f=>{const d=f.match(/motion_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})/);const ts=d?`${d[2]}/${d[3]} ${d[4]}:${d[5]}`:'';return`<div class="clip-card" onclick="window.open('/clips/${f}')"><video src="/clips/${f}" muted playsinline></video><div class="clip-time">${ts}</div></div>`}).join('')}catch{e.classList.remove('hidden');g.innerHTML=''}}
async function loadActivity(){const l=document.getElementById('events-list'),e=document.getElementById('activity-empty');try{const r=await fetch('/clips/');if(!r.ok)throw new Error();const t=await r.text();const m=[...t.matchAll(/href="([^"]+\.mp4)"/g)].map(x=>x[1]).reverse();if(m.length===0){e.classList.remove('hidden');l.innerHTML='';return}e.classList.add('hidden');l.innerHTML=m.slice(0,10).map(f=>{const d=f.match(/motion_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})/);const ts=d?`${d[2]}/${d[3]} at ${d[4]}:${d[5]}`:'Unknown time';return`<div class="event-item"><div class="event-icon">🐱</div><div class="event-info"><div class="event-message">Motion detected</div><div class="event-time">${ts}</div></div></div>`}).join('')}catch{e.classList.remove('hidden');l.innerHTML=''}}
async function init(){
loadTheme();
setRandomNames();
const k=getAuthKey();if(!k||!(await validateAuth(k))){toast('Auth required');return}
const s=document.getElementById('stream'),l=document.getElementById('loading'),r=document.getElementById('retry-btn');
s.onload=()=>{s.classList.add('loaded');l.classList.add('hidden');streamRetries=0};
s.onerror=()=>{streamRetries++;if(streamRetries<3){setTimeout(startStream,2000)}else{r.classList.remove('hidden')}};
r.onclick=()=>{streamRetries=0;startStream()};
document.getElementById('btn-hd').onclick=()=>setMode('hd');
document.getElementById('btn-save').onclick=()=>setMode('save');
if(localStorage.getItem(MODE_KEY)==='save'){isHD=false;document.getElementById('btn-hd').classList.remove('active');document.getElementById('btn-save').classList.add('active')}
// Settings
document.getElementById('tab-settings').onclick=openSettings;
document.getElementById('settings-close').onclick=closeSettings;
document.getElementById('settings-overlay').onclick=e=>{if(e.target.id==='settings-overlay')closeSettings()};
document.getElementById('theme-todd').onclick=()=>applyTheme('todd');
document.getElementById('theme-kevin').onclick=()=>applyTheme('kevin');
document.querySelectorAll('.tab[data-page]').forEach(t=>{t.onclick=()=>{document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.page').forEach(x=>x.classList.remove('active'));t.classList.add('active');document.getElementById('page-'+t.dataset.page).classList.add('active');if(t.dataset.page==='clips')loadClips();if(t.dataset.page==='activity')loadActivity()}});
startStream();
}
init();
</script>
</body>
</html>
HTMLEOF
echo "✅ Update complete!"
echo ""
echo "Changes:"
echo " • Kevin theme added (orange & cream)"
echo " • Settings tab with theme switcher"
echo " • Names no longer get cut off"
echo " • Theme saves between visits"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment