diff --git a/android/app/src/main/assets/index.html b/android/app/src/main/assets/index.html index 3ea45e7..0126d3e 100644 --- a/android/app/src/main/assets/index.html +++ b/android/app/src/main/assets/index.html @@ -4,7 +4,7 @@ - Radio1 Player + Radio Player diff --git a/layout_plan.md b/layout_plan.md index 3f3b397..eeb84db 100644 --- a/layout_plan.md +++ b/layout_plan.md @@ -1,8 +1,8 @@ -# Radio1 Player – Glassmorphism UI Redesign (Tauri + HTML) +# Radio Player – Glassmorphism UI Redesign (Tauri + HTML) ## Objective -Redesign the **Radio1 Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps. +Redesign the **Radio Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps. The app is built with: @@ -42,7 +42,7 @@ Single centered player card: ``` ┌──────────────────────────────┐ -│ Radio1 Player │ +│ Radio Player │ │ ● Playing / Ready │ │ │ │ [ Station Artwork / Logo ] │ @@ -52,7 +52,7 @@ Single centered player card: │ │ │ ────────●──────── │ │ │ -│ ⏮ ▶ / ⏸ ⏭ │ +│ ⏮ ▶ / ⏸ ⏭ │ │ │ │ 🔊 ─────●──── 50% │ └──────────────────────────────┘ @@ -64,7 +64,7 @@ Single centered player card: ### Header -* Title: `Radio1 Player` +* Title: `RadioPlayer` * Status indicator: * `● Ready` diff --git a/sidecar/index.js b/sidecar/index.js index e84a5f8..f519871 100644 --- a/sidecar/index.js +++ b/sidecar/index.js @@ -175,7 +175,7 @@ function loadMedia(url) { streamType: 'LIVE', metadata: { metadataType: 0, - title: 'Radio 1' + title: 'RadioPlayer' } }; diff --git a/src-tauri/check_log.txt b/src-tauri/check_log.txt index 418d45d..b32fb7b 100644 --- a/src-tauri/check_log.txt +++ b/src-tauri/check_log.txt @@ -1,4 +1,4 @@ - Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri) + Checking radio-tauri v0.1.0 (D:\Sites\Work\RadioPlayer\radio-tauri\src-tauri) warning: variable does not need to be mutable --> src\lib.rs:38:9 | diff --git a/src/index.html b/src/index.html index 3ea45e7..bc884b7 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ - Radio1 Player + RadioPlayer @@ -16,34 +16,52 @@
- -
- Radio1 Player - - Ready - +
+ + + + +
+ +
-
- - + +
+
+ + +
@@ -84,6 +102,10 @@

Radio 1 MB

Live Stream

+
@@ -151,6 +173,36 @@ + + +
diff --git a/src/main.js b/src/main.js index 50c6b53..66b16f2 100644 --- a/src/main.js +++ b/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 = '
  • No user stations
    Add your stream using the form below
  • '; + 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 = `
    +
    +
    ${main}
    +
    ${sub}
    +
    +
    + + +
    +
    `; + + 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 = '
  • Scanning...
    Searching for speakers
  • '; 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 = '
  • Loading...
    Preparing stations
  • '; // If stations not loaded yet, show message if (!stations || stations.length === 0) { + deviceListEl.classList.remove('stations-grid'); deviceListEl.innerHTML = '
  • No stations found
    Check your stations.json
  • '; 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 = `
    ${s.name}
    ${subtitle}
    `; + + 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); - }); + } } diff --git a/src/styles.css b/src/styles.css index 37e8c62..b664875 100644 --- a/src/styles.css +++ b/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; +}