Update 2025-04-24_11:44:19

This commit is contained in:
oib
2025-04-24 11:44:23 +02:00
commit e748c737f4
3408 changed files with 717481 additions and 0 deletions

429
static/app.js Normal file
View File

@ -0,0 +1,429 @@
// app.js — Frontend upload + minimal native player logic with slide-in and pulse effect
import { playBeep } from "./sound.js";
// 🔔 Minimal toast helper so calls to showToast() dont fail
function showToast(msg) {
const toast = document.createElement("div");
toast.className = "toast";
toast.textContent = msg;
toast.style.position = "fixed";
toast.style.bottom = "1.5rem";
toast.style.left = "50%";
toast.style.transform = "translateX(-50%)";
toast.style.background = "#333";
toast.style.color = "#fff";
toast.style.padding = "0.6em 1.2em";
toast.style.borderRadius = "6px";
toast.style.boxShadow = "0 2px 6px rgba(0,0,0,.2)";
toast.style.zIndex = 9999;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
document.addEventListener("DOMContentLoaded", () => {
// Guest vs. logged-in toggling is now handled by dashboard.js
// --- Public profile view logic ---
function showProfilePlayerFromUrl() {
const params = new URLSearchParams(window.location.search);
const profileUid = params.get("profile");
if (profileUid) {
const mePage = document.getElementById("me-page");
if (mePage) {
document.querySelectorAll("main > section").forEach(sec => sec.hidden = sec.id !== "me-page");
// Hide upload/delete/copy-url controls for guest view
const uploadArea = document.getElementById("upload-area");
if (uploadArea) uploadArea.hidden = true;
const copyUrlBtn = document.getElementById("copy-url");
if (copyUrlBtn) copyUrlBtn.style.display = "none";
const deleteBtn = document.getElementById("delete-account");
if (deleteBtn) deleteBtn.style.display = "none";
// Update heading and description for guest view
const meHeading = document.querySelector("#me-page h2");
if (meHeading) meHeading.textContent = `${profileUid}'s Stream 🎙️`;
const meDesc = document.querySelector("#me-page p");
if (meDesc) meDesc.textContent = `This is ${profileUid}'s public stream.`;
// Load playlist for the given profileUid
loadProfilePlaylist(profileUid);
}
}
}
// Run on popstate (SPA navigation and browser back/forward)
window.addEventListener('popstate', showProfilePlayerFromUrl);
async function loadProfilePlaylist(uid) {
const meAudio = document.getElementById("me-audio");
if (!meAudio) return;
const resp = await fetch(`/user-files/${encodeURIComponent(uid)}`);
const data = await resp.json();
if (!data.files || !Array.isArray(data.files) || data.files.length === 0) {
meAudio.src = "";
return;
}
// Shuffle playlist
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
window.mePlaylist = shuffle(data.files.map(f => `/audio/${uid}/${f}`));
window.mePlaylistIdx = 0;
const newSrc = window.mePlaylist[window.mePlaylistIdx];
meAudio.src = newSrc;
meAudio.load();
meAudio.play().catch(() => {/* autoplay may be blocked, ignore */});
}
window.loadProfilePlaylist = loadProfilePlaylist;
// --- Playlist for #me-page ---
const mePageLink = document.getElementById("show-me");
const meAudio = document.getElementById("me-audio");
const copyUrlBtn = document.getElementById("copy-url");
if (copyUrlBtn) copyUrlBtn.onclick = () => {
const uid = localStorage.getItem("uid");
if (uid) {
const streamUrl = `${window.location.origin}/stream/${encodeURIComponent(uid)}`;
navigator.clipboard.writeText(streamUrl);
showToast(`Copied your stream URL: ${streamUrl}`);
} else {
showToast("No user stream URL available");
}
};
let mePlaylist = [];
let mePlaylistIdx = 0;
// Playlist UI is hidden, so do not render
const mePrevBtn = document.getElementById("me-prev");
if (mePrevBtn) mePrevBtn.style.display = "none";
const meNextBtn = document.getElementById("me-next");
if (meNextBtn) meNextBtn.style.display = "none";
async function loadUserPlaylist() {
const uid = localStorage.getItem("uid");
if (!uid) return;
const resp = await fetch(`/user-files/${encodeURIComponent(uid)}`);
const data = await resp.json();
if (!data.files || !Array.isArray(data.files) || data.files.length === 0) {
meAudio.src = "";
return;
}
// Shuffle playlist
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
mePlaylist = shuffle(data.files.map(f => `/audio/${uid}/${f}`));
mePlaylistIdx = 0;
const newSrc = mePlaylist[mePlaylistIdx];
const prevSrc = meAudio.src;
const wasPlaying = !meAudio.paused && !meAudio.ended && meAudio.currentTime > 0;
const fullNewSrc = window.location.origin + newSrc;
if (prevSrc !== fullNewSrc) {
meAudio.src = newSrc;
meAudio.load();
} // else: do nothing, already loaded
// Don't call load() if already playing the correct file
// Don't call load() redundantly
// Don't set src redundantly
// This prevents DOMException from fetch aborts
}
if (mePageLink && meAudio) {
mePageLink.addEventListener("click", async () => {
await loadUserPlaylist();
// Start playback from current index
if (mePlaylist.length > 0) {
const newSrc = mePlaylist[mePlaylistIdx];
const prevSrc = meAudio.src;
const fullNewSrc = window.location.origin + newSrc;
if (prevSrc !== fullNewSrc) {
meAudio.src = newSrc;
meAudio.load();
}
meAudio.play();
}
});
meAudio.addEventListener("ended", () => {
if (mePlaylist.length > 1) {
mePlaylistIdx = (mePlaylistIdx + 1) % mePlaylist.length;
meAudio.src = mePlaylist[mePlaylistIdx];
meAudio.load();
meAudio.play();
} else if (mePlaylist.length === 1) {
// Only one file: restart
meAudio.currentTime = 0;
meAudio.load();
meAudio.play();
}
});
// Detect player stop and random play a new track
meAudio.addEventListener("pause", () => {
// Only trigger if playback reached the end and playlist has more than 1 track
if (meAudio.ended && mePlaylist.length > 1) {
let nextIdx;
do {
nextIdx = Math.floor(Math.random() * mePlaylist.length);
} while (nextIdx === mePlaylistIdx && mePlaylist.length > 1);
mePlaylistIdx = nextIdx;
meAudio.currentTime = 0;
meAudio.src = mePlaylist[mePlaylistIdx];
meAudio.load();
meAudio.play();
}
});
}
const deleteBtn = document.getElementById("delete-account");
if (deleteBtn) deleteBtn.onclick = async () => {
if (!confirm("Are you sure you want to delete your account and all uploaded audio?")) return;
const res = await fetch("/delete-account", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uid })
});
if (res.ok) {
showToast("✅ Account deleted");
localStorage.removeItem("uid");
setTimeout(() => window.location.reload(), 2000);
} else {
const msg = (await res.json()).detail || res.status;
showToast("❌ Delete failed: " + msg);
}
};
const fadeAllSections = () => {
const uid = localStorage.getItem('uid');
document.querySelectorAll("main > section").forEach(section => {
// Always keep upload-area visible for logged-in users
if (uid && section.id === 'upload-area') return;
if (!section.hidden) {
section.classList.add("fade-out");
setTimeout(() => {
section.classList.remove("fade-out");
section.hidden = true;
}, 300);
}
});
};
const dropzone = document.getElementById("upload-area");
dropzone.setAttribute("aria-label", "Upload area. Click or drop an audio file to upload.");
const fileInput = document.getElementById("fileInput");
const fileInfo = document.createElement("div");
fileInfo.id = "file-info";
fileInfo.style.textAlign = "center";
fileInput.parentNode.insertBefore(fileInfo, fileInput.nextSibling);
const streamInfo = document.getElementById("stream-info");
const streamUrlEl = document.getElementById("streamUrl");
const status = document.getElementById("status");
const spinner = document.getElementById("spinner");
const uid = localStorage.getItem("uid");
const uidTime = parseInt(localStorage.getItem("uid_time"), 10);
const now = Date.now();
// Hide register button if logged in
const registerBtn = document.getElementById("show-register");
if (uid && localStorage.getItem("confirmed_uid") === uid && uidTime && (now - uidTime) < 3600000) {
if (registerBtn) registerBtn.style.display = "none";
} else {
if (registerBtn) registerBtn.style.display = "";
}
if (!uid || !uidTime || (now - uidTime) > 3600000) {
localStorage.removeItem("uid");
localStorage.removeItem("confirmed_uid");
localStorage.removeItem("uid_time");
status.className = "error-toast";
status.innerText = "❌ Session expired. Please log in again.";
// Add Login or Register button only for this error
let loginBtn = document.createElement('button');
loginBtn.textContent = 'Login or Register';
loginBtn.className = 'login-register-btn';
loginBtn.onclick = () => {
document.querySelectorAll('main > section').forEach(sec => sec.hidden = sec.id !== 'register-page');
};
status.appendChild(document.createElement('br'));
status.appendChild(loginBtn);
// Remove the status div after a short delay so only toast remains
setTimeout(() => {
if (status.parentNode) status.parentNode.removeChild(status);
}, 100);
return;
}
const confirmed = localStorage.getItem("confirmed_uid");
if (!confirmed || uid !== confirmed) {
status.className = "error-toast";
status.innerText = "❌ Please confirm your account via email first.";
showToast(status.innerText);
return;
}
let abortController;
const upload = async (file) => {
if (abortController) abortController.abort();
abortController = new AbortController();
fileInfo.innerText = `📁 ${file.name}${(file.size / 1024 / 1024).toFixed(2)} MB`;
if (file.size > 100 * 1024 * 1024) {
status.className = "error-toast";
status.innerText = "❌ Session expired. Please log in again.";
// Add Login or Register button only for this error
let loginBtn = document.createElement('button');
loginBtn.textContent = 'Login or Register';
loginBtn.className = 'login-register-btn';
loginBtn.onclick = () => {
document.querySelectorAll('main > section').forEach(sec => sec.hidden = sec.id !== 'register-page');
};
status.appendChild(document.createElement('br'));
status.appendChild(loginBtn);
showToast(status.innerText);
return;
}
spinner.style.display = "block";
status.innerHTML = '📡 Uploading…';
status.className = "uploading-toast";
fileInput.disabled = true;
dropzone.classList.add("uploading");
const formData = new FormData();
formData.append("uid", uid);
formData.append("file", file);
const res = await fetch("/upload", {
signal: abortController.signal,
method: "POST",
body: formData,
});
let data, parseError;
try {
data = await res.json();
} catch (e) {
parseError = e;
}
if (!data) {
status.className = "error-toast";
status.innerText = "❌ Upload failed: " + (parseError && parseError.message ? parseError.message : "Unknown error");
showToast(status.innerText);
spinner.style.display = "none";
fileInput.disabled = false;
dropzone.classList.remove("uploading");
return;
}
if (res.ok) {
status.className = "success-toast";
streamInfo.hidden = false;
streamInfo.innerHTML = `
<p>Your stream is now live:</p>
<audio controls id="me-audio" aria-label="Stream audio player. Your uploaded voice loop plays here.">
<p style='font-size: 0.9em; text-align: center;'>🔁 This stream loops forever</p>
<source src="${data.stream_url}" type="audio/ogg">
</audio>
<p><a href="${data.stream_url}" target="_blank" class="button" aria-label="Open your stream in a new tab">Open in external player</a></p>
`;
const meAudio = document.getElementById("me-audio");
meAudio.addEventListener("ended", () => {
if (mePlaylist.length > 1) {
mePlaylistIdx = (mePlaylistIdx + 1) % mePlaylist.length;
meAudio.src = mePlaylist[mePlaylistIdx];
meAudio.load();
meAudio.play();
meUrl.value = mePlaylist[mePlaylistIdx];
} else if (mePlaylist.length === 1) {
// Only one file: restart
meAudio.currentTime = 0;
meAudio.load();
meAudio.play();
}
});
if (data.quota && data.quota.used_mb !== undefined) {
const bar = document.getElementById("quota-bar");
const text = document.getElementById("quota-text");
const quotaSec = document.getElementById("quota-meter");
if (bar && text && quotaSec) {
quotaSec.hidden = false;
const used = parseFloat(data.quota.used_mb);
bar.value = used;
bar.max = 100;
text.textContent = `${used.toFixed(1)} MB used`;
}
}
spinner.style.display = "none";
fileInput.disabled = false;
dropzone.classList.remove("uploading");
showToast(status.innerText);
status.innerText = "✅ Upload successful.";
playBeep(432, 0.25, "sine");
setTimeout(() => status.innerText = "", 5000);
streamInfo.classList.add("visible", "slide-in");
} else {
streamInfo.hidden = true;
status.className = "error-toast";
spinner.style.display = "none";
if ((data.detail || data.error || "").includes("music")) {
status.innerText = "🎵 Upload rejected: singing or music detected.";
} else {
status.innerText = `❌ Upload failed: ${data.detail || data.error}`;
}
showToast(status.innerText);
fileInput.value = null;
dropzone.classList.remove("uploading");
fileInput.disabled = false;
streamInfo.classList.remove("visible", "slide-in");
}
};
dropzone.addEventListener("click", () => {
console.log("[DEBUG] Dropzone clicked");
fileInput.click();
console.log("[DEBUG] fileInput.click() called");
});
dropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dropzone.classList.add("dragover");
dropzone.style.transition = "background-color 0.3s ease";
});
dropzone.addEventListener("dragleave", () => {
dropzone.classList.remove("dragover");
});
dropzone.addEventListener("drop", (e) => {
dropzone.classList.add("pulse");
setTimeout(() => dropzone.classList.remove("pulse"), 400);
e.preventDefault();
dropzone.classList.remove("dragover");
const file = e.dataTransfer.files[0];
if (file) upload(file);
});
fileInput.addEventListener("change", (e) => {
status.innerText = "";
status.className = "";
const file = e.target.files[0];
if (file) upload(file);
});
document.querySelectorAll('#links a[data-target]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = link.getAttribute('data-target');
// Only hide other sections when not opening #me-page
if (target !== 'me-page') fadeAllSections();
const section = document.getElementById(target);
if (section) {
section.hidden = false;
section.classList.add("slide-in");
section.scrollIntoView({ behavior: "smooth" });
}
const burger = document.getElementById('burger-toggle');
if (burger && burger.checked) burger.checked = false;
});
});
});