Starting implementation of user made custom CSS templates

This commit is contained in:
2025-11-17 14:14:45 +01:00
parent a120e30e03
commit 589ac73b25
5 changed files with 228 additions and 17 deletions

66
main.py
View File

@@ -1,7 +1,7 @@
import os
from fastapi import FastAPI, Request, Depends, HTTPException
from starlette.middleware.sessions import SessionMiddleware
from starlette.staticfiles import StaticFiles
from starlette.staticfiles import StaticFiles
from starlette.responses import FileResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from contextlib import asynccontextmanager
@@ -63,7 +63,14 @@ async def read_dashboard(request: Request, db: Session = Depends(auth.get_db)):
user.settings = models.Setting()
db.commit()
return templates.TemplateResponse("dashboard.html", {"request": request, "user": user, "overlay_url": overlay_url, "current_theme": user.settings.overlay_theme, "settings": settings})
return templates.TemplateResponse("dashboard.html", {
"request": request,
"user": user,
"overlay_url": overlay_url,
"current_theme": user.settings.overlay_theme,
"settings": settings,
"custom_themes": user.custom_themes
})
@app.get("/logout")
async def logout(request: Request):
@@ -78,13 +85,20 @@ async def read_overlay(request: Request, user_id: int, theme_override: str = Non
if not user:
raise HTTPException(status_code=404, detail="User not found")
# The theme can be forced by a query parameter for previewing
theme_name = "dark-purple" # Default theme
if theme_override:
theme = theme_override
else:
theme = "dark-purple" # Default theme
if user.settings and user.settings.overlay_theme:
theme = user.settings.overlay_theme
theme_name = theme_override
elif user.settings and user.settings.overlay_theme:
theme_name = user.settings.overlay_theme
# Check if it's a custom theme
if theme_name.startswith("custom-"):
theme_id = int(theme_name.split("-")[1])
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id, models.CustomTheme.owner_id == user.id).first()
if not theme:
raise HTTPException(status_code=404, detail="Custom theme not found")
# Use a generic overlay template that will link to the dynamic CSS
return templates.TemplateResponse("overlay-custom.html", {"request": request, "theme_id": theme.id})
return templates.TemplateResponse(f"overlay-{theme}.html", {"request": request})
@@ -101,4 +115,38 @@ async def update_settings(settings_data: schemas.SettingsUpdate, request: Reques
user.settings.overlay_theme = settings_data.overlay_theme
db.commit()
return {"message": "Settings updated successfully"}
return {"message": "Settings updated successfully"}
@app.post("/api/themes", response_model=schemas.CustomTheme)
async def create_theme(theme_data: schemas.CustomThemeCreate, request: Request, db: Session = Depends(auth.get_db)):
user_id = request.session.get('user_id')
if not user_id:
raise HTTPException(status_code=401, detail="Not authenticated")
new_theme = models.CustomTheme(**theme_data.dict(), owner_id=user_id)
db.add(new_theme)
db.commit()
db.refresh(new_theme)
return new_theme
@app.delete("/api/themes/{theme_id}")
async def delete_theme(theme_id: int, request: Request, db: Session = Depends(auth.get_db)):
user_id = request.session.get('user_id')
if not user_id:
raise HTTPException(status_code=401, detail="Not authenticated")
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id, models.CustomTheme.owner_id == user_id).first()
if not theme:
raise HTTPException(status_code=404, detail="Theme not found")
db.delete(theme)
db.commit()
return {"message": "Theme deleted successfully"}
@app.get("/css/custom/{theme_id}")
async def get_custom_css(theme_id: int, db: Session = Depends(auth.get_db)):
theme = db.query(models.CustomTheme).filter(models.CustomTheme.id == theme_id).first()
if not theme:
raise HTTPException(status_code=404, detail="Custom theme not found")
return Response(content=theme.css_content, media_type="text/css")

View File

@@ -228,4 +228,52 @@ picture {
.card p {
margin-top: 0;
color: var(--text-muted-color);
}
/* Custom Theme Form Styles */
.theme-form {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
border-radius: 6px;
font-family: inherit;
font-size: 1rem;
}
.custom-theme-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.5rem;
background-color: var(--background-color);
}
.delete-theme-btn {
background-color: #e43f5a;
color: white;
border: none;
padding: 0.3rem 0.8rem;
border-radius: 4px;
cursor: pointer;
}

View File

@@ -0,0 +1,12 @@
body {
background-color: transparent;
font-family: 'Inter', sans-serif;
font-size: 16px;
margin: 0;
overflow: hidden;
}
.chat-container {
padding: 10px;
height: 100vh;
}

View File

@@ -28,6 +28,13 @@
<option value="bright-green" {% if current_theme == 'bright-green' %}selected{% endif %}>Bright Green</option>
<option value="minimal-light" {% if current_theme == 'minimal-light' %}selected{% endif %}>Minimal Light</option>
<option value="hacker-green" {% if current_theme == 'hacker-green' %}selected{% endif %}>Hacker Green</option>
{% if custom_themes %}
<optgroup label="Your Themes">
{% for theme in custom_themes %}
<option value="custom-{{ theme.id }}" {% if current_theme == 'custom-' ~ theme.id %}selected{% endif %}>{{ theme.name }}</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
</div>
<div class="theme-preview">
@@ -37,7 +44,32 @@
</div>
</div>
<a href="/logout" class="btn logout-btn">Logout</a>
<div class="card">
<h2>Custom Themes</h2>
<p>Create your own themes with CSS. These are private to your account.</p>
<div id="custom-themes-list">
{% for theme in custom_themes %}
<div class="custom-theme-item" id="theme-item-{{ theme.id }}">
<span>{{ theme.name }}</span>
<button class="delete-theme-btn" data-theme-id="{{ theme.id }}">Delete</button>
</div>
{% endfor %}
</div>
<form id="theme-form" class="theme-form">
<h3>Create New Theme</h3>
<div class="form-group">
<label for="theme-name">Theme Name</label>
<input type="text" id="theme-name" name="name" required>
</div>
<div class="form-group">
<label for="theme-css">CSS Content</label>
<textarea id="theme-css" name="css_content" rows="8" required placeholder="body { color: red; }"></textarea>
</div>
<button type="submit" class="btn-primary">Save Theme</button>
</form>
</div>
<style>
.url-box {
@@ -53,13 +85,6 @@
border-radius: 6px;
font-family: monospace;
}
.logout-btn {
margin-top: 1rem;
background-color: #c72c41;
}
.logout-btn:hover {
background-color: #a62636;
}
.theme-selector-container {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -138,6 +163,69 @@ document.addEventListener('DOMContentLoaded', function() {
// Set initial preview on page load
updateTheme(themeSelect.value);
// --- Custom Theme Creation Logic ---
const themeForm = document.getElementById('theme-form');
themeForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(themeForm);
const data = Object.fromEntries(formData.entries());
fetch('/api/themes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
.then(response => response.json())
.then(newTheme => {
// Add new theme to the list and dropdown without reloading
addThemeToList(newTheme);
addThemeToSelect(newTheme);
themeForm.reset(); // Clear the form
})
.catch(error => console.error('Error creating theme:', error));
});
// --- Custom Theme Deletion Logic ---
const themesListContainer = document.getElementById('custom-themes-list');
themesListContainer.addEventListener('click', function(event) {
if (event.target.classList.contains('delete-theme-btn')) {
const themeId = event.target.dataset.themeId;
if (confirm('Are you sure you want to delete this theme?')) {
fetch(`/api/themes/${themeId}`, { method: 'DELETE' })
.then(response => {
if (response.ok) {
// Remove theme from the list and dropdown
document.getElementById(`theme-item-${themeId}`).remove();
document.querySelector(`#theme-select option[value="custom-${themeId}"]`).remove();
}
})
.catch(error => console.error('Error deleting theme:', error));
}
}
});
function addThemeToList(theme) {
const list = document.getElementById('custom-themes-list');
const item = document.createElement('div');
item.className = 'custom-theme-item';
item.id = `theme-item-${theme.id}`;
item.innerHTML = `<span>${theme.name}</span><button class="delete-theme-btn" data-theme-id="${theme.id}">Delete</button>`;
list.appendChild(item);
}
function addThemeToSelect(theme) {
let optgroup = document.querySelector('#theme-select optgroup[label="Your Themes"]');
if (!optgroup) {
optgroup = document.createElement('optgroup');
optgroup.label = 'Your Themes';
themeSelect.appendChild(optgroup);
}
const option = document.createElement('option');
option.value = `custom-${theme.id}`;
option.textContent = theme.name;
optgroup.appendChild(option);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Chat Overlay</title>
<link rel="stylesheet" href="{{ url_for('static', path='css/overlay-base.css') }}">
<link rel="stylesheet" href="/css/custom/{{ theme_id }}">
</head>
<body>
<div class="chat-container">
<!-- Chat messages will be injected here by the WebSocket client -->
</div>
</body>
</html>