First commit

This commit is contained in:
Joakim Svensson
2025-12-15 22:15:53 +01:00
commit 119f979cb6
2 changed files with 840 additions and 0 deletions

840
index.html Normal file
View File

@@ -0,0 +1,840 @@
<!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>

BIN
mtvify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB