speechify-clone/static/index.html
ifedan-ed fb8e1340f1 Add auth, Nextcloud, voice lock, saved docs, progress saving, and all new TTS voices
- 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>
2026-04-11 03:23:18 +02:00

201 lines
9.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>