tools: add sync-version.js to sync package.json -> Tauri files

- Add tools/sync-version.js script to read root package.json version
  and update src-tauri/tauri.conf.json and src-tauri/Cargo.toml.
- Update only the [package] version line in Cargo.toml to preserve formatting.
- Include JSON read/write helpers and basic error handling/reporting.
This commit is contained in:
2026-01-13 07:21:51 +01:00
parent abb7cafaed
commit 694f335408
50 changed files with 1128 additions and 6186 deletions

View File

@@ -11,6 +11,7 @@ let currentIndex = 0;
let isPlaying = false;
let currentMode = 'local'; // 'local' | 'cast'
let currentCastDevice = null;
let currentCastTransport = null; // 'tap' | 'proxy' | 'direct' | null
// Local playback is handled natively by the Tauri backend (player_* commands).
// The WebView is a control surface only.
@@ -158,12 +159,63 @@ const usIndex = document.getElementById('us_index');
// Init
async function init() {
try {
// Helpful debug information for release builds so we can compare parity with dev.
console.group && console.group('RadioCast init');
console.log('runningInTauri:', runningInTauri);
try { console.log('location:', location.href); } catch (_) {}
try { console.log('userAgent:', navigator.userAgent); } catch (_) {}
try { console.log('platform:', navigator.platform); } catch (_) {}
try { console.log('RADIO_DEBUG_DEVTOOLS flag:', localStorage.getItem('RADIO_DEBUG_DEVTOOLS')); } catch (_) {}
// Always try to read build stamp if present (bundled by build scripts).
try {
const resp = await fetch('/build-info.json', { cache: 'no-store' });
if (resp && resp.ok) {
const bi = await resp.json();
console.log('build-info:', bi);
} else {
console.log('build-info: not present');
}
} catch (e) {
console.log('build-info: failed to read');
}
restoreSavedVolume();
await loadStations();
try { console.log('stations loaded:', Array.isArray(stations) ? stations.length : typeof stations); } catch (_) {}
setupEventListeners();
ensureArtworkPointerFallback();
updateUI();
updateEngineBadge();
// Optionally open devtools in release builds for debugging parity with `tauri dev`.
// Enable by setting `localStorage.setItem('RADIO_DEBUG_DEVTOOLS', '1')` or by creating
// `src/build-info.json` with { debug: true } at build time (the `build:devlike` script does this).
try {
let shouldOpen = false;
try { if (localStorage && localStorage.getItem && localStorage.getItem('RADIO_DEBUG_DEVTOOLS') === '1') shouldOpen = true; } catch (_) {}
// Build-time flag file (created by tools/write-build-flag.js when running `build`/`build:devlike`).
try {
const resp = await fetch('/build-info.json', { cache: 'no-store' });
if (resp && resp.ok) {
const bi = await resp.json();
if (bi && bi.debug) shouldOpen = true;
}
} catch (_) {}
if (shouldOpen) {
try {
const w = getCurrentWindow();
if (w && typeof w.openDevTools === 'function') {
w.openDevTools();
console.log('Opened devtools via build-info/localStorage flag');
}
} catch (e) { console.warn('Failed to open devtools:', e); }
}
} catch (e) { /* ignore */ }
console.groupEnd && console.groupEnd();
} catch (e) {
console.error('Error during init', e);
if (statusTextEl) statusTextEl.textContent = 'Init error: ' + (e && e.message ? e.message : String(e));
@@ -1161,7 +1213,39 @@ async function play() {
} else if (currentMode === 'cast' && currentCastDevice) {
// Cast logic
try {
await invoke('cast_play', { deviceName: currentCastDevice, url: station.url });
// UX guard: if native playback is currently decoding a different station,
// stop it explicitly before starting the cast pipeline (which would otherwise
// replace the decoder behind the scenes).
try {
const st = await invoke('player_get_state');
const nativeActive = st && (st.status === 'playing' || st.status === 'buffering') && st.url;
if (nativeActive && st.url !== station.url) {
stopLocalPlayerStatePolling();
await invoke('player_stop').catch(() => {});
}
} catch (_) {
// Ignore: best-effort guard only.
}
let castUrl = station.url;
currentCastTransport = null;
try {
const res = await invoke('cast_proxy_start', { deviceName: currentCastDevice, url: station.url });
if (res && typeof res === 'object') {
castUrl = res.url || station.url;
currentCastTransport = res.mode || 'proxy';
} else {
// Backward-compat (older backend returned string)
castUrl = res || station.url;
currentCastTransport = 'proxy';
}
} catch (e) {
// If proxy cannot start (ffmpeg missing, firewall, etc), fall back to direct station URL.
console.warn('Cast proxy start failed; falling back to direct URL', e);
currentCastTransport = 'direct';
}
await invoke('cast_play', { deviceName: currentCastDevice, url: castUrl });
isPlaying = true;
// Sync volume
const vol = volumeSlider.value / 100;
@@ -1169,8 +1253,10 @@ async function play() {
updateUI();
} catch (e) {
console.error('Cast failed', e);
statusTextEl.textContent = 'Cast Error';
statusTextEl.textContent = 'Cast Error (check LAN/firewall)';
await invoke('cast_proxy_stop').catch(() => {});
currentMode = 'local'; // Fallback
currentCastTransport = null;
updateUI();
}
}
@@ -1186,6 +1272,7 @@ async function stop() {
}
} else if (currentMode === 'cast' && currentCastDevice) {
try {
await invoke('cast_proxy_stop').catch(() => {});
await invoke('cast_stop', { deviceName: currentCastDevice });
} catch (e) {
console.error(e);
@@ -1193,6 +1280,9 @@ async function stop() {
}
isPlaying = false;
if (currentMode !== 'cast') {
currentCastTransport = null;
}
updateUI();
}
@@ -1220,14 +1310,24 @@ function updateUI() {
playBtn.classList.add('playing'); // Add pulsing ring animation
statusTextEl.textContent = 'Playing';
statusDotEl.style.backgroundColor = 'var(--success)';
stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
if (currentMode === 'cast') {
const t = currentCastTransport ? ` (${currentCastTransport})` : '';
stationSubtitleEl.textContent = `Casting${t} to ${currentCastDevice}`;
} else {
stationSubtitleEl.textContent = 'Live Stream';
}
} else {
iconPlay.classList.remove('hidden');
iconStop.classList.add('hidden');
playBtn.classList.remove('playing'); // Remove pulsing ring
statusTextEl.textContent = 'Ready';
statusDotEl.style.backgroundColor = 'var(--text-muted)';
stationSubtitleEl.textContent = currentMode === 'cast' ? `Connected to ${currentCastDevice}` : 'Live Stream';
if (currentMode === 'cast') {
const t = currentCastTransport ? ` (${currentCastTransport})` : '';
stationSubtitleEl.textContent = `Connected${t} to ${currentCastDevice}`;
} else {
stationSubtitleEl.textContent = 'Live Stream';
}
}
updateEngineBadge();
@@ -1296,14 +1396,20 @@ async function selectCastDevice(deviceName) {
await stop();
}
// Best-effort cleanup: stop any lingering cast transport when changing device/mode.
await invoke('cast_proxy_stop').catch(() => {});
if (deviceName) {
currentMode = 'cast';
currentCastDevice = deviceName;
castBtn.style.color = 'var(--success)';
// Transport mode gets set on play.
currentCastTransport = currentCastTransport || null;
} else {
currentMode = 'local';
currentCastDevice = null;
castBtn.style.color = 'var(--text-main)';
currentCastTransport = null;
}
updateUI();
@@ -1313,22 +1419,51 @@ async function selectCastDevice(deviceName) {
// Let's prompt user to play.
}
// Best-effort: stop any cast transport when leaving the window.
window.addEventListener('beforeunload', () => {
try { invoke('cast_proxy_stop'); } catch (_) {}
});
window.addEventListener('DOMContentLoaded', init);
// Service worker is useful for the PWA, but it can cause confusing caching during
// Tauri development because it may serve an older cached `index.html`.
if ('serviceWorker' in navigator) {
if (runningInTauri) {
// Best-effort cleanup so the desktop app always reflects local file changes.
navigator.serviceWorker.getRegistrations()
.then((regs) => Promise.all(regs.map((r) => r.unregister())))
.catch(() => {});
// Best-effort cleanup so the desktop app doesn't get stuck on an old cached UI.
// If we clear anything, do a one-time reload to ensure the new bundled assets are used.
(async () => {
let changed = false;
if ('caches' in window) {
caches.keys()
.then((keys) => Promise.all(keys.map((k) => caches.delete(k))))
.catch(() => {});
}
try {
const regs = await navigator.serviceWorker.getRegistrations();
if (regs && regs.length) {
await Promise.all(regs.map((r) => r.unregister().catch(() => false)));
changed = true;
}
} catch (_) {}
if ('caches' in window) {
try {
const keys = await caches.keys();
if (keys && keys.length) {
await Promise.all(keys.map((k) => caches.delete(k).catch(() => false)));
changed = true;
}
} catch (_) {}
}
try {
if (changed) {
const k = '__radiocast_sw_cleared_once';
const already = sessionStorage.getItem(k);
if (!already) {
sessionStorage.setItem(k, '1');
location.reload();
}
}
} catch (_) {}
})();
} else {
// Register Service Worker for PWA installation (non-disruptive)
window.addEventListener('load', () => {
@@ -1400,7 +1535,9 @@ async function openStationsOverlay() {
li.onclick = async () => {
currentMode = 'local';
currentCastDevice = null;
currentCastTransport = null;
castBtn.style.color = 'var(--text-main)';
try { await invoke('cast_proxy_stop'); } catch (_) {}
await setStationByIndex(idx);
closeCastOverlay();
try { await play(); } catch (e) { console.error('Failed to play station from grid', e); }

View File

@@ -1,4 +1,10 @@
const CACHE_NAME = 'radiocast-core-v2';
// NOTE: This service worker is for the web/PWA build.
// For the Tauri desktop app we aggressively unregister SWs in `src/main.js`.
//
// Bump this value whenever caching logic changes to guarantee clients don't
// keep an old UI after updates.
const CACHE_NAME = 'radiocast-core-v3';
const CORE_ASSETS = [
'.',
'index.html',
@@ -7,14 +13,25 @@ const CORE_ASSETS = [
'stations.json',
'assets/favicon_io/android-chrome-192x192.png',
'assets/favicon_io/android-chrome-512x512.png',
'assets/favicon_io/apple-touch-icon.png'
'assets/favicon_io/apple-touch-icon.png',
// Optional build stamp (only present for some builds).
'build-info.json',
];
const CORE_PATHS = new Set(CORE_ASSETS.map((p) => (p === '.' ? '/' : '/' + p.replace(/^\//, ''))));
self.addEventListener('install', (event) => {
// Activate updated SW as soon as it's installed.
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS))
caches.open(CACHE_NAME).then((cache) => {
const reqs = CORE_ASSETS.map((p) => {
const url = p === '.' ? './' : p;
// Force a fresh fetch for core assets to avoid carrying forward stale UI.
return new Request(url, { cache: 'reload' });
});
return cache.addAll(reqs);
})
);
});
@@ -33,6 +50,30 @@ self.addEventListener('fetch', (event) => {
// Only handle GET requests
if (event.request.method !== 'GET') return;
const url = new URL(event.request.url);
// Don't cache cross-origin requests (station logos, APIs, etc.).
if (url.origin !== self.location.origin) {
return;
}
const isCore = CORE_PATHS.has(url.pathname) || url.pathname === '/';
const isHtmlNavigation = event.request.mode === 'navigate' || (event.request.headers.get('accept') || '').includes('text/html');
// Network-first for navigations and core assets to prevent "old UI" issues.
if (isHtmlNavigation || isCore) {
event.respondWith(
fetch(event.request)
.then((networkResp) => {
const respClone = networkResp.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, respClone)).catch(() => {});
return networkResp;
})
.catch(() => caches.match(event.request).then((cached) => cached || caches.match('index.html')))
);
return;
}
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;