SABRe-v2 / templates /index.html
Lguyogiro's picture
make style more similar to mdc
0ceef35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SABRe: Simple Audio Book Recorder</title>
<link href="https://fonts.googleapis.com/css2?family=Mozilla+Text:wght@200..700&display=swap" rel="stylesheet">
<style>
/* Mozilla Protocol Design System Variables */
:root {
--moz-black: #000000;
--moz-white: #ffffff;
--moz-orange: #ff4f00;
--moz-grey-light: #f9f9fa;
--moz-grey-border: #cfcfd8;
--moz-grey-dark: #4a4a4f;
}
body {
font-family: "Inter", "Nunito Sans", Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background-color: var(--moz-white);
color: var(--moz-black);
-webkit-font-smoothing: antialiased;
}
h1 {
font-family: "Zilla Slab", serif;
font-weight: 900; /* Extra bold for that 'darker' look */
font-size: 3.5rem; /* Significantly larger */
text-transform: uppercase;
letter-spacing: -0.02em;
margin-bottom: 0.2em;
color: var(--moz-black);
display: inline-block; /* Wraps the underline to the text width */
border-bottom: 8px solid var(--moz-orange); /* Thick orange underline */
line-height: 1.1;
padding-bottom: 5px;
}
h1 span {
color: var(--moz-orange);
}
hr {
border: 0;
height: 1px;
background: var(--moz-grey-border);
margin: 20px auto;
max-width: 900px;
}
/* Container for content */
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 20px;
}
/* Stats Box - White with grey outline, orange hover */
.stats-box {
text-align: center;
padding: 20px;
background: var(--moz-white);
color: var(--moz-grey-dark);
margin: 20px auto;
display: block;
border: 1px solid var(--moz-grey-border);
transition: all 0.2s ease-in-out;
max-width: 400px;
}
.stats {
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Reading Area - Minimalist */
#sentence {
font-family: "Zilla Slab", serif;
font-size: 1.5rem;
margin: 40px auto;
padding: 60px 40px;
background-color: var(--moz-white);
border: 1px solid var(--moz-grey-border);
min-height: 150px;
max-width: 800px;
}
/* Protocol Buttons */
button {
background: var(--moz-black);
color: var(--moz-white);
border: 1px solid var(--moz-black);
padding: 12px 24px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
margin: 5px;
}
button:hover:not(:disabled) {
background: var(--moz-grey-dark);
}
/* The 'Record' button is often the primary action in your app */
#recordBtn {
background-color: var(--moz-orange);
border-color: var(--moz-orange);
}
#recordBtn:hover:not(:disabled) {
background-color: #bf3b00;
}
button:disabled {
background-color: var(--moz-grey-border);
border-color: var(--moz-grey-border);
color: #888;
cursor: not-allowed;
}
/* Form styling - Clean Mozilla Look */
#uploadForm {
padding: 40px;
border: 1px solid var(--moz-grey-border);
background: var(--moz-grey-light);
max-width: 500px;
margin: 20px auto;
}
#uploadForm button {
background: var(--moz-black);
margin-top: 20px;
}
/* Download/Delete section */
.secondary-actions {
margin-top: 50px;
padding: 30px;
background: var(--moz-grey-light);
border-top: 1px solid var(--moz-grey-border);
}
#downloadBtn {
background: transparent;
color: var(--moz-black);
border: 1px solid var(--moz-black);
}
#downloadBtn:hover {
background: var(--moz-black);
color: var(--moz-white);
}
.delete-btn {
background-color: transparent;
color: #d70022; /* Mozilla Red */
border: 1px solid #d70022;
}
.delete-btn:hover {
background-color: #d70022;
color: var(--moz-white);
}
audio {
margin-top: 20px;
filter: brightness(0.95);
}
</style>
</head>
<body style="text-align: center; margin-left: 10em; margin-right: 10em">
<h1>SABRe: Simple Audio Book Recorder</h1>
<div style="background: #e1b7e0">
<hr>
<div class="stats-box">
<span class="stats" id="sentCntDisplay">Sentences recorded: 0 / 0</span><br>
<span class="stats" id="durationDisplay">Total duration: 0 seconds</span>
</div>
<form id="uploadForm" enctype="multipart/form-data">
<p>Upload a .txt file (one sentence per line) to begin.</p>
<input type="file" name="file" id="fileInput" accept=".txt" required>
<button type="submit">Upload selected file</button>
</form>
<div id="recorder" style="display: none;">
<div id="sentence"></div>
<div id="controls">
<button id="recordBtn">Record</button>
<button id="stopBtn" disabled>Stop</button>
<button id="nextBtn" disabled>Next Sentence</button>
</div>
<audio id="audioPlayback" controls style="display:none; margin-top:20px; margin-left: auto; margin-right: auto;"></audio>
</div>
<hr>
</div>
<div style="display: flex; justify-content: center; gap: 20px; margin-top: 30px;">
<button id="downloadBtn">Download Recordings (.zip)</button>
<button id="deleteBtn" class="delete-btn">Delete All Data</button>
</div>
<script>
let sentences = [];
let recordedIndices = new Set();
let current = 0;
let startTime, stopTime;
let totalTime = 0;
let mediaRecorder, audioChunks = [];
async function loadSession() {
const res = await fetch('/get-session');
const data = await res.json();
if (data.sentences && data.sentences.length > 0) {
sentences = data.sentences;
recordedIndices = new Set(data.recorded_indices.map(Number));
// Find first unrecorded
let firstIncomplete = sentences.findIndex((_, i) => !recordedIndices.has(i));
current = firstIncomplete !== -1 ? firstIncomplete : sentences.length - 1;
document.getElementById('recorder').style.display = 'block';
document.getElementById('uploadForm').style.display = 'none';
updateStats();
showSentence();
}
}
window.onload = loadSession;
document.getElementById('uploadForm').onsubmit = async function(e) {
e.preventDefault();
let form = new FormData();
form.append('file', document.getElementById('fileInput').files[0]);
let res = await fetch('/upload-sentences', { method: 'POST', body: form });
sentences = await res.json();
current = 0;
recordedIndices.clear();
updateStats();
showSentence();
document.getElementById('recorder').style.display = 'block';
document.getElementById('uploadForm').style.display = 'none';
};
document.getElementById('recordBtn').onclick = async function() {
let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
mediaRecorder.onstop = async function() {
let blob = new Blob(audioChunks, { type: 'audio/webm' });
let form = new FormData();
form.append('audio', blob, `sentence_${current}.webm`);
form.append('sentence_idx', current);
form.append("sentence_text", sentences[current]);
const res = await fetch('/upload-audio', { method: 'POST', body: form });
if(res.ok) {
recordedIndices.add(current);
updateStats();
showSentence();
document.getElementById('audioPlayback').src = URL.createObjectURL(blob);
document.getElementById('audioPlayback').style.display = 'block';
document.getElementById('nextBtn').disabled = false;
}
};
mediaRecorder.start();
startTime = performance.now();
document.getElementById('recordBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
};
document.getElementById('stopBtn').onclick = function() {
mediaRecorder.stop();
stopTime = performance.now();
totalTime += (stopTime - startTime);
document.getElementById('recordBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
};
document.getElementById('nextBtn').onclick = function() {
if (current + 1 < sentences.length) {
current++;
showSentence();
} else {
alert("All sentences recorded!");
}
};
document.getElementById('deleteBtn').onclick = async function() {
if (confirm("Delete all data? This cannot be undone.")) {
await fetch('/delete-data', { method: 'POST' });
location.reload();
}
};
document.getElementById('downloadBtn').onclick = () => window.location.href = '/download-recordings';
function showSentence() {
const container = document.getElementById('sentence');
container.innerHTML = '';
const createP = (text, color, bold, size) => {
const p = document.createElement("p");
p.style.color = color;
if(bold) p.style.fontWeight = "bold";
if(size) p.style.fontSize = size;
p.innerText = text;
return p;
};
container.appendChild(createP("previous: " + (current > 0 ? sentences[current-1] : "None"), "grey"));
container.appendChild(createP((recordedIndices.has(current) ? "✅ " : "🎤 ") + sentences[current], "black", true, "1.4em"));
container.appendChild(createP("next: " + (current < sentences.length - 1 ? sentences[current+1] : "End"), "grey"));
document.getElementById('nextBtn').disabled = !recordedIndices.has(current);
document.getElementById('audioPlayback').style.display = 'none';
}
function updateStats() {
document.getElementById("sentCntDisplay").textContent = `Sentences recorded: ${recordedIndices.size} / ${sentences.length}`;
document.getElementById("durationDisplay").textContent = `Total duration: ${(totalTime/1000).toFixed(1)} seconds`;
}
</script>
</body>
</html>