Compare commits
3 Commits
master
...
c5dc6b9dd4
| Author | SHA1 | Date | |
|---|---|---|---|
| c5dc6b9dd4 | |||
| c09b05b7e7 | |||
| b99d9ce524 |
11
android/README.md
Normal 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.
|
||||||
BIN
android/app/src/main/assets/assets/appIcon.png
Normal file
|
After Width: | Height: | Size: 682 KiB |
BIN
android/app/src/main/assets/assets/favicon_io.zip
Normal file
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 290 KiB |
BIN
android/app/src/main/assets/assets/favicon_io/app-icon.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 49 KiB |
BIN
android/app/src/main/assets/assets/favicon_io/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
android/app/src/main/assets/assets/favicon_io/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/assets/assets/favicon_io/icon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -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"}
|
||||||
1
android/app/src/main/assets/assets/javascript.svg
Normal 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 |
4
android/app/src/main/assets/assets/tauri.svg
Normal 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 |
158
android/app/src/main/assets/index.html
Normal 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>Radio1 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">Radio 1 MB</h2>
|
||||||
|
<p id="station-subtitle">Live Stream</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>
|
||||||
355
android/app/src/main/assets/main.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
1342
android/app/src/main/assets/stations.json
Normal file
606
android/app/src/main/assets/styles.css
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
BIN
android/app/src/main/jniLibs/arm64-v8a/libradio_tauri_lib.so
Normal file
BIN
android/app/src/main/jniLibs/armeabi-v7a/libradio_tauri_lib.so
Normal file
206
scripts/build-android.ps1
Normal 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
@@ -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)."
|
||||||
12
src-tauri/gen/android/.editorconfig
Normal 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
@@ -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
@@ -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
|
||||||
64
src-tauri/gen/android/app/build.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal 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
|
||||||
37
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
225
src-tauri/gen/android/app/src/main/assets/builder-debug.yml
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||