- Auth system: JWT login/register with Cloudflare Turnstile, admin + user roles - Admin panel: user management (role/active toggle, delete), Nextcloud config - 6 TTS models: OpenAI TTS / TTS-HD, ElevenLabs, Gemini 2.5 Flash/Pro - Voice lock: prevent model/voice changes while audio is playing - Nextcloud browser: PROPFIND WebDAV, folder navigation, PDF loading - Saved documents: save/load with PUT/POST, progress auto-saves every 2s - Resume playback: remembers sentence position per user per document - Better sentence highlighting with glow effect Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
9.7 KiB
HTML
201 lines
9.7 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Speechify</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
|
||
<link rel="stylesheet" href="/static/style.css"/>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
|
||
<!-- Top Bar -->
|
||
<header class="topbar">
|
||
<div class="topbar-left">
|
||
<div class="logo">
|
||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none">
|
||
<rect width="30" height="30" rx="9" fill="#7c3aed"/>
|
||
<polygon points="11,8 23,15 11,22" fill="white"/>
|
||
</svg>
|
||
<span>Speechify</span>
|
||
</div>
|
||
</div>
|
||
<div class="topbar-right">
|
||
<button class="btn-icon" id="btn-sidebar-toggle" title="Toggle Input">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
|
||
</button>
|
||
<button class="btn-icon" id="btn-settings-toggle" title="Voice Settings">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||
</button>
|
||
<div class="user-menu" id="user-menu">
|
||
<button class="user-btn" id="user-btn">
|
||
<span id="user-label">…</span>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
<div class="user-dropdown" id="user-dropdown">
|
||
<a href="/admin" class="dd-item" id="dd-admin">Admin Panel</a>
|
||
<button class="dd-item" onclick="logout()">Sign Out</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-layout">
|
||
|
||
<!-- Left Sidebar -->
|
||
<aside class="sidebar-left" id="sidebar-left">
|
||
<div class="sidebar-header"><h3>Add Content</h3></div>
|
||
<div class="tabs">
|
||
<button class="tab active" data-tab="text">Text</button>
|
||
<button class="tab" data-tab="pdf">PDF</button>
|
||
<button class="tab" data-tab="url">URL</button>
|
||
<button class="tab" data-tab="cloud">Cloud</button>
|
||
<button class="tab" data-tab="saved">Saved</button>
|
||
</div>
|
||
|
||
<!-- Text -->
|
||
<div class="tab-content active" id="tab-text">
|
||
<textarea id="text-input" placeholder="Paste your text here…"></textarea>
|
||
<button class="btn-primary" id="btn-load-text">Load Text</button>
|
||
</div>
|
||
|
||
<!-- PDF -->
|
||
<div class="tab-content" id="tab-pdf">
|
||
<div class="upload-area" id="upload-area">
|
||
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||
<p>Drop PDF here or<br/><strong>click to upload</strong></p>
|
||
<small>Text is extracted from all pages so the AI can read it aloud</small>
|
||
<input type="file" id="pdf-input" accept=".pdf" hidden/>
|
||
</div>
|
||
<div id="pdf-status" class="status-text"></div>
|
||
</div>
|
||
|
||
<!-- URL -->
|
||
<div class="tab-content" id="tab-url">
|
||
<input type="url" id="url-input" class="text-field" placeholder="https://example.com/article"/>
|
||
<button class="btn-primary" id="btn-load-url">Fetch Article</button>
|
||
<div id="url-status" class="status-text"></div>
|
||
</div>
|
||
|
||
<!-- Nextcloud -->
|
||
<div class="tab-content" id="tab-cloud">
|
||
<div id="nc-path-bar" class="nc-path-bar">
|
||
<span id="nc-path-display">/</span>
|
||
<button class="btn-sm" id="nc-up-btn" onclick="ncUp()">↑ Up</button>
|
||
</div>
|
||
<div id="nc-file-list" class="nc-file-list">
|
||
<div class="status-text">Click Browse to load files</div>
|
||
</div>
|
||
<button class="btn-primary" id="btn-nc-browse" onclick="ncBrowse()">Browse Nextcloud</button>
|
||
<div id="nc-status" class="status-text"></div>
|
||
</div>
|
||
|
||
<!-- Saved Docs -->
|
||
<div class="tab-content" id="tab-saved">
|
||
<div id="saved-list" class="saved-list">
|
||
<div class="status-text">No saved documents</div>
|
||
</div>
|
||
<button class="btn-secondary" id="btn-refresh-saved" onclick="loadSavedDocs()">Refresh</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content -->
|
||
<main class="content-area">
|
||
<div id="document-header" class="document-header" style="display:none">
|
||
<div class="doc-title-row">
|
||
<h1 class="document-title" id="document-title"></h1>
|
||
<button class="btn-save" id="btn-save-doc" title="Save document">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||
Save
|
||
</button>
|
||
</div>
|
||
<p class="document-meta" id="document-meta"></p>
|
||
<div class="doc-hint">Click any sentence to jump there · Space to play/pause · ← → to skip</div>
|
||
</div>
|
||
<div class="text-viewer" id="text-viewer">
|
||
<div class="empty-state">
|
||
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||
<p>Load a document to get started</p>
|
||
<span>Paste text, upload a PDF, enter a URL, or pick from Nextcloud</span>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Right Sidebar: Settings -->
|
||
<aside class="sidebar-right" id="sidebar-right">
|
||
<div class="sidebar-header"><h3>Voice Settings</h3></div>
|
||
|
||
<div class="setting-group">
|
||
<label class="setting-label">TTS Model</label>
|
||
<select id="model-select" class="select"></select>
|
||
<div class="lock-notice" id="lock-notice">Stop playback to change voice</div>
|
||
</div>
|
||
<div class="setting-group">
|
||
<label class="setting-label">Voice</label>
|
||
<select id="voice-select" class="select"></select>
|
||
</div>
|
||
<div class="setting-group">
|
||
<label class="setting-label">Speed — <span id="speed-label">1×</span></label>
|
||
<input type="range" id="speed-slider" min="0.5" max="3" step="0.25" value="1" class="slider"/>
|
||
<div class="speed-presets">
|
||
<button class="speed-btn" data-speed="0.75">0.75×</button>
|
||
<button class="speed-btn active" data-speed="1">1×</button>
|
||
<button class="speed-btn" data-speed="1.25">1.25×</button>
|
||
<button class="speed-btn" data-speed="1.5">1.5×</button>
|
||
<button class="speed-btn" data-speed="2">2×</button>
|
||
<button class="speed-btn" data-speed="3">3×</button>
|
||
</div>
|
||
</div>
|
||
<div class="setting-group">
|
||
<label class="setting-label">Font Size</label>
|
||
<div class="font-size-row">
|
||
<button class="btn-sz" id="font-decrease">A−</button>
|
||
<span id="font-size-label">18px</span>
|
||
<button class="btn-sz" id="font-increase">A+</button>
|
||
</div>
|
||
</div>
|
||
<div class="setting-group">
|
||
<button class="btn-secondary" id="btn-preview">▶ Preview Voice</button>
|
||
</div>
|
||
</aside>
|
||
|
||
</div>
|
||
|
||
<!-- Player Bar -->
|
||
<div class="player-bar">
|
||
<div class="player-left">
|
||
<p class="now-playing" id="now-playing">No document loaded</p>
|
||
</div>
|
||
<div class="player-center">
|
||
<button class="ctrl-btn" id="btn-prev" title="Previous (←)">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg>
|
||
</button>
|
||
<button class="play-btn" id="btn-play" title="Play/Pause (Space)">
|
||
<svg class="icon-play" width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||
<svg class="icon-pause" width="26" height="26" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||
</button>
|
||
<button class="ctrl-btn" id="btn-next" title="Next (→)">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zm2.5-6 5.5 4V8l-5.5 4zM16 6h2v12h-2z"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="player-right">
|
||
<span class="progress-text"><span id="cur-sent">0</span> / <span id="tot-sent">0</span></span>
|
||
<div class="progress-track" id="progress-track">
|
||
<div class="progress-fill" id="progress-fill"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Loading overlay -->
|
||
<div class="loading-overlay" id="loading-overlay">
|
||
<div class="spinner"></div>
|
||
<p id="loading-text">Loading…</p>
|
||
</div>
|
||
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|