heystreamer/app/routers/layout.py
Bashy 6ad661c220 Initial commit — BashyOverlay v1
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>
2026-04-22 23:48:35 +03:00

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}