Visually fix
This commit is contained in:
108
src/index.html
108
src/index.html
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Radio1 Player</title>
|
||||
<title>RadioPlayer</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="main.js" defer type="module"></script>
|
||||
</head>
|
||||
@@ -16,34 +16,52 @@
|
||||
|
||||
<main class="glass-card">
|
||||
<header data-tauri-drag-region>
|
||||
<button id="menu-btn" class="icon-btn" aria-label="Menu">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-info" data-tauri-drag-region>
|
||||
<span class="app-title">Radio1 Player</span>
|
||||
<span class="status-indicator" id="status-indicator">
|
||||
<span class="status-dot"></span> <span id="status-text">Ready</span>
|
||||
</span>
|
||||
<div class="header-top-row">
|
||||
<div class="header-icons-left" aria-hidden="true">
|
||||
<button id="edit-stations-btn" class="icon-btn" title="Edit Stations" aria-label="Edit Stations">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="cast-toggle-btn" class="icon-btn" aria-label="Cast" title="Cast">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a14 14 0 0 1 14 14h-2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- status moved below station info -->
|
||||
|
||||
<div class="header-close">
|
||||
<button id="close-btn" class="icon-btn close-btn" aria-label="Close" title="Close">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-buttons">
|
||||
<button id="cast-toggle-btn" class="icon-btn" aria-label="Cast">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a14 14 0 0 1 14 14h-2" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="close-btn" class="icon-btn close-btn" aria-label="Close">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="header-third-row">
|
||||
<div class="header-icons">
|
||||
<button id="edit-stations-btn" class="icon-btn" title="Edit Stations" aria-label="Edit Stations">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="cast-toggle-btn" class="icon-btn" aria-label="Cast" title="Cast">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a14 14 0 0 1 14 14h-2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -84,6 +102,10 @@
|
||||
<section class="track-info">
|
||||
<h2 id="station-name">Radio 1 MB</h2>
|
||||
<p id="station-subtitle">Live Stream</p>
|
||||
<div id="status-indicator" class="status-indicator-wrap" aria-hidden="true">
|
||||
<span class="status-dot"></span>
|
||||
<span id="status-text">Ready</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Visual Progress Bar (Live) -->
|
||||
@@ -151,6 +173,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stations Editor Overlay -->
|
||||
<div id="editor-overlay" class="overlay hidden" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="editorTitle">
|
||||
<h2 id="editorTitle">Edit Stations</h2>
|
||||
|
||||
<ul id="editor-list" class="device-list"></ul>
|
||||
|
||||
<form id="add-station-form">
|
||||
<div style="margin-bottom:8px;">
|
||||
<input id="us_title" placeholder="Title" required style="width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:inherit">
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<input id="us_url" placeholder="Stream URL" required style="width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:inherit">
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<input id="us_logo" placeholder="Logo URL (optional)" style="width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:inherit">
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<input id="us_www" placeholder="Website (optional)" style="width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:transparent;color:inherit">
|
||||
</div>
|
||||
<input type="hidden" id="us_id">
|
||||
<input type="hidden" id="us_index">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button id="us_save_btn" class="btn cancel" type="submit" style="flex:1">Save</button>
|
||||
<button id="editor-close-btn" class="btn" type="button" style="flex:0;background:#6b6bff">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
325
src/main.js
325
src/main.js
@@ -27,6 +27,19 @@ const closeOverlayBtn = document.getElementById('close-overlay');
|
||||
const deviceListEl = document.getElementById('device-list');
|
||||
const logoTextEl = document.querySelector('.station-logo-text');
|
||||
const logoImgEl = document.getElementById('station-logo-img');
|
||||
const artworkPlaceholder = document.querySelector('.artwork-placeholder');
|
||||
// Editor elements
|
||||
const editBtn = document.getElementById('edit-stations-btn');
|
||||
const editorOverlay = document.getElementById('editor-overlay');
|
||||
const editorCloseBtn = document.getElementById('editor-close-btn');
|
||||
const editorListEl = document.getElementById('editor-list');
|
||||
const addStationForm = document.getElementById('add-station-form');
|
||||
const usTitle = document.getElementById('us_title');
|
||||
const usUrl = document.getElementById('us_url');
|
||||
const usLogo = document.getElementById('us_logo');
|
||||
const usWww = document.getElementById('us_www');
|
||||
const usId = document.getElementById('us_id');
|
||||
const usIndex = document.getElementById('us_index');
|
||||
|
||||
// Init
|
||||
async function init() {
|
||||
@@ -62,9 +75,39 @@ async function loadStations() {
|
||||
// Filter out disabled stations and those without a stream URL
|
||||
.filter((s) => s.enabled !== false && s.url && s.url.length > 0);
|
||||
|
||||
// Load user-defined stations from localStorage and merge
|
||||
const user = loadUserStations();
|
||||
const userNormalized = user
|
||||
.map((s) => {
|
||||
const name = s.title || s.name || s.id || 'UserStation';
|
||||
const url = s.url || s.liveAudio || s.liveVideo || '';
|
||||
return {
|
||||
id: s.id || `user-${name.replace(/\s+/g, '-')}`,
|
||||
name,
|
||||
url,
|
||||
logo: s.logo || '',
|
||||
enabled: typeof s.enabled === 'boolean' ? s.enabled : true,
|
||||
raw: s,
|
||||
_user: true,
|
||||
};
|
||||
})
|
||||
.filter((s) => s.url && s.url.length > 0);
|
||||
|
||||
// Append user stations after file stations
|
||||
stations = stations.concat(userNormalized);
|
||||
|
||||
if (stations.length > 0) {
|
||||
currentIndex = 0;
|
||||
loadStation(currentIndex);
|
||||
// Try to restore last selected station by id
|
||||
const lastId = getLastStationId();
|
||||
if (lastId) {
|
||||
const found = stations.findIndex(s => s.id === lastId);
|
||||
if (found >= 0) currentIndex = found;
|
||||
else currentIndex = 0;
|
||||
} else {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
loadStation(currentIndex);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load stations', e);
|
||||
@@ -72,6 +115,159 @@ async function loadStations() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- User Stations (localStorage) ---
|
||||
function loadUserStations() {
|
||||
try {
|
||||
const raw = localStorage.getItem('userStations');
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.error('Error reading user stations', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveUserStations(arr) {
|
||||
try {
|
||||
localStorage.setItem('userStations', JSON.stringify(arr || []));
|
||||
} catch (e) {
|
||||
console.error('Error saving user stations', e);
|
||||
}
|
||||
}
|
||||
|
||||
function openEditorOverlay() {
|
||||
renderUserStationsList();
|
||||
editorOverlay.classList.remove('hidden');
|
||||
editorOverlay.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function closeEditorOverlay() {
|
||||
editorOverlay.classList.add('hidden');
|
||||
editorOverlay.setAttribute('aria-hidden', 'true');
|
||||
// clear form
|
||||
addStationForm.reset();
|
||||
usIndex.value = '';
|
||||
}
|
||||
|
||||
function renderUserStationsList() {
|
||||
const list = loadUserStations();
|
||||
editorListEl.innerHTML = '';
|
||||
if (!list || list.length === 0) {
|
||||
editorListEl.innerHTML = '<li class="device"><div class="device-main">No user stations</div><div class="device-sub">Add your stream using the form below</div></li>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach((s, idx) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'device';
|
||||
const main = s.title || s.name || s.id || 'User Station';
|
||||
const sub = s.url || '';
|
||||
li.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<div class=\"device-main\">${main}</div>
|
||||
<div class=\"device-sub\">${sub}</div>
|
||||
</div>
|
||||
<div style=\"display:flex;gap:8px;align-items:center;\">
|
||||
<button data-idx=\"${idx}\" class=\"btn edit-btn\" style=\"background:#6bd3ff;color:#042\">Edit</button>
|
||||
<button data-idx=\"${idx}\" class=\"btn delete-btn\" style=\"background:#ff6b6b\">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
editorListEl.appendChild(li);
|
||||
});
|
||||
|
||||
// Attach handlers
|
||||
editorListEl.querySelectorAll('.edit-btn').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const idx = Number(b.getAttribute('data-idx'));
|
||||
editUserStation(idx);
|
||||
});
|
||||
});
|
||||
editorListEl.querySelectorAll('.delete-btn').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const idx = Number(b.getAttribute('data-idx'));
|
||||
deleteUserStation(idx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editUserStation(idx) {
|
||||
const list = loadUserStations();
|
||||
const s = list[idx];
|
||||
if (!s) return;
|
||||
usTitle.value = s.title || s.name || '';
|
||||
usUrl.value = s.url || s.liveAudio || '';
|
||||
usLogo.value = s.logo || '';
|
||||
usWww.value = s.www || s.website || '';
|
||||
usId.value = s.id || '';
|
||||
usIndex.value = String(idx);
|
||||
}
|
||||
|
||||
function deleteUserStation(idx) {
|
||||
const list = loadUserStations();
|
||||
list.splice(idx, 1);
|
||||
saveUserStations(list);
|
||||
// refresh stations in runtime
|
||||
refreshStationsFromSources();
|
||||
renderUserStationsList();
|
||||
}
|
||||
|
||||
function refreshStationsFromSources() {
|
||||
// reload stations.json and user stations into `stations` array
|
||||
// For simplicity, re-run loadStations()
|
||||
loadStations();
|
||||
}
|
||||
|
||||
// Persist last-selected station id between sessions
|
||||
function saveLastStationId(id) {
|
||||
try {
|
||||
if (!id) return;
|
||||
localStorage.setItem('lastStationId', id);
|
||||
} catch (e) {
|
||||
console.error('Failed to save last station id', e);
|
||||
}
|
||||
}
|
||||
|
||||
function getLastStationId() {
|
||||
try {
|
||||
return localStorage.getItem('lastStationId');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submit (add/update)
|
||||
addStationForm && addStationForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const list = loadUserStations();
|
||||
const station = {
|
||||
id: usId.value || `user-${Date.now()}`,
|
||||
title: usTitle.value.trim(),
|
||||
url: usUrl.value.trim(),
|
||||
logo: usLogo.value.trim(),
|
||||
www: usWww.value.trim(),
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const idx = usIndex.value === '' ? -1 : Number(usIndex.value);
|
||||
if (idx >= 0 && idx < list.length) {
|
||||
list[idx] = station;
|
||||
} else {
|
||||
list.push(station);
|
||||
}
|
||||
|
||||
saveUserStations(list);
|
||||
renderUserStationsList();
|
||||
refreshStationsFromSources();
|
||||
addStationForm.reset();
|
||||
usIndex.value = '';
|
||||
});
|
||||
|
||||
// Editor button handlers
|
||||
editBtn && editBtn.addEventListener('click', openEditorOverlay);
|
||||
editorCloseBtn && editorCloseBtn.addEventListener('click', closeEditorOverlay);
|
||||
|
||||
|
||||
function setupEventListeners() {
|
||||
playBtn.addEventListener('click', togglePlay);
|
||||
prevBtn.addEventListener('click', playPrev);
|
||||
@@ -94,10 +290,10 @@ function setupEventListeners() {
|
||||
});
|
||||
|
||||
// Menu button - explicit functionality or placeholder?
|
||||
// For now just log or maybe show about
|
||||
document.getElementById('menu-btn').addEventListener('click', () => {
|
||||
openStationsOverlay();
|
||||
});
|
||||
// Menu removed — header click opens stations via artwork placeholder
|
||||
|
||||
// Click artwork to open stations chooser
|
||||
artworkPlaceholder && artworkPlaceholder.addEventListener('click', openStationsOverlay);
|
||||
|
||||
// Hotkeys?
|
||||
}
|
||||
@@ -113,9 +309,18 @@ function loadStation(index) {
|
||||
// Simple heuristic: if name has a number, use it, else first letter
|
||||
// If station has a logo URL, show the image; otherwise show the text fallback
|
||||
if (station.logo && station.logo.length > 0) {
|
||||
logoImgEl.src = station.logo;
|
||||
logoImgEl.classList.remove('hidden');
|
||||
logoTextEl.classList.add('hidden');
|
||||
// Verify the logo exists before showing it
|
||||
checkImageExists(station.logo).then((exists) => {
|
||||
if (exists) {
|
||||
logoImgEl.src = station.logo;
|
||||
logoImgEl.classList.remove('hidden');
|
||||
logoTextEl.classList.add('hidden');
|
||||
} else {
|
||||
logoImgEl.src = '';
|
||||
logoImgEl.classList.add('hidden');
|
||||
logoTextEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback to single-letter/logo text
|
||||
logoImgEl.src = '';
|
||||
@@ -130,6 +335,39 @@ function loadStation(index) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an image URL is reachable and valid
|
||||
function checkImageExists(url, timeout = 6000) {
|
||||
return new Promise((resolve) => {
|
||||
if (!url) return resolve(false);
|
||||
try {
|
||||
const img = new Image();
|
||||
let timedOut = false;
|
||||
const t = setTimeout(() => {
|
||||
timedOut = true;
|
||||
img.src = ''; // stop load
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
|
||||
img.onload = () => {
|
||||
if (!timedOut) {
|
||||
clearTimeout(t);
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (!timedOut) {
|
||||
clearTimeout(t);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
// Bypass caching oddities by assigning after handlers
|
||||
img.src = url;
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function togglePlay() {
|
||||
if (isPlaying) {
|
||||
await stop();
|
||||
@@ -202,6 +440,9 @@ async function playNext() {
|
||||
currentIndex = (currentIndex + 1) % stations.length;
|
||||
loadStation(currentIndex);
|
||||
|
||||
// persist selection
|
||||
saveLastStationId(stations[currentIndex].id);
|
||||
|
||||
if (wasPlaying) await play();
|
||||
}
|
||||
|
||||
@@ -215,6 +456,9 @@ async function playPrev() {
|
||||
currentIndex = (currentIndex - 1 + stations.length) % stations.length;
|
||||
loadStation(currentIndex);
|
||||
|
||||
// persist selection
|
||||
saveLastStationId(stations[currentIndex].id);
|
||||
|
||||
if (wasPlaying) await play();
|
||||
}
|
||||
|
||||
@@ -253,6 +497,8 @@ function handleVolumeInput() {
|
||||
async function openCastOverlay() {
|
||||
castOverlay.classList.remove('hidden');
|
||||
castOverlay.setAttribute('aria-hidden', 'false');
|
||||
// ensure cast overlay shows linear list style
|
||||
deviceListEl.classList.remove('stations-grid');
|
||||
deviceListEl.innerHTML = '<li class="device"><div class="device-main">Scanning...</div><div class="device-sub">Searching for speakers</div></li>';
|
||||
|
||||
try {
|
||||
@@ -316,40 +562,75 @@ async function selectCastDevice(deviceName) {
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// Open overlay and show list of stations (used by menu/hamburger)
|
||||
function openStationsOverlay() {
|
||||
async function openStationsOverlay() {
|
||||
castOverlay.classList.remove('hidden');
|
||||
castOverlay.setAttribute('aria-hidden', 'false');
|
||||
deviceListEl.innerHTML = '<li class="device"><div class="device-main">Loading...</div><div class="device-sub">Preparing stations</div></li>';
|
||||
|
||||
// If stations not loaded yet, show message
|
||||
if (!stations || stations.length === 0) {
|
||||
deviceListEl.classList.remove('stations-grid');
|
||||
deviceListEl.innerHTML = '<li class="device"><div class="device-main">No stations found</div><div class="device-sub">Check your stations.json</div></li>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render stations as responsive grid of cards (2-3 per row depending on width)
|
||||
deviceListEl.classList.add('stations-grid');
|
||||
deviceListEl.innerHTML = '';
|
||||
|
||||
stations.forEach((s, idx) => {
|
||||
for (let idx = 0; idx < stations.length; idx++) {
|
||||
const s = stations[idx];
|
||||
const li = document.createElement('li');
|
||||
li.className = 'device' + (currentIndex === idx ? ' selected' : '');
|
||||
li.className = 'station-card' + (currentIndex === idx ? ' selected' : '');
|
||||
|
||||
const logoUrl = s.logo || (s.raw && (s.raw.logo || s.raw.poster)) || '';
|
||||
const title = s.name || s.title || s.id || 'Station';
|
||||
const subtitle = (s.raw && s.raw.www) ? s.raw.www : (s.id || '');
|
||||
li.innerHTML = `<div class="device-main">${s.name}</div><div class="device-sub">${subtitle}</div>`;
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'station-card-left';
|
||||
|
||||
// Check if logo exists, otherwise show fallback
|
||||
const hasLogo = await checkImageExists(logoUrl);
|
||||
if (hasLogo) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'station-card-logo';
|
||||
img.src = logoUrl;
|
||||
img.alt = `${title} logo`;
|
||||
left.appendChild(img);
|
||||
} else {
|
||||
const fb = document.createElement('div');
|
||||
fb.className = 'station-card-fallback';
|
||||
fb.textContent = title.charAt(0).toUpperCase();
|
||||
left.appendChild(fb);
|
||||
}
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'station-card-body';
|
||||
const tEl = document.createElement('div');
|
||||
tEl.className = 'station-card-title';
|
||||
tEl.textContent = title;
|
||||
const sEl = document.createElement('div');
|
||||
sEl.className = 'station-card-sub';
|
||||
sEl.textContent = subtitle;
|
||||
body.appendChild(tEl);
|
||||
body.appendChild(sEl);
|
||||
|
||||
li.appendChild(left);
|
||||
li.appendChild(body);
|
||||
|
||||
li.onclick = async () => {
|
||||
// Always switch to local playback when selecting from stations menu
|
||||
currentMode = 'local';
|
||||
currentCastDevice = null;
|
||||
castBtn.style.color = 'var(--text-main)';
|
||||
|
||||
// Select and play
|
||||
currentIndex = idx;
|
||||
// Remember this selection
|
||||
saveLastStationId(stations[idx].id);
|
||||
loadStation(currentIndex);
|
||||
closeCastOverlay();
|
||||
try {
|
||||
await play();
|
||||
} catch (e) {
|
||||
console.error('Failed to play station from menu', e);
|
||||
}
|
||||
try { await play(); } catch (e) { console.error('Failed to play station from grid', e); }
|
||||
};
|
||||
|
||||
deviceListEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
256
src/styles.css
256
src/styles.css
@@ -29,6 +29,14 @@ body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: linear-gradient(-45deg, #7b7fd8, #b57cf2, #8b5cf6, #6930c3, #7b7fd8);
|
||||
.status-indicator-wrap {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
justify-content:center;
|
||||
margin-top:8px;
|
||||
color:var(--text-main);
|
||||
}
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 12s ease-in-out infinite;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
@@ -111,36 +119,96 @@ body {
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Make whole card draggable for window movement; interactive children override with no-drag */
|
||||
.glass-card {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
-webkit-app-region: drag; /* Draggable area */
|
||||
padding: 10px 14px 8px 14px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, rgba(60,84,255,0.14), rgba(123,127,216,0.10));
|
||||
border: 1px solid rgba(120,130,255,0.12);
|
||||
box-shadow: 0 10px 30px rgba(28,25,60,0.35), inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
backdrop-filter: blur(8px) saturate(120%);
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
|
||||
.header-top-row {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
|
||||
.header-icons-left { flex: 0 0 auto; display:flex; align-items:center; gap:8px; padding-left:8px; }
|
||||
|
||||
.header-center-status { flex:1; display:flex; justify-content:center; align-items:center; }
|
||||
|
||||
.header-close { flex:0 0 auto; }
|
||||
|
||||
.header-second-row {
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
width:100%;
|
||||
margin-top:6px;
|
||||
}
|
||||
|
||||
.status-indicator-wrap { display:flex; gap:8px; align-items:center; color:var(--text-main); }
|
||||
|
||||
.header-third-row { display:none; }
|
||||
.header-left {
|
||||
justify-content: flex-start;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.app-title { text-align: center; }
|
||||
|
||||
.header-info {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.05rem;
|
||||
color: var(--text-main);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--success);
|
||||
margin-top: 4px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -152,26 +220,28 @@ header {
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
background: rgba(255,255,255,0.02);
|
||||
border: 1px solid rgba(255,255,255,0.03);
|
||||
color: var(--text-main);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
|
||||
-webkit-app-region: no-drag; /* Buttons clickable */
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
@@ -195,8 +265,11 @@ header {
|
||||
height: 220px;
|
||||
border-radius: 24px;
|
||||
padding: 6px; /* spacing for ring */
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0));
|
||||
box-shadow: 5px 5px 15px rgba(0,0,0,0.1), inset 1px 1px 2px rgba(255,255,255,0.3);
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.00));
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(8px) saturate(120%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.artwork-placeholder {
|
||||
@@ -209,7 +282,28 @@ header {
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
|
||||
box-shadow: inset 0 0 30px rgba(0,0,0,0.22);
|
||||
border: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
/* glossy inner rim for artwork */
|
||||
.artwork-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 6px; /* follows padding to create rim */
|
||||
border-radius: 20px;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), inset 0 -20px 40px rgba(255,255,255,0.02);
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* Make artwork clickable and give subtle hover feedback */
|
||||
.artwork-placeholder {
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
.artwork-placeholder:hover {
|
||||
box-shadow: 0 18px 40px rgba(255, 255, 0, 0.45), inset 0 0 28px rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.artwork-placeholder {
|
||||
@@ -541,6 +635,90 @@ input[type=range]::-webkit-slider-thumb {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Stations grid to show cards (used for stations overlay) */
|
||||
.stations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.station-card {
|
||||
list-style: none;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s;
|
||||
}
|
||||
|
||||
.station-card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 18px 40px rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
.station-card.selected {
|
||||
background: linear-gradient(135deg, #c77dff, #8b5cf6);
|
||||
color: #111;
|
||||
box-shadow: 0 10px 30px rgba(199,125,255,0.22);
|
||||
}
|
||||
|
||||
.station-card-left {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
flex: 0 0 56px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
.station-card-logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit:contain;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.35);
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.station-card-fallback {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
font-weight:800;
|
||||
font-size:1.2rem;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.station-card-body {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:3px;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.station-card-title {
|
||||
font-weight:700;
|
||||
font-size:0.95rem;
|
||||
line-height:1.1;
|
||||
}
|
||||
|
||||
.station-card-sub {
|
||||
font-size:0.8rem;
|
||||
color: rgba(255,255,255,0.7);
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
/* Device row */
|
||||
.device {
|
||||
padding: 12px 14px;
|
||||
@@ -604,3 +782,51 @@ input[type=range]::-webkit-slider-thumb {
|
||||
transform: scale(1.02);
|
||||
background: #e17c8d;
|
||||
}
|
||||
|
||||
/* Editor specific tweaks */
|
||||
.modal form input {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Ensure editor overlay input fields look consistent */
|
||||
#editor-list .device {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn.edit-btn, .btn.delete-btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#add-station-form button.btn {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Make modal form inputs visible on dark translucent background */
|
||||
.modal input,
|
||||
.modal textarea,
|
||||
.modal select {
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
color: var(--text-main);
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
.modal input::placeholder,
|
||||
.modal textarea::placeholder {
|
||||
color: rgba(255,255,255,0.55);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user