10 Commits

Author SHA1 Message Date
f2732b36f2 update 2026-01-02 20:31:15 +01:00
7c0a202f16 fix 2026-01-02 19:38:16 +01:00
cb01a59051 Display current song 2026-01-02 19:37:08 +01:00
c3f594d102 fixed visually 2026-01-02 17:16:53 +01:00
a2753bcf66 add current song 2026-01-02 16:58:58 +01:00
e36bb1ab55 Visually fix 2026-01-02 13:25:10 +01:00
c5dc6b9dd4 Added Radio City 2026-01-02 11:38:54 +01:00
c09b05b7e7 fix 2026-01-01 20:57:03 +01:00
b99d9ce524 andorid app 2026-01-01 10:47:57 +01:00
ff9209062e Merge tag 'v0.1' into develop
Initial public preview of RadioPlayer: a lightweight Custom Google Cast Receiver + desktop sidecar for playing a LIVE radio stream.

Highlights

Custom CAF Receiver UI (HTML/CSS/JS) with branded artwork and playback status
Plays LIVE stream: https://live.radio1.si/Radio1MB (audio/mpeg, streamType: LIVE)
Controls: Play / Stop / Volume via Cast sender
Works on Nest Audio, Nest Hub/Hub Max, Chromecast, and speaker groups/stereo pairs (subject to sender capabilities)
Sidecar improvements: robust launch flow (handles NOT_ALLOWED by stopping existing sessions and retrying)
Included files

index.html
receiver.js (CAF Receiver initialization + LOAD interceptor for LIVE metadata)
styles.css (dark/purple UI)
logo.svg
index.js (launch retry + metadata fixes)
Important notes

Receiver must be hosted over HTTPS for Cast
Use Default Media Receiver for local testing; register a Custom Receiver App ID for production
Sidecar uses castv2-client (not official Google sender SDK) — group behavior may vary
No Node.js on receiver; no persistent storage guaranteed
How to test (quick)

Serve the receiver folder over HTTPS (e.g. mkcert + npx http-server).
From sender, LOAD:
contentId: https://live.radio1.si/Radio1MB
contentType: audio/mpeg
streamType: LIVE
Known limitations

UI is intentionally lightweight; some CSS effects may be limited on older devices
Sender-side group handling may need the official sender SDK for full reliability
If you want, I can draft the GitHub release body or a short changelog entry formatted for the Releases page.
2025-12-31 08:57:51 +01:00
155 changed files with 190014 additions and 121 deletions

11
android/README.md Normal file
View File

@@ -0,0 +1,11 @@
This folder is not a full Android Studio project.
The buildable Android Studio/Gradle project is generated by Tauri at:
- src-tauri/gen/android
If you haven't generated it yet, run from the repo root:
- .\node_modules\.bin\tauri.cmd android init --ci
Then open `src-tauri/gen/android` in Android Studio and build the APK/AAB.

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>

After

Width:  |  Height:  |  Size: 995 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="206" height="231" viewBox="0 0 206 231">
<!-- Wrapper SVG that embeds the PNG app icon so existing references to tauri.svg render the PNG -->
<image href="appIcon.png" width="206" height="231" preserveAspectRatio="xMidYMid slice" />
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Radio Player</title>
<link rel="stylesheet" href="styles.css">
<script src="main.js" defer type="module"></script>
</head>
<body>
<div class="app-container">
<div class="bg-shape shape-1"></div>
<div class="bg-shape shape-2"></div>
<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>
<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>
</header>
<section class="artwork-section">
<div class="artwork-container">
<div class="artwork-placeholder">
<!-- Gooey SVG filter for fluid blob blending -->
<svg width="0" height="0" style="position:absolute">
<defs>
<filter id="goo">
<!-- increased blur for smoother, more transparent blending -->
<feGaussianBlur in="SourceGraphic" stdDeviation="18" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="goo" />
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</defs>
</svg>
<div class="logo-blobs" aria-hidden="true">
<span class="blob b1"></span>
<span class="blob b2"></span>
<span class="blob b3"></span>
<span class="blob b4"></span>
<span class="blob b5"></span>
<span class="blob b6"></span>
<span class="blob b7"></span>
<span class="blob b8"></span>
<span class="blob b9"></span>
<span class="blob b10"></span>
</div>
<img id="station-logo-img" class="station-logo-img hidden" alt="station logo">
<span class="station-logo-text">1</span>
</div>
</div>
</section>
<section class="track-info">
<h2 id="station-name"></h2>
<p id="station-subtitle"></p>
</section>
<!-- Visual Progress Bar (Live) -->
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill"></div>
<div class="progress-handle"></div>
</div>
</div>
<section class="controls-section">
<button id="prev-btn" class="control-btn secondary" aria-label="Previous Station">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
</svg>
</button>
<button id="play-btn" class="control-btn primary" aria-label="Play">
<div class="icon-container">
<!-- Play Icon -->
<svg id="icon-play" width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
<!-- Stop/Pause Icon (Hidden by default) -->
<svg id="icon-stop" class="hidden" width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h12v12H6z" />
</svg>
</div>
</button>
<button id="next-btn" class="control-btn secondary" aria-label="Next Station">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
</svg>
</button>
</section>
<section class="volume-section">
<button id="mute-btn" class="icon-btn small">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>
</button>
<div class="slider-container">
<input type="range" id="volume-slider" min="0" max="100" value="50">
</div>
<span id="volume-value">50%</span>
</section>
<!-- Hidden Cast Overlay (Beautified) -->
<div id="cast-overlay" class="overlay hidden" aria-hidden="true" data-tauri-drag-region>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="deviceTitle">
<h2 id="deviceTitle">Choose</h2>
<ul id="device-list" class="device-list">
<!-- Render device items here -->
<li class="device">
<div class="device-main">Scanning...</div>
<div class="device-sub">Searching for speakers</div>
</li>
</ul>
<button id="close-overlay" class="btn cancel" type="button">Cancel</button>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,355 @@
const { invoke } = window.__TAURI__.core;
const { getCurrentWindow } = window.__TAURI__.window;
// State
let stations = [];
let currentIndex = 0;
let isPlaying = false;
let currentMode = 'local'; // 'local' | 'cast'
let currentCastDevice = null;
const audio = new Audio();
// UI Elements
const stationNameEl = document.getElementById('station-name');
const stationSubtitleEl = document.getElementById('station-subtitle');
const statusTextEl = document.getElementById('status-text');
const statusDotEl = document.querySelector('.status-dot');
const playBtn = document.getElementById('play-btn');
const iconPlay = document.getElementById('icon-play');
const iconStop = document.getElementById('icon-stop');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const volumeSlider = document.getElementById('volume-slider');
const volumeValue = document.getElementById('volume-value');
const castBtn = document.getElementById('cast-toggle-btn');
const castOverlay = document.getElementById('cast-overlay');
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');
// Init
async function init() {
await loadStations();
setupEventListeners();
updateUI();
}
async function loadStations() {
try {
const resp = await fetch('stations.json');
const raw = await resp.json();
// Normalize station objects so the rest of the app can rely on `name` and `url`.
stations = raw
.map((s) => {
// If already in the old format, keep as-is
if (s.name && s.url) return s;
const name = s.title || s.id || s.name || 'Unknown';
// Prefer liveAudio, fall back to liveVideo or any common fields
const url = s.liveAudio || s.liveVideo || s.liveStream || s.url || '';
return {
id: s.id || name,
name,
url,
logo: s.logo || s.poster || '',
enabled: typeof s.enabled === 'boolean' ? s.enabled : true,
raw: s,
};
})
// Filter out disabled stations and those without a stream URL
.filter((s) => s.enabled !== false && s.url && s.url.length > 0);
if (stations.length > 0) {
currentIndex = 0;
loadStation(currentIndex);
}
} catch (e) {
console.error('Failed to load stations', e);
statusTextEl.textContent = 'Error loading stations';
}
}
function setupEventListeners() {
playBtn.addEventListener('click', togglePlay);
prevBtn.addEventListener('click', playPrev);
nextBtn.addEventListener('click', playNext);
volumeSlider.addEventListener('input', handleVolumeInput);
castBtn.addEventListener('click', openCastOverlay);
closeOverlayBtn.addEventListener('click', closeCastOverlay);
// Close overlay on background click
castOverlay.addEventListener('click', (e) => {
if (e.target === castOverlay) closeCastOverlay();
});
// Close button
document.getElementById('close-btn').addEventListener('click', async () => {
const appWindow = getCurrentWindow();
await appWindow.close();
});
// Menu button - explicit functionality or placeholder?
// For now just log or maybe show about
document.getElementById('menu-btn').addEventListener('click', () => {
openStationsOverlay();
});
// Hotkeys?
}
function loadStation(index) {
if (index < 0 || index >= stations.length) return;
const station = stations[index];
stationNameEl.textContent = station.name;
stationSubtitleEl.textContent = currentMode === 'cast' ? `Casting to ${currentCastDevice}` : 'Live Stream';
// Update Logo Text (First letter or number)
// 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');
} else {
// Fallback to single-letter/logo text
logoImgEl.src = '';
logoImgEl.classList.add('hidden');
const numberMatch = station.name.match(/\d+/);
if (numberMatch) {
logoTextEl.textContent = numberMatch[0];
} else {
logoTextEl.textContent = station.name.charAt(0).toUpperCase();
}
logoTextEl.classList.remove('hidden');
}
}
async function togglePlay() {
if (isPlaying) {
await stop();
} else {
await play();
}
}
async function play() {
const station = stations[currentIndex];
if (!station) return;
statusTextEl.textContent = 'Buffering...';
statusDotEl.style.backgroundColor = 'var(--text-muted)'; // Grey/Yellow while loading
if (currentMode === 'local') {
audio.src = station.url;
audio.volume = volumeSlider.value / 100;
try {
await audio.play();
isPlaying = true;
updateUI();
} catch (e) {
console.error('Playback failed', e);
statusTextEl.textContent = 'Error';
}
} else if (currentMode === 'cast' && currentCastDevice) {
// Cast logic
try {
await invoke('cast_play', { deviceName: currentCastDevice, url: station.url });
isPlaying = true;
// Sync volume
const vol = volumeSlider.value / 100;
invoke('cast_set_volume', { deviceName: currentCastDevice, volume: vol });
updateUI();
} catch (e) {
console.error('Cast failed', e);
statusTextEl.textContent = 'Cast Error';
currentMode = 'local'; // Fallback
updateUI();
}
}
}
async function stop() {
if (currentMode === 'local') {
audio.pause();
audio.src = '';
} else if (currentMode === 'cast' && currentCastDevice) {
try {
await invoke('cast_stop', { deviceName: currentCastDevice });
} catch (e) {
console.error(e);
}
}
isPlaying = false;
updateUI();
}
async function playNext() {
if (stations.length === 0) return;
// If playing, stop first? Or seamless?
// For radio, seamless switch requires stop then play new URL
const wasPlaying = isPlaying;
if (wasPlaying) await stop();
currentIndex = (currentIndex + 1) % stations.length;
loadStation(currentIndex);
if (wasPlaying) await play();
}
async function playPrev() {
if (stations.length === 0) return;
const wasPlaying = isPlaying;
if (wasPlaying) await stop();
currentIndex = (currentIndex - 1 + stations.length) % stations.length;
loadStation(currentIndex);
if (wasPlaying) await play();
}
function updateUI() {
// Play/Stop Button
if (isPlaying) {
iconPlay.classList.add('hidden');
iconStop.classList.remove('hidden');
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';
} 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';
}
}
function handleVolumeInput() {
const val = volumeSlider.value;
volumeValue.textContent = `${val}%`;
const decimals = val / 100;
if (currentMode === 'local') {
audio.volume = decimals;
} else if (currentMode === 'cast' && currentCastDevice) {
invoke('cast_set_volume', { deviceName: currentCastDevice, volume: decimals });
}
}
// Cast Logic
async function openCastOverlay() {
castOverlay.classList.remove('hidden');
castOverlay.setAttribute('aria-hidden', 'false');
deviceListEl.innerHTML = '<li class="device"><div class="device-main">Scanning...</div><div class="device-sub">Searching for speakers</div></li>';
try {
const devices = await invoke('list_cast_devices');
deviceListEl.innerHTML = '';
// Add "This Computer" option
const localLi = document.createElement('li');
localLi.className = 'device' + (currentMode === 'local' ? ' selected' : '');
localLi.innerHTML = '<div class="device-main">This Computer</div><div class="device-sub">Local Playback</div>';
localLi.onclick = () => selectCastDevice(null);
deviceListEl.appendChild(localLi);
if (devices.length > 0) {
devices.forEach(d => {
const li = document.createElement('li');
li.className = 'device' + (currentMode === 'cast' && currentCastDevice === d ? ' selected' : '');
li.innerHTML = `<div class="device-main">${d}</div><div class="device-sub">Google Cast Speaker</div>`;
li.onclick = () => selectCastDevice(d);
deviceListEl.appendChild(li);
});
}
} catch (e) {
deviceListEl.innerHTML = `<li class="device"><div class="device-main">Error</div><div class="device-sub">${e}</div></li>`;
}
}
function closeCastOverlay() {
castOverlay.classList.add('hidden');
castOverlay.setAttribute('aria-hidden', 'true');
}
async function selectCastDevice(deviceName) {
closeCastOverlay();
// If checking same device, do nothing
if (deviceName === currentCastDevice) return;
// If switching mode, stop current playback
if (isPlaying) {
await stop();
}
if (deviceName) {
currentMode = 'cast';
currentCastDevice = deviceName;
castBtn.style.color = 'var(--success)';
} else {
currentMode = 'local';
currentCastDevice = null;
castBtn.style.color = 'var(--text-main)';
}
updateUI();
// Auto-play if we were playing? Let's stay stopped to be safe/explicit
// Or auto-play for better UX?
// Let's prompt user to play.
}
window.addEventListener('DOMContentLoaded', init);
// Open overlay and show list of stations (used by menu/hamburger)
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.innerHTML = '<li class="device"><div class="device-main">No stations found</div><div class="device-sub">Check your stations.json</div></li>';
return;
}
deviceListEl.innerHTML = '';
stations.forEach((s, idx) => {
const li = document.createElement('li');
li.className = 'device' + (currentIndex === idx ? ' selected' : '');
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>`;
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;
loadStation(currentIndex);
closeCastOverlay();
try {
await play();
} catch (e) {
console.error('Failed to play station from menu', e);
}
};
deviceListEl.appendChild(li);
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,629 @@
:root {
--bg-gradient: linear-gradient(135deg, #7b7fd8, #b57cf2);
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--accent: #dfa6ff;
--accent-glow: rgba(223, 166, 255, 0.5);
--text-main: #ffffff;
--text-muted: rgba(255, 255, 255, 0.7);
--danger: #cf6679;
--success: #7dffb3;
--card-radius: 10px;
}
* {
box-sizing: border-box;
user-select: none;
-webkit-user-drag: none;
cursor: default;
}
/* Show pointer cursor for interactive / clickable elements (override global default) */
a, a[href], button, input[type="button"], input[type="submit"],
[role="button"], [onclick], .clickable, .icon-btn, .control-btn, label[for],
.station-item, [tabindex]:not([tabindex="-1"]) {
cursor: pointer !important;
}
/* Hide Scrollbars */
::-webkit-scrollbar {
display: none;
}
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
background: linear-gradient(-45deg, #7b7fd8, #b57cf2, #8b5cf6, #6930c3, #7b7fd8);
background-size: 400% 400%;
animation: gradientShift 12s ease-in-out infinite;
font-family: 'Segoe UI', system-ui, sans-serif;
color: var(--text-main);
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
25% {
background-position: 100% 50%;
}
50% {
background-position: 50% 100%;
}
75% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* Background Blobs */
.bg-shape {
position: absolute;
border-radius: 50%;
filter: blur(60px);
z-index: 0;
opacity: 0.6;
animation: float 10s infinite alternate;
}
.shape-1 {
width: 300px;
height: 300px;
background: #5e60ce;
top: -50px;
left: -50px;
}
.shape-2 {
width: 250px;
height: 250px;
background: #ff6bf0;
bottom: -50px;
right: -50px;
animation-delay: -5s;
}
@keyframes float {
0% { transform: translate(0, 0); }
100% { transform: translate(30px, 30px); }
}
.app-container {
width: 100%;
height: 100%;
position: relative;
padding: 10px; /* Slight padding from window edges if desired, or 0 */
}
.glass-card {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
backdrop-filter: blur(24px);
border-radius: var(--card-radius);
display: flex;
flex-direction: column;
padding: 24px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
}
/* Header */
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
-webkit-app-region: drag; /* Draggable area */
}
.header-info {
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.app-title {
font-weight: 600;
font-size: 1rem;
color: var(--text-main);
}
.status-indicator {
font-size: 0.8rem;
color: var(--success);
margin-top: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 6px;
height: 6px;
background-color: var(--success);
border-radius: 50%;
box-shadow: 0 0 8px var(--success);
}
.icon-btn {
background: none;
border: none;
color: var(--text-main);
padding: 8px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
-webkit-app-region: no-drag; /* Buttons clickable */
}
.icon-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.header-buttons {
display: flex;
gap: 4px;
align-items: center;
-webkit-app-region: no-drag;
}
.close-btn:hover {
background: rgba(207, 102, 121, 0.3) !important;
color: var(--danger);
}
/* Artwork */
.artwork-section {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.artwork-container {
width: 220px;
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);
}
.artwork-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4ea8de, #6930c3);
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
}
.artwork-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #4ea8de, #6930c3);
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
}
.station-logo-text {
font-size: 5rem;
font-weight: 800;
font-style: italic;
color: rgba(255,255,255,0.9);
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
position: relative;
z-index: 3;
}
.station-logo-img {
/* Fill the artwork placeholder while keeping aspect ratio and inner padding */
width: 100%;
height: 100%;
object-fit: contain;
display: block;
padding: 12px; /* inner spacing from rounded edges */
box-sizing: border-box;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0,0,0,0.35);
position: relative;
z-index: 3;
}
/* Logo blobs container sits behind logo but inside artwork placeholder */
.logo-blobs {
position: absolute;
inset: 0;
filter: url(#goo);
z-index: 1;
pointer-events: none;
}
/* Make artwork/logo clickable: show pointer cursor */
.artwork-placeholder,
.artwork-placeholder:hover,
.station-logo-img,
.station-logo-text {
cursor: pointer !important;
pointer-events: auto;
}
/* Subtle hover affordance to make clickability clearer */
.artwork-placeholder:hover .station-logo-img,
.artwork-placeholder:hover .station-logo-text {
transform: scale(1.03);
transition: transform 160ms ease;
}
.blob {
position: absolute;
border-radius: 50%;
/* more transparent overall */
opacity: 0.18;
/* slightly smaller blur for subtle definition */
filter: blur(6px);
}
.b1 { width: 110px; height: 110px; left: 8%; top: 20%; background: radial-gradient(circle at 30% 30%, #c77dff, #8b5cf6); animation: float1 6s ease-in-out infinite; }
.b2 { width: 85px; height: 85px; right: 6%; top: 10%; background: radial-gradient(circle at 30% 30%, #7bffd1, #7dffb3); animation: float2 5.5s ease-in-out infinite; }
.b3 { width: 95px; height: 95px; left: 20%; bottom: 12%; background: radial-gradient(circle at 20% 20%, #ffd07a, #ff6bf0); animation: float3 7s ease-in-out infinite; }
.b4 { width: 70px; height: 70px; right: 24%; bottom: 18%; background: radial-gradient(circle at 30% 30%, #6bd3ff, #4ea8de); animation: float4 6.5s ease-in-out infinite; }
.b5 { width: 50px; height: 50px; left: 46%; top: 36%; background: radial-gradient(circle at 40% 40%, #ffa6d6, #c77dff); animation: float5 8s ease-in-out infinite; }
/* Additional blobs */
.b6 { width: 75px; height: 75px; left: 12%; top: 48%; background: radial-gradient(circle at 30% 30%, #bde7ff, #6bd3ff); animation: float6 6.8s ease-in-out infinite; }
.b7 { width: 42px; height: 42px; right: 10%; top: 42%; background: radial-gradient(circle at 40% 40%, #ffd9b3, #ffd07a); animation: float7 7.2s ease-in-out infinite; }
.b8 { width: 70px; height: 70px; left: 34%; bottom: 8%; background: radial-gradient(circle at 30% 30%, #e3b6ff, #c77dff); animation: float8 6.4s ease-in-out infinite; }
.b9 { width: 36px; height: 36px; right: 34%; bottom: 6%; background: radial-gradient(circle at 30% 30%, #9ef7d3, #7bffd1); animation: float9 8.4s ease-in-out infinite; }
.b10 { width: 30px; height: 30px; left: 52%; bottom: 28%; background: radial-gradient(circle at 30% 30%, #ffd0f0, #ffa6d6); animation: float10 5.8s ease-in-out infinite; }
@keyframes float1 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(12px) translateX(8px) scale(1.06); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float2 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-10px) translateX(-6px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float3 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(8px) translateX(-10px) scale(1.05); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float4 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-6px) translateX(10px) scale(1.03); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float5 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-12px) translateX(4px) scale(1.07); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float6 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-8px) translateX(6px) scale(1.05); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float7 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(10px) translateX(-6px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float8 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-6px) translateX(10px) scale(1.03); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float9 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(12px) translateX(-4px) scale(1.06); } 100% { transform: translateY(0) translateX(0) scale(1); } }
@keyframes float10 { 0% { transform: translateY(0) translateX(0) scale(1); } 50% { transform: translateY(-10px) translateX(2px) scale(1.04); } 100% { transform: translateY(0) translateX(0) scale(1); } }
/* Slightly darken backdrop gradient so blobs read better */
.artwork-placeholder::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.06), rgba(0,0,0,0.12));
z-index: 0;
}
/* Track Info */
.track-info {
text-align: center;
margin-bottom: 20px;
}
.track-info h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.track-info p {
margin: 6px 0 0;
color: var(--text-muted);
font-size: 0.95rem;
}
/* Progress Bar (Visual) */
.progress-container {
width: 100%;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
margin-bottom: 30px;
position: relative;
}
.progress-fill {
width: 100%; /* Live always full or pulsing */
height: 100%;
background: linear-gradient(90deg, var(--accent), #fff);
border-radius: 2px;
opacity: 0.8;
box-shadow: 0 0 10px var(--accent-glow);
}
.progress-handle {
position: absolute;
right: 0;
top: 50%;
transform: translate(50%, -50%);
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px rgba(255,255,255,0.8);
}
/* Controls */
.controls-section {
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
margin-bottom: 30px;
}
.control-btn {
background: none;
border: none;
color: var(--text-main);
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.control-btn:active {
transform: scale(0.9);
}
.control-btn.secondary {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.control-btn.primary {
width: 72px;
height: 72px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05));
border: 1px solid rgba(255,255,255,0.3);
box-shadow: 0 8px 20px rgba(0,0,0,0.2), inset 0 0 10px rgba(255,255,255,0.1);
color: #fff;
}
.control-btn.primary svg {
filter: drop-shadow(0 0 5px var(--accent-glow));
}
/* Playing state - pulsing glow ring */
.control-btn.primary.playing {
animation: pulse-ring 2s ease-in-out infinite;
}
@keyframes pulse-ring {
0%, 100% {
box-shadow: 0 8px 20px rgba(0,0,0,0.2),
inset 0 0 10px rgba(255,255,255,0.1),
0 0 0 0 rgba(223, 166, 255, 0.7);
}
50% {
box-shadow: 0 8px 20px rgba(0,0,0,0.2),
inset 0 0 10px rgba(255,255,255,0.1),
0 0 0 8px rgba(223, 166, 255, 0);
}
}
/* Icon container prevents layout jump */
.icon-container {
position: relative;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-container svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.hidden {
display: none !important;
}
/* Volume */
.volume-section {
display: flex;
align-items: center;
gap: 12px;
margin-top: auto;
padding: 0 10px;
}
.slider-container {
flex: 1;
}
input[type=range] {
width: 100%;
background: transparent;
-webkit-appearance: none;
appearance: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255,255,255,0.2);
border-radius: 2px;
}
input[type=range]::-webkit-slider-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
background: #ffffff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -6px; /* align with track */
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
#volume-value {
font-size: 0.8rem;
font-weight: 500;
width: 30px;
text-align: right;
}
.icon-btn.small {
padding: 0;
width: 24px;
height: 24px;
}
/* Cast Overlay (Beautified as per layout2_plan.md) */
.overlay {
position: fixed;
inset: 0;
background: rgba(20, 10, 35, 0.45);
backdrop-filter: blur(14px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.overlay:not(.hidden) {
opacity: 1;
pointer-events: auto;
}
/* Modal */
.modal {
width: min(420px, calc(100vw - 48px));
padding: 22px;
border-radius: 22px;
background: rgba(30, 30, 40, 0.82);
border: 1px solid rgba(255,255,255,0.12);
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
color: #fff;
animation: pop 0.22s ease;
-webkit-app-region: no-drag;
}
@keyframes pop {
from { transform: scale(0.94); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal h2 {
margin: 0 0 14px;
text-align: center;
font-size: 20px;
}
/* Device list */
.device-list {
list-style: none;
padding: 10px 5px;
margin: 0 0 18px;
max-height: 360px;
overflow-y: auto;
}
/* Device row */
.device {
padding: 12px 14px;
border-radius: 14px;
margin-bottom: 8px;
cursor: pointer;
background: rgba(255,255,255,0.05);
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
text-align: left;
}
.device:hover {
background: rgba(255,255,255,0.10);
transform: translateY(-1px);
}
.device .device-main {
font-size: 15px;
font-weight: 600;
color: var(--text-main);
}
.device .device-sub {
margin-top: 3px;
font-size: 12px;
opacity: 0.7;
color: var(--text-muted);
}
/* Selected device */
.device.selected {
background: linear-gradient(135deg, #c77dff, #8b5cf6);
box-shadow: 0 0 18px rgba(199,125,255,0.65);
color: #111;
}
.device.selected .device-main,
.device.selected .device-sub {
color: #111;
}
.device.selected .device-sub {
opacity: 0.85;
}
/* Cancel button */
.btn.cancel {
width: 100%;
padding: 12px;
border-radius: 999px;
border: none;
background: #d16b7d;
color: #fff;
font-size: 15px;
cursor: pointer;
transition: transform 0.15s ease, background 0.2s;
font-weight: 600;
}
.btn.cancel:hover {
transform: scale(1.02);
background: #e17c8d;
}

View File

@@ -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 ] │
@@ -64,7 +64,7 @@ Single centered player card:
### Header
* Title: `Radio1 Player`
* Title: `RadioPlayer`
* Status indicator:
* `● Ready`

View File

@@ -19,7 +19,7 @@
<img src="assets/logo.svg" alt="Radio Player" />
</div>
<p id="station">Radio 1 Live Stream</p>
<p id="station">Radio Live Stream</p>
</div>
<script src="receiver.js"></script>

206
scripts/build-android.ps1 Normal file
View File

@@ -0,0 +1,206 @@
<#
Build helper for Android (Windows PowerShell)
What it does:
- Checks for required commands (`npm`, `rustup`, `cargo`, `cargo-ndk`)
- Builds frontend (runs `npm run build` if `dist`/`build` not present)
- Copies frontend files from `dist` or `src` into `android/app/src/main/assets`
- Builds Rust native libs using `cargo-ndk` (if available) for `aarch64` and `armv7`
- Copies produced `.so` files into `android/app/src/main/jniLibs/*`
Note: This script prepares the Android project. To produce the APK, open `android/` in Android Studio and run Build -> Assemble, or run `gradlew assembleDebug` locally.
#>
Set-StrictMode -Version Latest
function Check-Command($name) {
$which = Get-Command $name -ErrorAction SilentlyContinue
return $which -ne $null
}
Write-Output "Starting Android prep script..."
if (-not (Check-Command npm)) { Write-Warning "npm not found in PATH. Install Node.js to build frontend." }
if (-not (Check-Command rustup)) { Write-Warning "rustup not found in PATH. Install Rust toolchain." }
if (-not (Check-Command cargo)) { Write-Warning "cargo not found in PATH." }
$cargoNdkAvailable = Check-Command cargo-ndk
if (-not $cargoNdkAvailable) { Write-Warning "cargo-ndk not found. Native libs will not be built. Install via 'cargo install cargo-ndk'" }
# Determine repository root (parent of the scripts folder)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$root = Split-Path -Parent $scriptDir
Push-Location $root
# Prefer Tauri-generated Android Studio project (tauri android init)
$androidRoot = Join-Path $root 'src-tauri\gen\android'
if (-not (Test-Path $androidRoot)) {
# Legacy fallback (non-Tauri project)
$androidRoot = Join-Path $root 'android'
}
function Escape-LocalPropertiesPath([string]$p) {
# local.properties expects ':' escaped and backslashes doubled on Windows.
# Use plain string replacements to avoid regex escaping pitfalls.
return ($p.Replace('\', '\\').Replace(':', '\:'))
}
# Ensure Android SDK/NDK locations are set for Gradle (local.properties)
$sdkRoot = $env:ANDROID_SDK_ROOT
if (-not $sdkRoot) { $sdkRoot = $env:ANDROID_HOME }
if (-not $sdkRoot) { $sdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
$ndkRoot = $env:ANDROID_NDK_ROOT
if (-not $ndkRoot) { $ndkRoot = $env:ANDROID_NDK_HOME }
if (-not $ndkRoot -and (Test-Path (Join-Path $sdkRoot 'ndk'))) {
$ndkVersions = Get-ChildItem -Path (Join-Path $sdkRoot 'ndk') -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending
if ($ndkVersions -and (@($ndkVersions)).Count -gt 0) { $ndkRoot = @($ndkVersions)[0].FullName }
}
if (Test-Path $androidRoot) {
$localPropsPath = Join-Path $androidRoot 'local.properties'
$lines = @()
if ($sdkRoot) { $lines += "sdk.dir=$(Escape-LocalPropertiesPath $sdkRoot)" }
if ($ndkRoot) { $lines += "ndk.dir=$(Escape-LocalPropertiesPath $ndkRoot)" }
if ($lines.Count -gt 0) {
Set-Content -Path $localPropsPath -Value ($lines -join "`n") -Encoding ASCII
Write-Output "Wrote Android SDK/NDK config to: $localPropsPath"
}
}
# Build frontend (optional)
Write-Output "Preparing frontend files..."
$distDirs = @('dist','build')
$foundDist = $null
foreach ($d in $distDirs) {
if (Test-Path (Join-Path $root $d)) { $foundDist = $d; break }
}
if (-not $foundDist) {
# IMPORTANT: `npm run build` in this repo runs `tauri build`, which is a desktop bundling step.
# For Android prep we only need web assets, so we fall back to copying `src/` as assets.
Write-Warning "No dist/build output found — copying `src/` as assets (skipping `npm run build` to avoid desktop bundling)."
}
$assetsDst = Join-Path $androidRoot 'app\src\main\assets'
if (-not (Test-Path $assetsDst)) { New-Item -ItemType Directory -Path $assetsDst -Force | Out-Null }
if ($foundDist) {
Write-Output "Copying frontend from '$foundDist' to Android assets..."
robocopy (Join-Path $root $foundDist) $assetsDst /MIR | Out-Null
} else {
Write-Output "Copying raw 'src' to Android assets..."
robocopy (Join-Path $root 'src') $assetsDst /MIR | Out-Null
}
# Build native libs if cargo-ndk available
if ($cargoNdkAvailable) {
Write-Output "Building Rust native libs via cargo-ndk from project root: $root"
try {
# Build from the Rust crate directory `src-tauri`
$crateDir = Join-Path $root 'src-tauri'
if (-not (Test-Path (Join-Path $crateDir 'Cargo.toml'))) {
Write-Warning "Cargo.toml not found in src-tauri; skipping native build."
} else {
# Prefer Ninja generator for CMake if available (avoids Visual Studio generator issues)
# Restore env vars at the end so we don't pollute the current PowerShell session.
$oldCmakeGenerator = $env:CMAKE_GENERATOR
$oldCmakeMakeProgram = $env:CMAKE_MAKE_PROGRAM
$ninjaCmd = Get-Command ninja -ErrorAction SilentlyContinue
if ($ninjaCmd) {
Write-Output "Ninja detected at $($ninjaCmd.Source); setting CMake generator to Ninja."
$env:CMAKE_GENERATOR = 'Ninja'
$env:CMAKE_MAKE_PROGRAM = $ninjaCmd.Source
} else {
Write-Warning "Ninja not found in PATH. Installing Ninja or adding it to PATH is strongly recommended to avoid Visual Studio CMake generator on Windows."
}
# Attempt to locate Android NDK if environment variables are not set
if (-not $env:ANDROID_NDK_ROOT -and -not $env:ANDROID_NDK_HOME) {
$candidates = @()
if ($env:ANDROID_SDK_ROOT) { $candidates += Join-Path $env:ANDROID_SDK_ROOT 'ndk' }
if ($env:ANDROID_HOME) { $candidates += Join-Path $env:ANDROID_HOME 'ndk' }
$candidates += Join-Path $env:LOCALAPPDATA 'Android\sdk\ndk'
$candidates += Join-Path $env:USERPROFILE 'AppData\Local\Android\sdk\ndk'
$candidates += 'C:\Program Files (x86)\Android\AndroidNDK'
foreach ($cand in $candidates) {
if (Test-Path $cand) {
$versions = Get-ChildItem -Path $cand -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending
if ($versions -and (@($versions)).Count -gt 0) {
$ndkPath = @($versions)[0].FullName
Write-Output "Detected Android NDK at: $ndkPath"
$env:ANDROID_NDK_ROOT = $ndkPath
$env:ANDROID_NDK = $ndkPath
break
}
}
}
if (-not $env:ANDROID_NDK_ROOT) { Write-Warning "ANDROID_NDK_ROOT/ANDROID_NDK not set and no NDK found in common locations. Set ANDROID_NDK_ROOT to your NDK path." }
} else {
Write-Output "Using existing ANDROID_NDK_ROOT: $($env:ANDROID_NDK_ROOT)"
if (-not $env:ANDROID_NDK) { $env:ANDROID_NDK = $env:ANDROID_NDK_ROOT }
}
# Ensure expected external binary placeholders exist so Tauri bundling doesn't fail
$binariesDir = Join-Path $crateDir 'binaries'
if (-not (Test-Path $binariesDir)) { New-Item -ItemType Directory -Path $binariesDir -Force | Out-Null }
$placeholder1 = Join-Path $binariesDir 'RadioPlayer-aarch64-linux-android'
$placeholder2 = Join-Path $binariesDir 'RadioPlayer-armv7-linux-androideabi'
if (-not (Test-Path $placeholder1)) { New-Item -ItemType File -Path $placeholder1 -Force | Out-Null; Write-Output "Created placeholder: $placeholder1" }
if (-not (Test-Path $placeholder2)) { New-Item -ItemType File -Path $placeholder2 -Force | Out-Null; Write-Output "Created placeholder: $placeholder2" }
# If a previous build used a different CMake generator (e.g., Visual Studio), aws-lc-sys can fail with
# "Does not match the generator used previously". Clean only the aws-lc-sys CMake build dirs.
$awsLcBuildDirs = Get-ChildItem -Path (Join-Path $crateDir 'target') -Recurse -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like 'aws-lc-sys-*' }
foreach ($d in @($awsLcBuildDirs)) {
$cmakeBuildDir = Join-Path $d.FullName 'out\build'
$cmakeCache = Join-Path $cmakeBuildDir 'CMakeCache.txt'
if (Test-Path $cmakeCache) {
Write-Output "Cleaning stale CMake cache for aws-lc-sys: $cmakeBuildDir"
Remove-Item -Path $cmakeBuildDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
Push-Location $crateDir
try {
# Use API 24 to ensure libc symbols like getifaddrs/freeifaddrs are available.
# Build only the library to avoid linking the desktop binary for Android.
Write-Output "Running: cargo ndk -t arm64-v8a -t armeabi-v7a -P 24 build --release --lib (in $crateDir)"
cargo ndk -t arm64-v8a -t armeabi-v7a -P 24 build --release --lib
} finally {
Pop-Location
if ($null -eq $oldCmakeGenerator) { Remove-Item Env:\CMAKE_GENERATOR -ErrorAction SilentlyContinue } else { $env:CMAKE_GENERATOR = $oldCmakeGenerator }
if ($null -eq $oldCmakeMakeProgram) { Remove-Item Env:\CMAKE_MAKE_PROGRAM -ErrorAction SilentlyContinue } else { $env:CMAKE_MAKE_PROGRAM = $oldCmakeMakeProgram }
}
# Search for produced .so files under src-tauri/target
$soFiles = Get-ChildItem -Path (Join-Path $crateDir 'target') -Recurse -Filter "*.so" -ErrorAction SilentlyContinue
if (-not $soFiles) {
Write-Warning "No .so files found after build. Check cargo-ndk output above for errors."
} else {
foreach ($f in @($soFiles)) {
$full = $f.FullName
if ($full -match 'aarch64|aarch64-linux-android|arm64-v8a') { $abi = 'arm64-v8a' }
elseif ($full -match 'armv7|armv7-linux-androideabi|armeabi-v7a') { $abi = 'armeabi-v7a' }
else { continue }
$dst = Join-Path $androidRoot "app\src\main\jniLibs\$abi"
if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst -Force | Out-Null }
Copy-Item $full -Destination $dst -Force
Write-Output "Copied $($f.Name) -> $dst"
}
}
}
} catch {
Write-Warning "cargo-ndk build failed. Exception: $($_.Exception.Message)"
if ($_.ScriptStackTrace) { Write-Output $_.ScriptStackTrace }
}
} else {
Write-Warning "Skipping native lib build (cargo-ndk missing)."
}
Write-Output "Android prep complete. Open '$androidRoot' in Android Studio and build the APK (or run './gradlew assembleDebug' in that folder)."
Pop-Location

45
scripts/build-android.sh Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# Cross-platform helper for Unix-like shells
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
echo "Preparing Android assets and native libs..."
if command -v npm >/dev/null 2>&1; then
echo "Running npm install & build"
npm install
npm run build || true
fi
DIST_DIR="dist"
if [ ! -d "$DIST_DIR" ]; then DIST_DIR="build"; fi
if [ -d "$DIST_DIR" ]; then
echo "Copying $DIST_DIR -> android/app/src/main/assets"
mkdir -p android/app/src/main/assets
rsync -a --delete "$DIST_DIR/" android/app/src/main/assets/
else
echo "No dist/build found, copying src/ -> android assets"
mkdir -p android/app/src/main/assets
rsync -a --delete src/ android/app/src/main/assets/
fi
if command -v cargo-ndk >/dev/null 2>&1; then
echo "Building native libs with cargo-ndk"
cargo-ndk -t aarch64 -t armv7 build --release || true
# copy so files
find target -type f -name "*.so" | while read -r f; do
if [[ "$f" =~ aarch64|aarch64-linux-android ]]; then abi=arm64-v8a; fi
if [[ "$f" =~ armv7|armv7-linux-androideabi ]]; then abi=armeabi-v7a; fi
if [ -n "${abi-}" ]; then
mkdir -p android/app/src/main/jniLibs/$abi
cp "$f" android/app/src/main/jniLibs/$abi/
echo "Copied $f -> android/app/src/main/jniLibs/$abi/"
fi
done
else
echo "cargo-ndk not found; skipping native lib build"
fi
echo "Prepared Android project. Open android/ in Android Studio to build the APK (or run ./gradlew assembleDebug)."

View File

@@ -175,7 +175,7 @@ function loadMedia(url) {
streamType: 'LIVE',
metadata: {
metadataType: 0,
title: 'Radio 1'
title: 'RadioPlayer'
}
};

461
src-tauri/Cargo.lock generated
View File

@@ -550,7 +550,7 @@ dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"core-graphics-types",
"foreign-types",
"foreign-types 0.5.0",
"libc",
]
@@ -837,7 +837,7 @@ dependencies = [
"rustc_version",
"toml 0.9.10+spec-1.1.0",
"vswhom",
"winreg",
"winreg 0.55.0",
]
[[package]]
@@ -988,6 +988,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -995,7 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
"foreign-types-shared 0.3.1",
]
[[package]]
@@ -1009,6 +1018,12 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
@@ -1425,6 +1440,25 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "h2"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.12.1",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1482,6 +1516,17 @@ dependencies = [
"match_token",
]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http"
version = "1.4.0"
@@ -1492,6 +1537,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
@@ -1499,7 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http",
"http 1.4.0",
]
[[package]]
@@ -1510,8 +1566,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http 1.4.0",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -1521,6 +1577,36 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "0.14.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.8.1"
@@ -1531,8 +1617,8 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
"http",
"http-body",
"http 1.4.0",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
@@ -1542,6 +1628,33 @@ dependencies = [
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"rustls 0.21.12",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "hyper-util"
version = "0.1.19"
@@ -1553,14 +1666,14 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"http 1.4.0",
"http-body 1.0.1",
"hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
@@ -2039,7 +2152,7 @@ dependencies = [
"log",
"mio",
"socket-pktinfo",
"socket2",
"socket2 0.6.1",
]
[[package]]
@@ -2106,6 +2219,23 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2428,12 +2558,50 @@ dependencies = [
"pathdiff",
]
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2907,6 +3075,7 @@ name = "radio-tauri"
version = "0.1.0"
dependencies = [
"mdns-sd",
"reqwest 0.11.27",
"rust_cast",
"serde",
"serde_json",
@@ -3073,6 +3242,50 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-rustls",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"winreg 0.50.0",
]
[[package]]
name = "reqwest"
version = "0.12.28"
@@ -3083,10 +3296,10 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper",
"hyper 1.8.1",
"hyper-util",
"js-sys",
"log",
@@ -3095,7 +3308,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tokio-util",
"tower",
@@ -3132,7 +3345,7 @@ dependencies = [
"log",
"protobuf",
"protobuf-codegen",
"rustls",
"rustls 0.23.35",
"rustls-native-certs",
"serde",
"serde_derive",
@@ -3174,6 +3387,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.35"
@@ -3184,7 +3409,7 @@ dependencies = [
"log",
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"rustls-webpki 0.103.8",
"subtle",
"zeroize",
]
@@ -3196,12 +3421,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@@ -3220,6 +3454,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
@@ -3319,6 +3563,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@@ -3636,10 +3890,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927136cc2ae6a1b0e66ac6b1210902b75c3f726db004a73bc18686dcd0dcd22f"
dependencies = [
"libc",
"socket2",
"socket2 0.6.1",
"windows-sys 0.60.2",
]
[[package]]
name = "socket2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.1"
@@ -3789,6 +4053,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -3809,6 +4079,27 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -3895,7 +4186,7 @@ dependencies = [
"glob",
"gtk",
"heck 0.5.0",
"http",
"http 1.4.0",
"jni",
"libc",
"log",
@@ -3909,7 +4200,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.12.28",
"serde",
"serde_json",
"serde_repr",
@@ -4062,7 +4353,7 @@ dependencies = [
"cookie",
"dpi",
"gtk",
"http",
"http 1.4.0",
"jni",
"objc2",
"objc2-ui-kit",
@@ -4085,7 +4376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
dependencies = [
"gtk",
"http",
"http 1.4.0",
"jni",
"log",
"objc2",
@@ -4118,7 +4409,7 @@ dependencies = [
"dunce",
"glob",
"html5ever",
"http",
"http 1.4.0",
"infer",
"json-patch",
"kuchikiki",
@@ -4271,7 +4562,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.6.1",
"tokio-macros",
"windows-sys 0.61.2",
]
@@ -4287,6 +4578,26 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.17"
@@ -4405,7 +4716,7 @@ dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
@@ -4420,8 +4731,8 @@ dependencies = [
"bitflags 2.10.0",
"bytes",
"futures-util",
"http",
"http-body",
"http 1.4.0",
"http-body 1.0.1",
"iri-string",
"pin-project-lite",
"tower",
@@ -4630,6 +4941,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.1"
@@ -4827,6 +5144,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webview2-com"
version = "0.38.0"
@@ -5069,6 +5392,15 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -5120,6 +5452,21 @@ dependencies = [
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -5177,6 +5524,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -5195,6 +5548,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -5213,6 +5572,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -5243,6 +5608,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -5261,6 +5632,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -5279,6 +5656,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -5297,6 +5680,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@@ -5327,6 +5716,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
@@ -5365,7 +5764,7 @@ dependencies = [
"gdkx11",
"gtk",
"html5ever",
"http",
"http 1.4.0",
"javascriptcore-rs",
"jni",
"kuchikiki",

View File

@@ -26,4 +26,5 @@ rust_cast = "0.19.0"
mdns-sd = "0.17.1"
tokio = { version = "1.48.0", features = ["full"] }
tauri-plugin-shell = "2.3.3"
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }

View File

@@ -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
|

View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

19
src-tauri/gen/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
/.tauri
/tauri.settings.gradle

6
src-tauri/gen/android/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/src/main/java/si/klevze/radioPlayer/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties

View File

@@ -0,0 +1,64 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 36
namespace = "si.klevze.radioPlayer"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "si.klevze.radioPlayer"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.radio_tauri"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,225 @@
x64:
firstOrDefaultFilePatterns:
- '!**/node_modules'
- '!build{,/**/*}'
- '!dist{,/**/*}'
- electron/**/*
- src/**/*
- receiver/**/*
- package.json
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,forge-meta,pdb}'
- '!**/._*'
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
- '!.yarn{,/**/*}'
- '!.editorconfig'
- '!.yarnrc.yml'
nodeModuleFilePatterns:
- '**/*'
- electron/**/*
- src/**/*
- receiver/**/*
- package.json
nsis:
script: |-
!include "D:\Sites\Work\RadioCast\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
!addincludedir "D:\Sites\Work\RadioCast\node_modules\app-builder-lib\templates\nsis\include"
!macro _isUpdated _a _b _t _f
${StdUtils.TestParameter} $R9 "updated"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isUpdated `"" isUpdated ""`
!macro _isForceRun _a _b _t _f
${StdUtils.TestParameter} $R9 "force-run"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForceRun `"" isForceRun ""`
!macro _isKeepShortcuts _a _b _t _f
${StdUtils.TestParameter} $R9 "keep-shortcuts"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isKeepShortcuts `"" isKeepShortcuts ""`
!macro _isNoDesktopShortcut _a _b _t _f
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isNoDesktopShortcut `"" isNoDesktopShortcut ""`
!macro _isDeleteAppData _a _b _t _f
${StdUtils.TestParameter} $R9 "delete-app-data"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isDeleteAppData `"" isDeleteAppData ""`
!macro _isForAllUsers _a _b _t _f
${StdUtils.TestParameter} $R9 "allusers"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForAllUsers `"" isForAllUsers ""`
!macro _isForCurrentUser _a _b _t _f
${StdUtils.TestParameter} $R9 "currentuser"
StrCmp "$R9" "true" `${_t}` `${_f}`
!macroend
!define isForCurrentUser `"" isForCurrentUser ""`
!macro addLangs
!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "German"
!insertmacro MUI_LANGUAGE "French"
!insertmacro MUI_LANGUAGE "SpanishInternational"
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "TradChinese"
!insertmacro MUI_LANGUAGE "Japanese"
!insertmacro MUI_LANGUAGE "Korean"
!insertmacro MUI_LANGUAGE "Italian"
!insertmacro MUI_LANGUAGE "Dutch"
!insertmacro MUI_LANGUAGE "Danish"
!insertmacro MUI_LANGUAGE "Swedish"
!insertmacro MUI_LANGUAGE "Norwegian"
!insertmacro MUI_LANGUAGE "Finnish"
!insertmacro MUI_LANGUAGE "Russian"
!insertmacro MUI_LANGUAGE "Portuguese"
!insertmacro MUI_LANGUAGE "PortugueseBR"
!insertmacro MUI_LANGUAGE "Polish"
!insertmacro MUI_LANGUAGE "Ukrainian"
!insertmacro MUI_LANGUAGE "Czech"
!insertmacro MUI_LANGUAGE "Slovak"
!insertmacro MUI_LANGUAGE "Hungarian"
!insertmacro MUI_LANGUAGE "Arabic"
!insertmacro MUI_LANGUAGE "Turkish"
!insertmacro MUI_LANGUAGE "Thai"
!insertmacro MUI_LANGUAGE "Vietnamese"
!macroend
!include "C:\Users\Gregor\AppData\Local\Temp\t-6x2nSt\0-messages.nsh"
!addplugindir /x86-unicode "C:\Users\Gregor\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1\plugins\x86-unicode"
Var newStartMenuLink
Var oldStartMenuLink
Var newDesktopLink
Var oldDesktopLink
Var oldShortcutName
Var oldMenuDirectory
!include "common.nsh"
!include "MUI2.nsh"
!include "multiUser.nsh"
!include "allowOnlyOneInstallerInstance.nsh"
!ifdef INSTALL_MODE_PER_ALL_USERS
!ifdef BUILD_UNINSTALLER
RequestExecutionLevel user
!else
RequestExecutionLevel admin
!endif
!else
RequestExecutionLevel user
!endif
!ifdef BUILD_UNINSTALLER
SilentInstall silent
!else
Var appExe
Var launchLink
!endif
!ifdef ONE_CLICK
!include "oneClick.nsh"
!else
!include "assistedInstaller.nsh"
!endif
!insertmacro addLangs
!ifmacrodef customHeader
!insertmacro customHeader
!endif
Function .onInit
Call setInstallSectionSpaceRequired
SetOutPath $INSTDIR
${LogSet} on
!ifmacrodef preInit
!insertmacro preInit
!endif
!ifdef DISPLAY_LANG_SELECTOR
!insertmacro MUI_LANGDLL_DISPLAY
!endif
!ifdef BUILD_UNINSTALLER
WriteUninstaller "${UNINSTALLER_OUT_FILE}"
!insertmacro quitSuccess
!else
!insertmacro check64BitAndSetRegView
!ifdef ONE_CLICK
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
!else
${IfNot} ${UAC_IsInnerInstance}
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
${EndIf}
!endif
!insertmacro initMultiUser
!ifmacrodef customInit
!insertmacro customInit
!endif
!ifmacrodef addLicenseFiles
InitPluginsDir
!insertmacro addLicenseFiles
!endif
!endif
FunctionEnd
!ifndef BUILD_UNINSTALLER
!include "installUtil.nsh"
!endif
Section "install" INSTALL_SECTION_ID
!ifndef BUILD_UNINSTALLER
# If we're running a silent upgrade of a per-machine installation, elevate so extracting the new app will succeed.
# For a non-silent install, the elevation will be triggered when the install mode is selected in the UI,
# but that won't be executed when silent.
!ifndef INSTALL_MODE_PER_ALL_USERS
!ifndef ONE_CLICK
${if} $hasPerMachineInstallation == "1" # set in onInit by initMultiUser
${andIf} ${Silent}
${ifNot} ${UAC_IsAdmin}
ShowWindow $HWNDPARENT ${SW_HIDE}
!insertmacro UAC_RunElevated
${Switch} $0
${Case} 0
${Break}
${Case} 1223 ;user aborted
${Break}
${Default}
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
${Break}
${EndSwitch}
Quit
${else}
!insertmacro setInstallModePerAllUsers
${endIf}
${endIf}
!endif
!endif
!include "installSection.nsh"
!endif
SectionEnd
Function setInstallSectionSpaceRequired
!insertmacro setSpaceRequired ${INSTALL_SECTION_ID}
FunctionEnd
!ifdef BUILD_UNINSTALLER
!include "uninstaller.nsh"
!endif

View File

@@ -0,0 +1,25 @@
directories:
output: dist
buildResources: build
appId: si.klevze.radioPlayer
productName: RadioPlayer
files:
- filter:
- electron/**/*
- src/**/*
- receiver/**/*
- package.json
win:
target:
- nsis
signAndEditExecutable: false
icon: src-tauri/icons/icon.ico
mac:
target:
- dmg
icon: src-tauri/icons/icon.icns
linux:
target:
- AppImage
icon: src-tauri/icons
electronVersion: 30.5.1

View File

@@ -0,0 +1,21 @@
Copyright (c) Electron contributors
Copyright (c) 2013-2020 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More