Twitch stream event platform with OBS browser source overlay. Features: - Twitch OAuth login (streamer + mod access) - EventSub WebSocket for subs, gift subs, bits, channel points, follows, raids - Chat commands via tmi.js in the overlay - Gap audio — tone plays on new chat message after silence, volume/pitch scale with gap length - Self-hosted media library (upload sounds and videos) - Action configuration — map any event to sound, video, or alert - Per-action cooldowns, test button, enable/disable toggle - Multiple actions per event (all matching actions fire) - Activity log dashboard with EventSub connection status - Layout editor — iframe-based WYSIWYG with drag handles, live style preview - Custom CSS and custom JS injection into overlay - Custom DOM events (bashyoverlay:sub, bashyoverlay:raid, etc.) for custom JS hooks - deploy.sh — one-shot setup and launch script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Form, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from pydantic import BaseModel
|
|
from sqlmodel import Session, select
|
|
|
|
from app.auth import require_user
|
|
from app.database import get_session
|
|
from app.models import OverlayLayout
|
|
from app.templates import templates
|
|
|
|
router = APIRouter(prefix="/layout")
|
|
|
|
|
|
def _get_or_create(session: Session) -> OverlayLayout:
|
|
layout = session.exec(select(OverlayLayout)).first()
|
|
if not layout:
|
|
layout = OverlayLayout()
|
|
session.add(layout)
|
|
session.commit()
|
|
session.refresh(layout)
|
|
return layout
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def layout_page(request: Request, session: Session = Depends(get_session), user=Depends(require_user)):
|
|
layout = _get_or_create(session)
|
|
return templates.TemplateResponse("layout.html", {"request": request, "user": user, "layout": layout})
|
|
|
|
|
|
@router.post("", response_class=HTMLResponse)
|
|
async def save_styles(
|
|
request: Request,
|
|
chat_font_size: int = Form(default=14),
|
|
chat_bg_opacity: float = Form(default=0.55),
|
|
chat_text_color: str = Form(default="#ffffff"),
|
|
chat_max_messages: int = Form(default=20),
|
|
alert_font_size: int = Form(default=24),
|
|
alert_bg_opacity: float = Form(default=0.8),
|
|
alert_text_color: str = Form(default="#ffffff"),
|
|
alert_duration: int = Form(default=4),
|
|
session: Session = Depends(get_session),
|
|
user=Depends(require_user),
|
|
):
|
|
layout = _get_or_create(session)
|
|
layout.chat_font_size = max(8, min(48, chat_font_size))
|
|
layout.chat_bg_opacity = max(0.0, min(1.0, chat_bg_opacity))
|
|
layout.chat_text_color = chat_text_color
|
|
layout.chat_max_messages = max(1, min(50, chat_max_messages))
|
|
layout.alert_font_size = max(8, min(72, alert_font_size))
|
|
layout.alert_bg_opacity = max(0.0, min(1.0, alert_bg_opacity))
|
|
layout.alert_text_color = alert_text_color
|
|
layout.alert_duration = max(1, min(30, alert_duration))
|
|
session.add(layout)
|
|
session.commit()
|
|
session.refresh(layout)
|
|
return templates.TemplateResponse("layout.html", {
|
|
"request": request,
|
|
"user": user,
|
|
"layout": layout,
|
|
"saved": True,
|
|
})
|
|
|
|
|
|
class PositionUpdate(BaseModel):
|
|
widget: str
|
|
left: float
|
|
top: float
|
|
width: Optional[float] = None
|
|
height: Optional[float] = None
|
|
|
|
|
|
@router.post("/positions")
|
|
async def save_positions(
|
|
data: PositionUpdate,
|
|
session: Session = Depends(get_session),
|
|
user=Depends(require_user),
|
|
):
|
|
layout = _get_or_create(session)
|
|
if data.widget == "chat":
|
|
layout.chat_left = round(max(0.0, min(95.0, data.left)), 2)
|
|
layout.chat_top = round(max(0.0, min(95.0, data.top)), 2)
|
|
if data.width is not None: layout.chat_width = round(max(5.0, min(100.0, data.width)), 2)
|
|
if data.height is not None: layout.chat_height = round(max(5.0, min(100.0, data.height)), 2)
|
|
elif data.widget == "alert":
|
|
layout.alert_left = round(max(0.0, min(100.0, data.left)), 2)
|
|
layout.alert_top = round(max(0.0, min(100.0, data.top)), 2)
|
|
session.add(layout)
|
|
session.commit()
|
|
return {"ok": True}
|