Record / index.html
Hectozar's picture
Update index.html
130fea5 verified
Raw History Blame Contribute Delete
67.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ProAudio Studio - Advanced Web Recorder</title>
<!-- Custom embedded SVG Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMjU2M2ViIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTEyIDJhMyAzIDAgMCAwLTMgM3Y3YTMgMyAwIDAgMCA2IDBWNWEzIDMgMCAwIDAtMy0zeiI+PC9wYXRoPjxwb2x5bGluZSBwb2ludHM9IjE5IDEwIDE5IDEyIDE5IDE0Ij48L3BvbHlsaW5lPjxsaW5lIHgxPSI1IiB5MT0iMTIiIHgyPSIxOSIgeTI9IjEyIj48L2xpbmU+PHBhdGggZD0iTTUgMTB2NGExIDEgMCAwIDAgMSAxIDEgMSAwIDAgMCAxLTF2LTQiPjwvcGF0aD48L3N2Zz4=">
<style>
/* ==========================================================================
CSS VARIABLES & THEME (Ultra-Professional Light Mode)
========================================================================== */
:root {
/* Core Colors */
--brand-primary: #2563eb;
--brand-primary-hover: #1d4ed8;
--brand-primary-light: #eff6ff;
--brand-danger: #ef4444;
--brand-danger-hover: #dc2626;
--brand-success: #10b981;
--brand-warning: #f59e0b;
/* Backgrounds */
--bg-base: #ffffff;
--bg-surface: #f8fafc;
--bg-surface-hover: #f1f5f9;
--bg-surface-active: #e2e8f0;
--bg-modal: rgba(15, 23, 42, 0.5);
/* Typography */
--text-strong: #0f172a;
--text-normal: #334155;
--text-muted: #64748b;
/* Borders & Shadows */
--border-light: #e2e8f0;
--border-medium: #cbd5e1;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* Sizing & Radius */
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
--sidebar-width: 320px;
/* Graph Colors */
--graph-color-1: #3b82f6;
--graph-color-2: #60a5fa;
/* Fonts */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* ==========================================================================
RESET & GLOBAL STYLES
========================================================================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-sans);
background-color: var(--bg-base);
color: var(--text-strong);
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
input, button, textarea {
font-family: inherit;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-medium);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ==========================================================================
TYPOGRAPHY & UTILITIES
========================================================================== */
h1, h2, h3 { font-weight: 600; color: var(--text-strong); }
h1 { font-size: 28px; letter-spacing: -0.02em; }
h2 { font-size: 20px; }
p { color: var(--text-normal); line-height: 1.5; }
.text-muted { color: var(--text-muted); font-size: 13px; }
.hidden { display: none !important; }
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 8px; }
.gap-4 { gap: 16px; }
/* ==========================================================================
BUTTONS
========================================================================== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 18px;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn:active:not(:disabled) {
transform: scale(0.98);
}
.btn-primary { background-color: var(--brand-primary); color: white; box-shadow: var(--shadow-sm); }
.btn-primary:hover:not(:disabled) { background-color: var(--brand-primary-hover); }
.btn-danger { background-color: var(--brand-danger); color: white; box-shadow: var(--shadow-sm); }
.btn-danger:hover:not(:disabled) { background-color: var(--brand-danger-hover); }
.btn-outline { background-color: transparent; border-color: var(--border-medium); color: var(--text-strong); }
.btn-outline:hover:not(:disabled) { background-color: var(--bg-surface-hover); border-color: var(--text-muted); }
.btn-ghost { background-color: transparent; color: var(--text-normal); padding: 8px; border-radius: var(--radius-md); }
.btn-ghost:hover:not(:disabled) { background-color: var(--bg-surface-active); color: var(--text-strong); }
.btn-icon { padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; color: var(--text-muted); transition: all 0.2s; }
.btn-icon:hover { background-color: var(--bg-surface-active); color: var(--text-strong); }
.btn-icon.active { color: var(--brand-warning); }
/* SVG Icon Sizing */
svg.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
svg.icon-sm { width: 14px; height: 14px; }
svg.icon-lg { width: 24px; height: 24px; }
/* ==========================================================================
LAYOUT - SIDEBAR (Gemini Style)
========================================================================== */
.sidebar {
width: var(--sidebar-width);
background-color: var(--bg-surface);
border-right: 1px solid var(--border-light);
display: flex;
flex-direction: column;
z-index: 10;
}
.sidebar-header {
padding: 24px;
border-bottom: 1px solid var(--border-light);
}
.logo-area {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.logo-icon {
width: 32px;
height: 32px;
background: var(--brand-primary-light);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--brand-primary);
}
.logo-text { font-size: 18px; font-weight: 700; color: var(--text-strong); }
.new-record-btn { width: 100%; justify-content: flex-start; padding: 12px 16px; font-size: 15px; }
.sidebar-search {
padding: 16px 24px;
position: relative;
}
.sidebar-search input {
width: 100%;
padding: 10px 10px 10px 36px;
border-radius: var(--radius-md);
border: 1px solid var(--border-medium);
background-color: var(--bg-base);
font-size: 14px;
transition: all 0.2s;
}
.sidebar-search input:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 3px var(--brand-primary-light); }
.sidebar-search svg { position: absolute; left: 36px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
.history-list {
flex: 1;
overflow-y: auto;
padding: 0 12px 24px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.history-item {
padding: 12px 16px;
border-radius: var(--radius-md);
display: flex;
align-items: flex-start;
justify-content: space-between;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.history-item:hover { background-color: var(--bg-surface-hover); }
.history-item.active { background-color: var(--brand-primary-light); border-color: #bfdbfe; }
.history-info { display: flex; flex-direction: column; gap: 4px; flex: 1; overflow: hidden; }
.history-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-strong); }
.history-meta { font-size: 12px; color: var(--text-muted); display: flex; gap: 8px; align-items: center; }
/* ==========================================================================
LAYOUT - MAIN CONTENT
========================================================================== */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--bg-base);
position: relative;
overflow-y: auto;
}
.topbar {
padding: 24px 40px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-light);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
position: sticky;
top: 0;
z-index: 5;
}
.workspace {
padding: 40px;
max-width: 1000px;
margin: 0 auto;
width: 100%;
display: flex;
flex-direction: column;
gap: 40px;
}
/* ==========================================================================
RECORDING CONSOLE & VISUALIZER
========================================================================== */
.recording-console {
background-color: var(--bg-surface);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: var(--shadow-md);
transition: all 0.3s ease;
}
.recording-console.is-recording {
border-color: #fca5a5;
box-shadow: 0 0 0 4px #fee2e2;
}
.visualizer-container {
width: 100%;
height: 160px;
background-color: var(--text-strong);
border-radius: var(--radius-md);
overflow: hidden;
position: relative;
margin-bottom: 30px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2);
}
canvas#liveVisualizer {
width: 100%;
height: 100%;
display: block;
}
.recording-status {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--text-muted);
transition: all 0.3s;
}
.is-recording .status-dot {
background-color: var(--brand-danger);
animation: pulse-red 1.5s infinite;
}
.is-paused .status-dot {
background-color: var(--brand-warning);
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
.timer-display {
font-size: 56px;
font-weight: 300;
font-variant-numeric: tabular-nums;
letter-spacing: -1px;
color: var(--text-strong);
}
.controls-bar {
display: flex;
gap: 16px;
margin-top: 20px;
}
/* ==========================================================================
PLAYBACK & EDITING CONSOLE (The Professional Editor)
========================================================================== */
.editor-console {
background-color: var(--bg-base);
border: 1px solid var(--border-medium);
border-radius: var(--radius-lg);
padding: 30px;
box-shadow: var(--shadow-lg);
display: none;
flex-direction: column;
gap: 24px;
}
.editor-console.active {
display: flex;
animation: fadeIn 0.4s ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.title-group { display: flex; align-items: center; gap: 12px; }
.editable-title {
font-size: 24px;
font-weight: 600;
border: 1px dashed transparent;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
cursor: pointer;
}
.editable-title:hover { border-color: var(--border-medium); background: var(--bg-surface); }
.editable-title:focus { outline: none; border-color: var(--brand-primary); background: var(--bg-base); cursor: text; }
.editor-actions { display: flex; gap: 8px; }
/* Complex Static Waveform */
.waveform-container {
width: 100%;
height: 120px;
background-color: var(--bg-surface-hover);
border-radius: var(--radius-md);
position: relative;
cursor: pointer;
border: 1px solid var(--border-light);
overflow: hidden;
}
canvas#staticWaveform {
width: 100%;
height: 100%;
display: block;
pointer-events: none;
}
.playhead {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background-color: var(--brand-danger);
left: 0%;
pointer-events: none;
z-index: 2;
box-shadow: 0 0 4px rgba(239, 68, 68, 0.5);
}
/* Editing / Trimming Controls */
.trim-controls {
display: flex;
align-items: center;
gap: 20px;
background: var(--bg-surface);
padding: 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
}
.trim-group { display: flex; align-items: center; gap: 10px; }
.trim-group label { font-size: 13px; font-weight: 600; color: var(--text-normal); text-transform: uppercase;}
.trim-group input[type="number"] {
width: 80px;
padding: 8px;
border: 1px solid var(--border-medium);
border-radius: 4px;
font-size: 14px;
font-family: monospace;
}
.playback-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
}
.play-btn-large {
width: 56px;
height: 56px;
border-radius: 50%;
background-color: var(--brand-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
border: none;
cursor: pointer;
box-shadow: var(--shadow-md);
transition: all 0.2s;
}
.play-btn-large:hover { background-color: var(--brand-primary-hover); transform: scale(1.05); }
.play-btn-large svg { width: 24px; height: 24px; fill: currentColor; }
.export-section {
display: flex;
gap: 16px;
padding-top: 20px;
border-top: 1px solid var(--border-light);
}
.link-generator {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-surface);
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
padding: 4px 4px 4px 12px;
}
.link-generator input {
flex: 1;
border: none;
background: transparent;
font-size: 13px;
color: var(--text-muted);
outline: none;
}
/* ==========================================================================
MODALS & LOADING SCREEN
========================================================================== */
#loadingScreen {
position: fixed;
inset: 0;
background-color: var(--bg-base);
z-index: 9999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: opacity 0.6s ease;
}
.loader-wave {
display: flex;
gap: 6px;
margin-bottom: 24px;
}
.loader-bar {
width: 8px;
height: 40px;
background-color: var(--brand-primary);
border-radius: 4px;
animation: wave 1s ease-in-out infinite;
}
.loader-bar:nth-child(2) { animation-delay: 0.1s; }
.loader-bar:nth-child(3) { animation-delay: 0.2s; }
.loader-bar:nth-child(4) { animation-delay: 0.3s; }
.loader-bar:nth-child(5) { animation-delay: 0.4s; }
@keyframes wave {
0%, 100% { transform: scaleY(0.5); }
50% { transform: scaleY(1); }
}
/* Modals */
.modal-overlay {
position: fixed;
inset: 0;
background-color: var(--bg-modal);
backdrop-filter: blur(4px);
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.modal-overlay.active {
display: flex;
opacity: 1;
}
.modal-content {
background-color: var(--bg-base);
width: 100%;
max-width: 400px;
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow-lg);
transform: scale(0.95);
transition: transform 0.2s;
}
.modal-overlay.active .modal-content {
transform: scale(1);
}
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
.modal-body { color: var(--text-normal); margin-bottom: 24px; font-size: 14px; }
.modal-footer { display: flex; justify-content: flex-end; gap: 12px; }
/* Toast */
#toast {
position: fixed;
bottom: 24px;
right: 24px;
background-color: var(--text-strong);
color: white;
padding: 12px 24px;
border-radius: var(--radius-md);
font-size: 14px;
box-shadow: var(--shadow-md);
transform: translateY(100px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
z-index: 2000;
}
#toast.show { transform: translateY(0); opacity: 1; }
</style>
</head>
<body>
<!-- ==========================================
LOADING SCREEN
========================================== -->
<div id="loadingScreen">
<div class="loader-wave">
<div class="loader-bar"></div>
<div class="loader-bar"></div>
<div class="loader-bar"></div>
<div class="loader-bar"></div>
<div class="loader-bar"></div>
</div>
<h2>Initializing Audio Engine...</h2>
<p class="text-muted" style="margin-top: 10px;">Setting up local workspace</p>
</div>
<!-- ==========================================
SIDEBAR (WORKSPACE)
========================================== -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo-area">
<div class="logo-icon">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><polyline points="19 10 19 12 19 14"/><line x1="5" y1="12" x2="19" y2="12"/><path d="M5 10v4a1 1 0 0 0 1 1 1 1 0 0 0 1-1v-4"/></svg>
</div>
<div class="logo-text">ProAudio Studio</div>
</div>
<button class="btn btn-primary new-record-btn" id="navNewRecordBtn">
<svg class="icon" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New Recording
</button>
</div>
<div class="sidebar-search">
<svg class="icon icon-sm" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="searchInput" placeholder="Search recordings...">
</div>
<div class="history-list" id="historyList">
<!-- Dynamic items injected here via JS -->
</div>
</aside>
<!-- ==========================================
MAIN CONTENT AREA
========================================== -->
<main class="main-content">
<header class="topbar">
<h2>Workspace</h2>
<div class="text-muted" id="storageInfo">Local Storage Active</div>
</header>
<div class="workspace">
<!-- RECORDING CONSOLE -->
<section class="recording-console" id="recordingConsole">
<div class="visualizer-container">
<canvas id="liveVisualizer"></canvas>
</div>
<div class="recording-status">
<div class="status-dot"></div>
<div class="text-muted" id="statusText">Ready to Record</div>
</div>
<div class="timer-display" id="liveTimer">00:00:00</div>
<div class="controls-bar">
<button class="btn btn-danger" id="recordBtn">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
Start Recording
</button>
<button class="btn btn-outline hidden" id="pauseBtn">
<svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
Pause
</button>
<button class="btn btn-primary hidden" id="stopBtn">
<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/></svg>
Stop & Save
</button>
</div>
</section>
<!-- EDITOR / PLAYBACK CONSOLE -->
<section class="editor-console" id="editorConsole">
<div class="editor-header">
<div class="title-group">
<svg class="icon icon-lg" style="color:var(--brand-primary)" viewBox="0 0 24 24"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/></svg>
<!-- Editable Title -->
<h2 class="editable-title" id="editTitle" contenteditable="true" spellcheck="false" title="Click to rename">Recording Name</h2>
<button class="btn-icon" id="renameTriggerBtn" title="Rename"><svg class="icon icon-sm" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
</div>
<div class="editor-actions">
<button class="btn-icon" id="editorFavBtn" title="Favorite"><svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg></button>
<button class="btn-icon" id="editorDeleteBtn" title="Delete" style="color:var(--brand-danger)"><svg class="icon" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg></button>
</div>
</div>
<!-- Static Waveform -->
<div class="waveform-container" id="waveformContainer">
<canvas id="staticWaveform"></canvas>
<div class="playhead" id="playhead"></div>
</div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<span class="text-muted" id="currentTimeDisplay">00:00.00</span>
<span class="text-muted" id="totalDurationDisplay">00:00.00</span>
</div>
<!-- Advanced Trimming -->
<div class="trim-controls">
<span style="font-size:13px; font-weight:600; color:var(--text-strong);">TRIM AUDIO</span>
<div class="trim-group">
<label>Start (s)</label>
<input type="number" id="trimStart" value="0.00" step="0.1" min="0">
</div>
<div class="trim-group">
<label>End (s)</label>
<input type="number" id="trimEnd" value="0.00" step="0.1" min="0">
</div>
<button class="btn btn-outline" id="applyTrimBtn" style="padding:6px 12px; font-size:12px;">Apply Trim</button>
</div>
<div class="playback-controls">
<button class="btn-icon" id="rewindBtn"><svg class="icon" viewBox="0 0 24 24"><polygon points="11 19 2 12 11 5 11 19"/><polygon points="22 19 13 12 22 5 22 19"/></svg></button>
<button class="play-btn-large" id="playPauseBtn">
<svg viewBox="0 0 24 24" id="playIconSVG"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button class="btn-icon" id="forwardBtn"><svg class="icon" viewBox="0 0 24 24"><polygon points="13 19 22 12 13 5 13 19"/><polygon points="2 19 11 12 2 5 2 19"/></svg></button>
</div>
<div class="export-section">
<button class="btn btn-primary" id="downloadBtn">
<svg class="icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Download Audio
</button>
<div class="link-generator">
<svg class="icon icon-sm" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
<input type="text" id="shareLinkInput" readonly value="blob:http://...">
<button class="btn btn-outline" id="copyLinkBtn" style="padding: 6px 12px; font-size: 12px;">Copy Link</button>
</div>
</div>
<!-- Hidden Audio Element for playback -->
<audio id="hiddenAudioPlayer" style="display:none;"></audio>
</section>
</div>
</main>
<!-- ==========================================
MODALS & TOASTS
========================================== -->
<!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="modal-content">
<h3 class="modal-title">Delete Recording?</h3>
<p class="modal-body">Are you sure you want to delete "<span id="deleteItemName" style="font-weight:600;"></span>"? This action cannot be undone and the file will be removed from your local database.</p>
<div class="modal-footer">
<button class="btn btn-outline" id="cancelDeleteBtn">Cancel</button>
<button class="btn btn-danger" id="confirmDeleteBtn">Delete</button>
</div>
</div>
</div>
<!-- Toast Notification -->
<div id="toast">Notification Message</div>
<!-- ==========================================
JAVASCRIPT / APPLICATION LOGIC
========================================== -->
<script>
/**
* PROAUDIO STUDIO APP
* Massive monolithic architecture for advanced audio processing in browser.
*/
// --- 1. UTILITIES ---
const Utils = {
formatTime: (seconds) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 100);
const mStr = m.toString().padStart(2, '0');
const sStr = s.toString().padStart(2, '0');
if (h > 0) return `${h.toString().padStart(2, '0')}:${mStr}:${sStr}`;
// If requesting ms precision (for editor)
if (arguments[1] === true) return `${mStr}:${sStr}.${ms.toString().padStart(2, '0')}`;
return `${mStr}:${sStr}`;
},
formatDate: (timestamp) => {
const d = new Date(timestamp);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute:'2-digit' });
},
generateId: () => 'rec_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
showToast: (message, duration = 3000) => {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), duration);
}
};
// --- 2. DATABASE MANAGEMENT (IndexedDB) ---
class Database {
constructor() {
this.dbName = 'ProAudioDB';
this.dbVersion = 1;
this.storeName = 'recordings';
this.db = null;
}
init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onupgradeneeded = (event) => {
this.db = event.target.result;
if (!this.db.objectStoreNames.contains(this.storeName)) {
const store = this.db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => reject(event.target.error);
});
}
async saveRecord(record) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([this.storeName], 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.put(record);
request.onsuccess = () => resolve(record);
request.onerror = () => reject(request.error);
});
}
async getAllRecords() {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([this.storeName], 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => {
const records = request.result.sort((a, b) => b.timestamp - a.timestamp);
resolve(records);
};
request.onerror = () => reject(request.error);
});
}
async deleteRecord(id) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([this.storeName], 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async getRecord(id) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([this.storeName], 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// --- 3. ADVANCED AUDIO ENGINE (Web Audio API) ---
class AudioEngine {
constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
async decodeAudioData(blob) {
const arrayBuffer = await blob.arrayBuffer();
return await this.audioContext.decodeAudioData(arrayBuffer);
}
// High-performance waveform extraction
getWaveformData(audioBuffer, points = 200) {
const rawData = audioBuffer.getChannelData(0); // Left channel
const samples = rawData.length;
const blockSize = Math.floor(samples / points);
const filteredData = [];
for (let i = 0; i < points; i++) {
let blockStart = blockSize * i;
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum += Math.abs(rawData[blockStart + j]);
}
filteredData.push(sum / blockSize);
}
// Normalize
const multiplier = Math.max(...filteredData) ** -1;
return filteredData.map(n => n * multiplier);
}
// Trims an AudioBuffer and encodes back to WAV blob
async trimAudioBuffer(buffer, startSec, endSec) {
const rate = buffer.sampleRate;
const startOffset = Math.max(0, Math.floor(startSec * rate));
const endOffset = Math.min(buffer.length, Math.floor(endSec * rate));
const frameCount = endOffset - startOffset;
if (frameCount <= 0) throw new Error("Invalid trim range");
const newContext = new OfflineAudioContext(buffer.numberOfChannels, frameCount, rate);
const newBuffer = newContext.createBuffer(buffer.numberOfChannels, frameCount, rate);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
const channelData = buffer.getChannelData(channel);
const newChannelData = newBuffer.getChannelData(channel);
newChannelData.set(channelData.subarray(startOffset, endOffset));
}
return this.bufferToWave(newBuffer, frameCount);
}
// Convert AudioBuffer to WAV format blob
bufferToWave(abuffer, len) {
const numOfChan = abuffer.numberOfChannels;
const length = len * numOfChan * 2 + 44;
const buffer = new ArrayBuffer(length);
const view = new DataView(buffer);
const channels = [];
let i;
let sample;
let offset = 0;
let pos = 0;
// Write WAV header
const setUint16 = (data) => { view.setUint16(pos, data, true); pos += 2; };
const setUint32 = (data) => { view.setUint32(pos, data, true); pos += 4; };
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // length = 16
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
setUint32(abuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
setUint16(numOfChan * 2); // block-align
setUint16(16); // 16-bit
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4); // chunk length
for(i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i));
while(pos < length) {
for(i = 0; i < numOfChan; i++) {
sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767)|0; // scale
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}
return new Blob([buffer], {type: "audio/wav"});
}
}
// --- 4. VISUALIZER CLASS ---
class Visualizer {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.animationId = null;
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width * (window.devicePixelRatio || 1);
this.canvas.height = rect.height * (window.devicePixelRatio || 1);
this.ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
this.width = rect.width;
this.height = rect.height;
this.clear();
}
clear() {
this.ctx.fillStyle = '#0f172a'; // dark background
this.ctx.fillRect(0, 0, this.width, this.height);
// Draw middle line
this.ctx.beginPath();
this.ctx.moveTo(0, this.height/2);
this.ctx.lineTo(this.width, this.height/2);
this.ctx.strokeStyle = '#334155';
this.ctx.lineWidth = 1;
this.ctx.stroke();
}
drawLive(analyser) {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const draw = () => {
this.animationId = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
this.ctx.fillStyle = '#0f172a';
this.ctx.fillRect(0, 0, this.width, this.height);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#3b82f6';
this.ctx.beginPath();
const sliceWidth = this.width * 1.0 / bufferLength;
let x = 0;
for(let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * this.height/2;
if(i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.lineTo(this.width, this.height/2);
this.ctx.stroke();
// Add glow effect
this.ctx.shadowBlur = 10;
this.ctx.shadowColor = '#60a5fa';
this.ctx.stroke();
this.ctx.shadowBlur = 0;
}
draw();
}
stop() {
if(this.animationId) cancelAnimationFrame(this.animationId);
this.clear();
}
// Draw Static Waveform (For Editor)
drawStatic(waveformData) {
this.clear();
this.ctx.fillStyle = 'transparent'; // Use container background
this.ctx.clearRect(0, 0, this.width, this.height);
const barWidth = (this.width / waveformData.length);
const centerY = this.height / 2;
waveformData.forEach((val, i) => {
const h = Math.max(2, val * this.height * 0.8);
const x = i * barWidth;
// Gradient based on height
const grad = this.ctx.createLinearGradient(0, centerY - h/2, 0, centerY + h/2);
grad.addColorStop(0, '#60a5fa');
grad.addColorStop(0.5, '#2563eb');
grad.addColorStop(1, '#60a5fa');
this.ctx.fillStyle = grad;
// Rounded bars
this.roundRect(this.ctx, x + 1, centerY - h/2, barWidth - 2, h, 2);
this.ctx.fill();
});
}
roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
}
// --- 5. MAIN APPLICATION CONTROLLER ---
class App {
constructor() {
this.db = new Database();
this.audioEngine = new AudioEngine();
this.liveVisualizer = new Visualizer('liveVisualizer');
this.staticVisualizer = new Visualizer('staticWaveform');
// State variables
this.mediaRecorder = null;
this.audioChunks = [];
this.startTime = null;
this.timerInterval = null;
this.elapsedSeconds = 0;
this.currentStream = null;
this.analyser = null;
this.currentRecordId = null;
this.currentAudioUrl = null;
this.currentAudioBuffer = null;
this.isPlaying = false;
// Playback
this.audioPlayer = document.getElementById('hiddenAudioPlayer');
this.init();
}
async init() {
try {
await this.db.init();
this.bindEvents();
await this.renderHistory();
// Hide loading screen
setTimeout(() => {
document.getElementById('loadingScreen').style.opacity = '0';
setTimeout(() => document.getElementById('loadingScreen').style.display = 'none', 600);
}, 1000); // 1 sec fake delay for feel
} catch (err) {
alert("Application failed to initialize. Please ensure browser storage is allowed.");
console.error(err);
}
}
bindEvents() {
// Recording Controls
document.getElementById('recordBtn').addEventListener('click', () => this.startRecording());
document.getElementById('pauseBtn').addEventListener('click', () => this.togglePauseRecording());
document.getElementById('stopBtn').addEventListener('click', () => this.stopRecording());
// Navigation / Sidebar
document.getElementById('navNewRecordBtn').addEventListener('click', () => this.showRecordingConsole());
document.getElementById('searchInput').addEventListener('input', (e) => this.filterHistory(e.target.value));
// Editor Controls
document.getElementById('playPauseBtn').addEventListener('click', () => this.togglePlayback());
document.getElementById('hiddenAudioPlayer').addEventListener('timeupdate', () => this.updatePlayhead());
document.getElementById('hiddenAudioPlayer').addEventListener('ended', () => {
this.isPlaying = false;
this.updatePlayIcon();
document.getElementById('playhead').style.left = '0%';
});
// Clicking on waveform to seek
document.getElementById('waveformContainer').addEventListener('click', (e) => {
if(!this.currentAudioBuffer) return;
const rect = e.currentTarget.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
this.audioPlayer.currentTime = percent * this.audioPlayer.duration;
this.updatePlayhead();
});
// Rename Logic
const titleEl = document.getElementById('editTitle');
titleEl.addEventListener('blur', () => this.saveRename());
titleEl.addEventListener('keydown', (e) => { if(e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } });
document.getElementById('renameTriggerBtn').addEventListener('click', () => titleEl.focus());
// Editor Action Buttons
document.getElementById('editorFavBtn').addEventListener('click', () => this.toggleFavoriteCurrent());
document.getElementById('editorDeleteBtn').addEventListener('click', () => this.promptDelete(this.currentRecordId));
document.getElementById('downloadBtn').addEventListener('click', () => this.downloadCurrent());
document.getElementById('copyLinkBtn').addEventListener('click', () => this.copyShareLink());
document.getElementById('applyTrimBtn').addEventListener('click', () => this.applyTrim());
// Modal
document.getElementById('cancelDeleteBtn').addEventListener('click', () => this.closeModal());
document.getElementById('confirmDeleteBtn').addEventListener('click', () => this.executeDelete());
}
// --- RECORDING LOGIC ---
async startRecording() {
try {
this.currentStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
// Setup Web Audio nodes for visualizer
const source = this.audioEngine.audioContext.createMediaStreamSource(this.currentStream);
this.analyser = this.audioEngine.audioContext.createAnalyser();
this.analyser.fftSize = 2048;
source.connect(this.analyser);
this.liveVisualizer.drawLive(this.analyser);
this.mediaRecorder = new MediaRecorder(this.currentStream);
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) this.audioChunks.push(e.data);
};
this.mediaRecorder.onstop = () => this.processRecordingSave();
this.mediaRecorder.start(100); // 100ms chunks
this.elapsedSeconds = 0;
this.updateTimerDisplay();
this.timerInterval = setInterval(() => {
this.elapsedSeconds++;
this.updateTimerDisplay();
}, 1000);
this.updateUIRecording(true);
} catch (err) {
alert("Microphone access denied or unavailable.");
console.error(err);
}
}
togglePauseRecording() {
if (this.mediaRecorder.state === "recording") {
this.mediaRecorder.pause();
clearInterval(this.timerInterval);
this.liveVisualizer.stop();
const btn = document.getElementById('pauseBtn');
btn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Resume';
document.getElementById('recordingConsole').className = 'recording-console is-paused';
document.getElementById('statusText').textContent = "Paused";
} else if (this.mediaRecorder.state === "paused") {
this.mediaRecorder.resume();
this.timerInterval = setInterval(() => {
this.elapsedSeconds++;
this.updateTimerDisplay();
}, 1000);
this.liveVisualizer.drawLive(this.analyser);
const btn = document.getElementById('pauseBtn');
btn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Pause';
document.getElementById('recordingConsole').className = 'recording-console is-recording';
document.getElementById('statusText').textContent = "Recording...";
}
}
stopRecording() {
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
this.mediaRecorder.stop();
clearInterval(this.timerInterval);
this.currentStream.getTracks().forEach(track => track.stop());
this.liveVisualizer.stop();
this.updateUIRecording(false);
}
}
async processRecordingSave() {
const blob = new Blob(this.audioChunks, { type: 'audio/webm' });
const id = Utils.generateId();
const record = {
id: id,
name: `Recording ${Utils.formatDate(Date.now())}`,
timestamp: Date.now(),
duration: this.elapsedSeconds,
blob: blob,
favorite: false
};
await this.db.saveRecord(record);
Utils.showToast("Recording saved successfully");
await this.renderHistory();
this.loadRecordingIntoEditor(record);
}
updateTimerDisplay() {
document.getElementById('liveTimer').textContent = Utils.formatTime(this.elapsedSeconds);
}
updateUIRecording(isRecording) {
const recBtn = document.getElementById('recordBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const consoleEl = document.getElementById('recordingConsole');
const statusTxt = document.getElementById('statusText');
if (isRecording) {
recBtn.classList.add('hidden');
pauseBtn.classList.remove('hidden');
stopBtn.classList.remove('hidden');
consoleEl.className = 'recording-console is-recording';
statusTxt.textContent = "Recording...";
} else {
recBtn.classList.remove('hidden');
pauseBtn.classList.add('hidden');
stopBtn.classList.add('hidden');
consoleEl.className = 'recording-console';
statusTxt.textContent = "Ready to Record";
document.getElementById('liveTimer').textContent = "00:00:00";
pauseBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Pause';
}
}
showRecordingConsole() {
// Stop any playback
this.audioPlayer.pause();
this.isPlaying = false;
document.getElementById('editorConsole').classList.remove('active');
document.getElementById('recordingConsole').style.display = 'flex';
// Remove active class from history
document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active'));
}
// --- EDITOR & PLAYBACK LOGIC ---
async loadRecordingIntoEditor(record) {
// UI Switch
document.getElementById('recordingConsole').style.display = 'none';
document.getElementById('editorConsole').classList.add('active');
// Highlight active item
document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active'));
const hItem = document.getElementById(`hist_${record.id}`);
if(hItem) hItem.classList.add('active');
// Set Data
this.currentRecordId = record.id;
document.getElementById('editTitle').textContent = record.name;
// Update Favorite Icon
const favBtn = document.getElementById('editorFavBtn');
if(record.favorite) {
favBtn.classList.add('active');
favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
} else {
favBtn.classList.remove('active');
favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
}
// Setup Audio
if (this.currentAudioUrl) URL.revokeObjectURL(this.currentAudioUrl);
this.currentAudioUrl = URL.createObjectURL(record.blob);
this.audioPlayer.src = this.currentAudioUrl;
document.getElementById('shareLinkInput').value = this.currentAudioUrl;
// Process Waveform
try {
this.currentAudioBuffer = await this.audioEngine.decodeAudioData(record.blob);
const waveData = this.audioEngine.getWaveformData(this.currentAudioBuffer, 150);
this.staticVisualizer.drawStatic(waveData);
const dur = this.currentAudioBuffer.duration;
document.getElementById('totalDurationDisplay').textContent = Utils.formatTime(dur, true);
document.getElementById('currentTimeDisplay').textContent = "00:00.00";
// Setup trim inputs
document.getElementById('trimStart').value = 0;
document.getElementById('trimEnd').value = dur.toFixed(2);
document.getElementById('trimEnd').max = dur.toFixed(2);
} catch(e) {
console.error("Error decoding audio for visualizer", e);
}
// Reset Playback UI
this.isPlaying = false;
this.updatePlayIcon();
document.getElementById('playhead').style.left = '0%';
}
togglePlayback() {
if(!this.currentRecordId) return;
if (this.isPlaying) {
this.audioPlayer.pause();
} else {
this.audioPlayer.play();
}
this.isPlaying = !this.isPlaying;
this.updatePlayIcon();
}
updatePlayIcon() {
const svg = document.getElementById('playIconSVG');
if (this.isPlaying) {
// Pause Icon
svg.innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
} else {
// Play Icon
svg.innerHTML = '<polygon points="5 3 19 12 5 21 5 3"/>';
}
}
updatePlayhead() {
const percent = (this.audioPlayer.currentTime / this.audioPlayer.duration) * 100;
document.getElementById('playhead').style.left = `${percent}%`;
document.getElementById('currentTimeDisplay').textContent = Utils.formatTime(this.audioPlayer.currentTime, true);
}
// --- EDITING & ACTIONS ---
async applyTrim() {
if(!this.currentAudioBuffer) return;
const start = parseFloat(document.getElementById('trimStart').value);
const end = parseFloat(document.getElementById('trimEnd').value);
if (start >= end || start < 0 || end > this.currentAudioBuffer.duration) {
Utils.showToast("Invalid trim parameters.");
return;
}
Utils.showToast("Processing audio trim...", 1000);
try {
const newWavBlob = await this.audioEngine.trimAudioBuffer(this.currentAudioBuffer, start, end);
// Update Database
const record = await this.db.getRecord(this.currentRecordId);
record.blob = newWavBlob;
record.duration = end - start;
record.name = record.name + " (Trimmed)";
await this.db.saveRecord(record);
Utils.showToast("Trim applied successfully!");
// Reload
await this.renderHistory();
this.loadRecordingIntoEditor(record);
} catch(e) {
console.error(e);
Utils.showToast("Failed to trim audio.");
}
}
async saveRename() {
if(!this.currentRecordId) return;
const newName = document.getElementById('editTitle').textContent.trim();
if(newName === "") return;
const record = await this.db.getRecord(this.currentRecordId);
if(record.name !== newName) {
record.name = newName;
await this.db.saveRecord(record);
Utils.showToast("Renamed successfully");
this.renderHistory();
}
}
async toggleFavoriteCurrent() {
if(!this.currentRecordId) return;
const record = await this.db.getRecord(this.currentRecordId);
record.favorite = !record.favorite;
await this.db.saveRecord(record);
this.renderHistory();
// Update UI instantly
const favBtn = document.getElementById('editorFavBtn');
if(record.favorite) {
favBtn.classList.add('active');
favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
} else {
favBtn.classList.remove('active');
favBtn.innerHTML = '<svg class="icon" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
}
}
downloadCurrent() {
if(!this.currentAudioUrl || !this.currentRecordId) return;
const title = document.getElementById('editTitle').textContent.trim();
const a = document.createElement('a');
a.href = this.currentAudioUrl;
// Try to infer type based on whether we trimmed it (trim outputs wav)
const ext = this.currentAudioUrl.includes('wav') ? '.wav' : '.webm';
a.download = `${title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_export${ext}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
Utils.showToast("Download started");
}
copyShareLink() {
const input = document.getElementById('shareLinkInput');
input.select();
document.execCommand('copy');
Utils.showToast("Local Blob Link Copied!");
}
// --- HISTORY & MODALS ---
async renderHistory(filterText = '') {
const records = await this.db.getAllRecords();
const list = document.getElementById('historyList');
list.innerHTML = '';
records.forEach(rec => {
if (filterText && !rec.name.toLowerCase().includes(filterText.toLowerCase())) return;
const div = document.createElement('div');
div.className = `history-item ${this.currentRecordId === rec.id ? 'active' : ''}`;
div.id = `hist_${rec.id}`;
const starIcon = rec.favorite ?
'<svg class="icon-sm" style="color:var(--brand-warning)" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>' : '';
div.innerHTML = `
<div class="history-info">
<span class="history-title">${rec.name}</span>
<span class="history-meta">
${Utils.formatTime(rec.duration)} • ${Utils.formatDate(rec.timestamp)}
${starIcon}
</span>
</div>
`;
div.onclick = () => this.loadRecordingIntoEditor(rec);
list.appendChild(div);
});
}
filterHistory(text) {
this.renderHistory(text);
}
promptDelete(id) {
if(!id) return;
this.itemToDelete = id;
const name = document.getElementById('editTitle').textContent;
document.getElementById('deleteItemName').textContent = name;
document.getElementById('deleteModal').classList.add('active');
}
closeModal() {
document.getElementById('deleteModal').classList.remove('active');
this.itemToDelete = null;
}
async executeDelete() {
if (this.itemToDelete) {
await this.db.deleteRecord(this.itemToDelete);
Utils.showToast("Recording deleted");
this.closeModal();
this.showRecordingConsole(); // go back to main view
this.renderHistory();
}
}
}
// Initialize App on Load
window.addEventListener('DOMContentLoaded', () => {
window.proAudioApp = new App();
});
</script>
</body>
</html>