Files
mtvify/index.html
Joakim Svensson 119f979cb6 First commit
2025-12-15 22:15:53 +01:00

841 lines
21 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="sv">
<head>
<meta charset="UTF-8">
<title>MTVify</title>
<style>
body {
margin: 0;
background: #000;
font-family: Arial, sans-serif;
color: #fff;
overflow: hidden;
}
#player {
width: 100vw;
height: 100vh;
}
/* ==== NOW PLAYING OVERLAY — flyttad till övre vänstra hörnet ==== */
#overlay {
position: absolute;
top: 0; /* ÄNDRAT: tidigare bottom:0 */
left: 0;
width: 100%;
pointer-events: none;
z-index: 500;
}
.overlay-inner {
display: inline-flex;
align-items: center;
gap: 16px;
/* Ny overlay-positionering för top-left */
padding: 14px 30px;
margin: 20px 0 0 20px; /* ÄNDRAT: tidigare margin-bottom */
background: linear-gradient(90deg, #ff2fb0, #ffd300);
border-radius: 999px;
border: 2px solid rgba(0,0,0,0.7);
box-shadow: 0 0 28px rgba(255, 0, 160, 0.65);
transform: translateY(-160%); /* ÄNDRAT: tidigare +140% */
opacity: 0;
transition: transform 0.45s ease-out, opacity 0.45s ease-out;
}
#overlay.visible .overlay-inner {
transform: translateY(0);
opacity: 1;
animation: neonPulse 1.7s ease-in-out infinite;
}
.overlay-logo {
height: 56px;
width: auto;
filter: drop-shadow(0 0 6px rgba(0,0,0,0.7));
}
.overlay-text {
display: flex;
flex-direction: column;
justify-content: center;
}
.overlay-label {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #1b0020;
opacity: 0.9;
}
.overlay-title {
font-size: 34px;
font-weight: 800;
color: #ffffff;
text-shadow: 0 0 6px rgba(0,0,0,0.9);
white-space: nowrap;
max-width: 70vw;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes neonPulse {
0% { box-shadow: 0 0 14px rgba(255, 0, 160, 0.6); }
50% { box-shadow: 0 0 26px rgba(255, 255, 255, 0.95); }
100% { box-shadow: 0 0 14px rgba(255, 0, 160, 0.6); }
}
/* ==== PANEL / KONTROLLER ==== */
#panel {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.7);
padding: 10px;
width: 340px;
border-radius: 4px;
z-index: 600;
}
#panel.hidden {
display: none;
}
#togglePanel {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: #fff;
padding: 8px 12px;
border: 1px solid #666;
cursor: pointer;
z-index: 700;
font-size: 13px;
transition: opacity 0.5s ease;
}
#togglePanel.hiddenAuto {
opacity: 0;
pointer-events: none;
}
#panel input[type="text"] {
width: 100%;
padding: 6px;
margin-bottom: 6px;
box-sizing: border-box;
}
#panel button {
padding: 6px;
margin: 2px;
background: #444;
color: white;
border: 1px solid #666;
cursor: pointer;
font-size: 13px;
}
#panel h3 {
margin: 10px 0 6px 0;
font-size: 16px;
}
.slider-row {
display: flex;
align-items: center;
margin: 4px 0;
}
.slider-row label {
width: 20px;
margin-right: 5px;
font-size: 14px;
}
.slider-row input[type=range] {
flex-grow: 1;
margin-right: 8px;
}
.percent {
width: 35px;
text-align: right;
font-size: 13px;
}
.list {
font-size: 14px;
background: #111;
padding: 5px;
max-height: 150px;
overflow-y: auto;
border: 1px solid #333;
margin-bottom: 10px;
}
.song {
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px solid #333;
}
.song-title {
margin-bottom: 3px;
}
.song label {
margin-right: 6px;
font-size: 12px;
}
.song-actions {
margin-top: 3px;
}
.song-actions button {
padding: 3px 6px;
font-size: 11px;
margin-right: 4px;
}
#importFile { display: none; }
/* ==== IDENT / BUMPER ==== */
#identOverlay {
position: absolute;
inset: 0;
background: radial-gradient(circle at top left, #ff2fb0 0%, #2b0b60 35%, #050015 80%);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.4s ease-out;
z-index: 800;
overflow: hidden;
}
#identOverlay.visible {
opacity: 1;
pointer-events: auto;
}
.ident-inner {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 24px 36px;
border-radius: 24px;
background: rgba(0,0,0,0.55);
box-shadow: 0 0 40px rgba(0, 0, 0, 0.9);
overflow: hidden;
}
.ident-inner::before,
.ident-inner::after {
content: "";
position: absolute;
width: 160%;
height: 4px;
background: linear-gradient(90deg, #00e5ff, #ff2fb0, #ffd300);
opacity: 0.6;
transform: rotate(-18deg);
left: -30%;
animation: identStripes 2.2s linear infinite;
}
.ident-inner::after {
top: auto;
bottom: 18%;
animation-delay: 0.6s;
}
@keyframes identStripes {
0% { transform: translateX(0) rotate(-18deg); }
100% { transform: translateX(40%) rotate(-18deg); }
}
.ident-logo {
height: 100px;
width: auto;
filter: drop-shadow(0 0 10px rgba(0,0,0,0.9));
animation: identLogo 1.3s ease-out forwards;
}
@keyframes identLogo {
0% { transform: scale(0.3) rotate(-10deg); opacity: 0; }
50% { transform: scale(1.1) rotate(3deg); opacity: 1; }
100% { transform: scale(1.0) rotate(0deg); opacity: 1; }
}
.ident-text {
font-size: 24px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #ffffff;
text-shadow: 0 0 8px rgba(0,0,0,0.9);
}
.ident-tagline {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.16em;
color: #ffdfff;
opacity: 0.9;
}
</style>
</head>
<body>
<div id="togglePanel" onclick="togglePanel()">Visa / Dölj panel</div>
<div id="player"></div>
<!-- NOW PLAYING overlay (nu top-left) -->
<div id="overlay">
<div class="overlay-inner">
<img src="mtvify.png" alt="MTVify" class="overlay-logo">
<div class="overlay-text">
<div class="overlay-label">NOW PLAYING</div>
<div class="overlay-title" id="overlayTitle">Artist Titel</div>
</div>
</div>
</div>
<!-- IDENT -->
<div id="identOverlay">
<div class="ident-inner">
<img src="mtvify.png" alt="MTVify" class="ident-logo">
<div class="ident-text">MTVIFY</div>
<div class="ident-tagline">non-stop music videos</div>
</div>
</div>
<!-- PANEL (oförändrat) -->
<div id="panel" class="hidden">
<h3>Lägg till låt</h3>
<input id="urlInput" type="text" placeholder="YouTube-länk eller ID">
<div>
<button onclick="addSongTo('A')">Lägg i A</button>
<button onclick="addSongTo('B')">Lägg i B</button>
<button onclick="addSongTo('C')">Lägg i C</button>
</div>
<h3>Viktning</h3>
<div class="slider-row">
<label>A:</label>
<input type="range" id="weightA" min="0" max="100" value="60" oninput="updateWeights()">
<div class="percent" id="weightAVal">60%</div>
</div>
<div class="slider-row">
<label>B:</label>
<input type="range" id="weightB" min="0" max="100" value="30" oninput="updateWeights()">
<div class="percent" id="weightBVal">30%</div>
</div>
<div class="slider-row">
<label>C:</label>
<input type="range" id="weightC" min="0" max="100" value="10" oninput="updateWeights()">
<div class="percent" id="weightCVal">10%</div>
</div>
<div class="toolbar">
<button onclick="exportPlaylists()">Exportera</button>
<button onclick="document.getElementById('importFile').click()">Importera</button>
<button onclick="clearAll()">Rensa allt</button>
<input type="file" id="importFile" accept="application/json" onchange="importPlaylists(event)">
</div>
<h3>Lista A</h3>
<div class="list" id="listA"></div>
<h3>Lista B</h3>
<div class="list" id="listB"></div>
<h3>Lista C</h3>
<div class="list" id="listC"></div>
</div>
<script src="https://www.youtube.com/iframe_api"></script>
<script>
/* ============================
DATA & STORAGE
============================ */
let lists = { A: [], B: [], C: [] };
let weights = { A: 0.6, B: 0.3, C: 0.1 };
if (localStorage.getItem("mtvify")) {
try { lists = JSON.parse(localStorage.getItem("mtvify")); } catch(e) {}
}
if (localStorage.getItem("mtvify-weights")) {
try {
weights = JSON.parse(localStorage.getItem("mtvify-weights"));
const wA = Math.round(weights.A * 100);
const wB = Math.round(weights.B * 100);
const wC = Math.round(weights.C * 100);
document.getElementById("weightA").value = wA;
document.getElementById("weightB").value = wB;
document.getElementById("weightC").value = wC;
document.getElementById("weightAVal").innerText = wA + "%";
document.getElementById("weightBVal").innerText = wB + "%";
document.getElementById("weightCVal").innerText = wC + "%";
} catch(e) {}
}
function save() {
localStorage.setItem("mtvify", JSON.stringify(lists));
localStorage.setItem("mtvify-weights", JSON.stringify(weights));
}
/* ============================
AUTO-HIDE TOGGLE BUTTON
============================ */
let hideTimer = null;
function resetHideTimer() {
const btn = document.getElementById("togglePanel");
btn.classList.remove("hiddenAuto");
if (hideTimer) clearTimeout(hideTimer);
hideTimer = setTimeout(() => {
btn.classList.add("hiddenAuto");
}, 3000);
}
resetHideTimer();
document.addEventListener("mousemove", resetHideTimer);
document.addEventListener("mousedown", resetHideTimer);
document.addEventListener("keydown", resetHideTimer);
/* ============================
PANEL
============================ */
function togglePanel() {
document.getElementById("panel").classList.toggle("hidden");
}
/* ============================
UI RENDER LISTS
============================ */
function updateUI() {
["A","B","C"].forEach(L => {
const el = document.getElementById("list" + L);
el.innerHTML = lists[L].map(song => `
<div class="song">
<div class="song-title">${song.title}</div>
<div>
<label><input type="radio" name="${song.id}" ${L === "A" ? "checked" : ""} onclick="moveSong('${song.id}','A')"> A</label>
<label><input type="radio" name="${song.id}" ${L === "B" ? "checked" : ""} onclick="moveSong('${song.id}','B')"> B</label>
<label><input type="radio" name="${song.id}" ${L === "C" ? "checked" : ""} onclick="moveSong('${song.id}','C')"> C</label>
</div>
<div class="song-actions">
<button onclick="editSongTitle('${song.id}')">Ändra titel</button>
<button onclick="deleteSong('${song.id}')">Ta bort</button>
</div>
</div>
`).join("");
});
}
updateUI();
/* ============================
WEIGHTS (SLIDERS)
============================ */
function updateWeights() {
let a = parseFloat(document.getElementById("weightA").value);
let b = parseFloat(document.getElementById("weightB").value);
let c = parseFloat(document.getElementById("weightC").value);
let total = a + b + c;
if (total === 0) total = 1;
weights.A = a / total;
weights.B = b / total;
weights.C = c / total;
document.getElementById("weightAVal").innerText = Math.round(weights.A * 100) + "%";
document.getElementById("weightBVal").innerText = Math.round(weights.B * 100) + "%";
document.getElementById("weightCVal").innerText = Math.round(weights.C * 100) + "%";
save();
}
/* ============================
LIST HANDLING
============================ */
function moveSong(id, targetList) {
for (let L of ["A","B","C"]) {
const idx = lists[L].findIndex(s => s.id === id);
if (idx !== -1) {
const song = lists[L][idx];
lists[L].splice(idx, 1);
if (!lists[targetList].some(s => s.id === id)) {
lists[targetList].push(song);
}
save();
updateUI();
return;
}
}
}
function editSongTitle(id) {
for (let L of ["A","B","C"]) {
const s = lists[L].find(song => song.id === id);
if (s) {
const newTitle = prompt("Ny titel:", s.title);
if (newTitle && newTitle.trim() !== "") {
s.title = newTitle.trim();
save();
updateUI();
}
return;
}
}
}
function deleteSong(id) {
if (!confirm("Ta bort denna video från alla listor?")) return;
["A","B","C"].forEach(L => {
lists[L] = lists[L].filter(song => song.id !== id);
});
save();
updateUI();
}
/* ============================
ADD SONG + TITLE FETCH
============================ */
function extractID(url) {
if (url.includes("youtube.com")) {
try { return new URL(url).searchParams.get("v"); } catch(e) {}
}
if (url.includes("youtu.be"))
return url.split("/").pop().split("?")[0];
return url;
}
function addSongTo(listName) {
const input = document.getElementById("urlInput");
const url = input.value.trim();
if (!url) return;
const id = extractID(url);
if (!id) return;
if (lists[listName].some(s => s.id === id)) {
input.value = "";
return;
}
lists[listName].push({ id, title: "Laddar titel…" });
save();
updateUI();
input.value = "";
fetchRealTitle(id, real => {
["A","B","C"].forEach(L => {
const s = lists[L].find(x => x.id === id);
if (s) s.title = real;
});
save();
updateUI();
});
}
function fetchRealTitle(id, callback) {
if (typeof YT === "undefined" || !YT.Player) {
callback(id);
return;
}
const miniDiv = document.createElement("div");
miniDiv.style.width = "1px";
miniDiv.style.height = "1px";
miniDiv.style.overflow = "hidden";
miniDiv.style.position = "absolute";
miniDiv.style.bottom = "0";
miniDiv.style.left = "0";
miniDiv.style.zIndex = "1";
document.body.appendChild(miniDiv);
const mini = new YT.Player(miniDiv, {
height: "1",
width: "1",
videoId: id,
playerVars: { controls: 0, autoplay: 0, iv_load_policy: 3 },
events: {
onReady: function() {
setTimeout(() => {
let title = id;
try {
const data = mini.getVideoData();
if (data && data.title) title = data.title;
} catch(e) {}
callback(title);
mini.destroy();
miniDiv.remove();
}, 500);
}
}
});
}
/* ============================
IMPORT / EXPORT / CLEAR
============================ */
function exportPlaylists() {
const data = JSON.stringify(lists, null, 2);
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "mtvify-playlists.json";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function importPlaylists(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const imported = JSON.parse(e.target.result);
["A","B","C"].forEach(L => {
if (Array.isArray(imported[L])) {
imported[L].forEach(song => {
if (!song || !song.id) return;
if (!lists[L].some(s => s.id === song.id)) {
lists[L].push({
id: song.id,
title: song.title || song.id
});
}
});
}
});
save();
updateUI();
} catch (err) {
console.error("Fel vid import", err);
} finally {
event.target.value = "";
}
};
reader.readAsText(file);
}
function clearAll() {
if (!confirm("Rensa alla listor permanent?")) return;
lists = { A: [], B: [], C: [] };
save();
updateUI();
}
/* ============================
PLAYBACK HISTORY
============================ */
let playHistory = [];
const historyLimit = 3;
function registerPlayed(id) {
playHistory.push(id);
if (playHistory.length > historyLimit) {
playHistory.shift();
}
}
/* ============================
PICK NEXT
============================ */
function pickNext() {
for (let attempt = 0; attempt < 30; attempt++) {
const r = Math.random();
const a = weights.A;
const b = weights.B;
let pick;
if (r < a) pick = pickFrom("A");
else if (r < a + b) pick = pickFrom("B");
else pick = pickFrom("C");
if (!pick) continue;
if (!playHistory.includes(pick.id))
return pick;
}
return pickFrom("A") || pickFrom("B") || pickFrom("C");
}
function pickFrom(L) {
if (lists[L].length === 0) return null;
return lists[L][Math.floor(Math.random() * lists[L].length)];
}
/* ============================
PLAYER + IDENT + END-SKIP
============================ */
let player;
let endWatchInterval = null;
let earlyHandled = false;
function showIdent() {
const el = document.getElementById("identOverlay");
el.classList.add("visible");
}
function hideIdent() {
const el = document.getElementById("identOverlay");
el.classList.remove("visible");
}
function startTrack(track) {
if (!player || !track) return;
player.loadVideoById(track.id);
player.playVideo();
showOverlay(track.title);
}
function showIdentAndMaybePlay(track, isFirst, forceIdent) {
const shouldShow = isFirst || forceIdent;
if (shouldShow) {
showIdent();
setTimeout(() => {
startTrack(track);
hideIdent();
}, 2500);
} else {
startTrack(track);
}
}
function handleSongFinished(isFirst = false) {
const next = pickNext();
if (!next) return;
registerPlayed(next.id);
const forceIdent = Math.random() < 0.05;
showIdentAndMaybePlay(next, false, forceIdent);
}
function onYouTubeIframeAPIReady() {
const first = pickNext() || { id: "dQw4w9WgXcQ", title: "Fallback" };
registerPlayed(first.id);
player = new YT.Player("player", {
height: "100%",
width: "100%",
videoId: first.id,
playerVars: {
autoplay: 0,
controls: 0,
modestbranding: 1,
rel: 0,
fs: 0,
iv_load_policy: 3,
disablekb: 1
},
events: {
onReady: () => {
showIdentAndMaybePlay(first, true, true);
},
onStateChange: onPlayerStateChange
}
});
}
function onPlayerStateChange(e) {
if (e.data === YT.PlayerState.PLAYING) {
earlyHandled = false;
if (endWatchInterval) clearInterval(endWatchInterval);
endWatchInterval = setInterval(() => {
if (!player || typeof player.getDuration !== "function") return;
const dur = player.getDuration();
const ct = player.getCurrentTime();
if (dur > 0 && dur - ct <= 4 && !earlyHandled) {
earlyHandled = true;
clearInterval(endWatchInterval);
endWatchInterval = null;
handleSongFinished(false);
}
}, 500);
} else if (e.data === YT.PlayerState.ENDED) {
if (endWatchInterval) {
clearInterval(endWatchInterval);
endWatchInterval = null;
}
if (!earlyHandled) {
handleSongFinished(false);
}
} else if (e.data === YT.PlayerState.PAUSED || e.data === YT.PlayerState.BUFFERING) {
if (endWatchInterval) {
clearInterval(endWatchInterval);
endWatchInterval = null;
}
}
}
/* ============================
NOW PLAYING OVERLAY
============================ */
function showOverlay(title) {
const overlay = document.getElementById("overlay");
const titleEl = document.getElementById("overlayTitle");
if (titleEl) {
titleEl.textContent = title;
}
overlay.classList.add("visible");
clearTimeout(showOverlay._timer);
showOverlay._timer = setTimeout(() => {
overlay.classList.remove("visible");
}, 5000);
}
</script>
</body>
</html>