Initial commit — Meridian OS browser desktop demo

This commit is contained in:
2026-04-19 23:18:55 +00:00
commit 8e30dd3597
30 changed files with 6277 additions and 0 deletions

248
js/apps/music.js Normal file
View File

@@ -0,0 +1,248 @@
// Music player — three tracks, play/pause/skip/seek/volume.
const TRACKS = [
{ title: 'Grace Ascent', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2022/10/18/audio_31c1679d6c.mp3' },
{ title: 'Siege Breaker', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2022/11/17/audio_febc508520.mp3' },
{ title: 'Final Whistle', artist: 'Meridian OST', src: 'https://cdn.pixabay.com/audio/2023/06/13/audio_2c8e3b31b0.mp3' },
];
export default {
id: 'music',
title: 'Music',
render(container, api) {
const audio = new Audio();
let currentTrack = 0;
let playedSeconds = 0;
let playSecondsTimer = null;
const root = document.createElement('div');
root.style.cssText = 'height:100%;display:flex;overflow:hidden;';
// Left panel: track list
const listPanel = document.createElement('div');
listPanel.style.cssText = 'width:220px;background:var(--bg-2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto;flex-shrink:0;';
const trackList = TRACKS.map((track, i) => {
const row = document.createElement('div');
row.style.cssText = 'padding:10px 14px;cursor:pointer;display:flex;flex-direction:column;border-left:2px solid transparent;border-bottom:1px solid rgba(255,255,255,0.04);transition:all 150ms ease-out;';
const titleSpan = document.createElement('div');
titleSpan.style.cssText = 'font-size:13px;font-weight:500;color:var(--text);';
titleSpan.textContent = track.title;
const artistSpan = document.createElement('div');
artistSpan.style.cssText = 'font-size:11px;color:var(--text-dim);margin-top:2px;';
artistSpan.textContent = track.artist;
row.appendChild(titleSpan);
row.appendChild(artistSpan);
row.addEventListener('click', () => loadTrack(i));
return { row, titleSpan, artistSpan };
});
TRACKS.forEach((_, i) => listPanel.appendChild(trackList[i].row));
// Right panel: now playing
const nowPanel = document.createElement('div');
nowPanel.style.cssText = 'flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 24px;background:var(--bg-1);overflow-y:auto;';
const cover = document.createElement('div');
cover.style.cssText = 'width:120px;height:120px;background:rgba(212,162,76,0.15);border:1px solid rgba(212,162,76,0.2);display:flex;align-items:center;justify-content:center;font-size:48px;font-weight:600;color:var(--accent);margin-bottom:24px;';
const nowTitle = document.createElement('div');
nowTitle.style.cssText = 'font-size:28px;font-weight:600;color:var(--text);margin-bottom:6px;text-align:center;';
const nowArtist = document.createElement('div');
nowArtist.style.cssText = 'font-size:14px;color:var(--text-dim);margin-bottom:32px;text-align:center;';
// Controls row
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;align-items:center;gap:20px;margin-bottom:28px;';
const btnStyle = 'width:40px;height:40px;border-radius:50%;background:var(--bg-3);border:1px solid var(--border);color:var(--text);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:background 150ms ease-out;';
const prevBtn = document.createElement('button');
prevBtn.style.cssText = btnStyle;
prevBtn.textContent = '<<';
prevBtn.addEventListener('click', () => { currentTrack = (currentTrack - 1 + TRACKS.length) % TRACKS.length; loadTrack(currentTrack); });
const playBtn = document.createElement('button');
playBtn.style.cssText = 'width:48px;height:48px;border-radius:50%;background:var(--accent);border:none;color:#0f1114;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;transition:opacity 150ms ease-out;';
playBtn.textContent = '>';
const nextBtn = document.createElement('button');
nextBtn.style.cssText = btnStyle;
nextBtn.textContent = '>>';
nextBtn.addEventListener('click', () => { currentTrack = (currentTrack + 1) % TRACKS.length; loadTrack(currentTrack); });
controls.appendChild(prevBtn);
controls.appendChild(playBtn);
controls.appendChild(nextBtn);
// Seek bar
const seekContainer = document.createElement('div');
seekContainer.style.cssText = 'width:100%;max-width:400px;margin-bottom:16px;';
const seekLabel = document.createElement('div');
seekLabel.style.cssText = 'font-size:11px;color:var(--text-dim);margin-bottom:4px;display:flex;justify-content:space-between;';
const seekLabelLeft = document.createElement('span');
seekLabelLeft.textContent = '0:00';
const seekLabelRight = document.createElement('span');
seekLabelRight.textContent = '0:00';
seekLabel.appendChild(seekLabelLeft);
seekLabel.appendChild(seekLabelRight);
const seekInput = document.createElement('input');
seekInput.type = 'range';
seekInput.min = '0';
seekInput.max = '100';
seekInput.value = '0';
seekInput.style.cssText = 'width:100%;height:4px;-webkit-appearance:none;appearance:none;background:var(--bg-3);border-radius:2px;outline:none;cursor:pointer;';
seekInput.addEventListener('input', () => {
if (audio.duration) {
audio.currentTime = (seekInput.value / 100) * audio.duration;
}
});
seekContainer.appendChild(seekLabel);
seekContainer.appendChild(seekInput);
// Volume slider
const volContainer = document.createElement('div');
volContainer.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:12px;';
const volLabel = document.createElement('span');
volLabel.style.cssText = 'font-size:12px;color:var(--text-dim);';
volLabel.textContent = '\u{25A0}\u{25A0}';
const volInput = document.createElement('input');
volInput.type = 'range';
volInput.min = '0';
volInput.max = '100';
volInput.value = '70';
volInput.style.cssText = 'width:100px;height:4px;-webkit-appearance:none;appearance:none;background:var(--bg-3);border-radius:2px;outline:none;cursor:pointer;';
volInput.addEventListener('input', () => {
audio.volume = volInput.value / 100;
});
volContainer.appendChild(volLabel);
volContainer.appendChild(volInput);
nowPanel.appendChild(cover);
nowPanel.appendChild(nowTitle);
nowPanel.appendChild(nowArtist);
nowPanel.appendChild(controls);
nowPanel.appendChild(seekContainer);
nowPanel.appendChild(volContainer);
root.appendChild(listPanel);
root.appendChild(nowPanel);
container.appendChild(root);
function formatTime(s) {
if (!isFinite(s)) return '0:00';
return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0');
}
function updateUI() {
const track = TRACKS[currentTrack];
const urlOk = urlAvailable(track.src);
cover.textContent = track.title.charAt(0);
nowTitle.textContent = track.title;
nowArtist.textContent = track.artist;
playBtn.textContent = audio.paused ? '>' : '||';
// Highlight selected track
TRACKS.forEach((_, i) => {
const row = trackList[i].row;
if (i === currentTrack) {
row.style.borderLeftColor = 'var(--accent)';
row.style.background = 'var(--bg-3)';
} else {
row.style.borderLeftColor = 'transparent';
row.style.background = 'transparent';
}
});
seekLabelLeft.textContent = formatTime(audio.currentTime);
seekLabelRight.textContent = formatTime(audio.duration);
seekInput.value = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
}
function loadTrack(index) {
stopPlaySecondsTimer();
currentTrack = index;
const track = TRACKS[index];
if (!urlAvailable(track.src)) {
nowTitle.textContent = track.title;
nowArtist.textContent = 'Track unavailable (offline)';
cover.textContent = track.title.charAt(0);
cover.style.background = 'rgba(150,150,150,0.15)';
playBtn.textContent = '>';
playBtn.disabled = true;
seekInput.value = 0;
seekLabelLeft.textContent = '0:00';
seekLabelRight.textContent = '0:00';
return;
}
cover.style.background = 'rgba(212,162,76,0.15)';
playBtn.disabled = false;
audio.src = track.src;
audio.volume = volInput.value / 100;
audio.load();
updateUI();
}
playBtn.addEventListener('click', () => {
if (playBtn.disabled) return;
if (!audio.src) return;
if (audio.paused) {
audio.play().then(() => {
api.bus.emit('music:play');
updateUI();
}).catch(() => {
updateUI();
});
startPlaySecondsTimer();
} else {
audio.pause();
updateUI();
stopPlaySecondsTimer();
}
});
audio.addEventListener('timeupdate', updateUI);
audio.addEventListener('ended', () => {
currentTrack = (currentTrack + 1) % TRACKS.length;
loadTrack(currentTrack);
audio.play().then(() => {
updateUI();
startPlaySecondsTimer();
}).catch(() => { updateUI(); });
});
audio.addEventListener('error', () => {
nowArtist.textContent = 'Track unavailable (offline)';
playBtn.disabled = true;
updateUI();
stopPlaySecondsTimer();
});
updateUI();
loadTrack(0);
audio.pause();
function urlAvailable(url) {
try {
const u = new URL(url);
return true;
} catch {
return false;
}
}
function startPlaySecondsTimer() {
stopPlaySecondsTimer();
playSecondsTimer = setInterval(() => {
playedSeconds++;
api.bus.emit('music:played', { seconds: playedSeconds });
}, 5000);
}
function stopPlaySecondsTimer() {
clearInterval(playSecondsTimer);
}
},
};