Compare commits

...

9 Commits

Author SHA1 Message Date
f87f7fc8aa merge 2025-12-31 10:37:37 +01:00
e3fb14e9aa updated readme 2025-12-31 10:37:37 +01:00
747d5296fa small fixes 2025-12-31 10:37:37 +01:00
dba3f21114 visually updated 2025-12-31 10:37:35 +01:00
676a715187 beautify 2025-12-31 10:37:34 +01:00
71fab9def0 fixed cast 2025-12-31 10:37:33 +01:00
34c4521629 visual fix 2025-12-31 10:37:33 +01:00
a2ba71a2a6 Initial commit 2025-12-31 10:37:33 +01:00
e57d9a0229 Initial commit
# Conflicts:
#	README.md
2025-12-31 10:37:32 +01:00
108 changed files with 9990 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

143
README.md
View File

@@ -1,2 +1,145 @@
# RadioPlayer # RadioPlayer
A lightweight, cross-platform radio player built with Tauri and Vanilla JavaScript. Features local playback and Google Cast integration.
## Prerequisites
Before you begin, ensure you have the following installed on your machine:
1. **Node.js**: [Download Node.js](https://nodejs.org/) (LTS version recommended).
2. **Rust**: Install via [rustup.rs](https://rustup.rs/).
3. **Visual Studio C++ Build Tools** (Windows only): Required for compiling Rust. Ensure "Desktop development with C++" is selected during installation.
## Installation
1. **Clone the repository**:
```bash
git clone <repository-url>
cd RadioPlayer
```
2. **Install dependencies**:
```bash
npm install
```
3. **Verify Rust environment**:
It's good practice to ensure your Rust environment is ready.
```bash
cd src-tauri
cargo check
cd ..
```
## Development
To start the application in development mode (with hot-reloading for frontend changes):
```bash
npm run tauri dev
```
This command will:
1. Compile the Rust backend.
2. Launch the application window.
3. Watch for changes in `src/` and `src-tauri/`.
## Building for Production
To create an optimized, standalone executable for your operating system:
1. **Run the build command**:
```bash
npm run tauri build
```
2. **Locate the artifacts**:
After the build completes, the installers and executables will be found in:
- **Windows**: `src-tauri/target/release/bundle/msi/` or `nsis/`
- **macOS**: `src-tauri/target/release/bundle/dmg/` or `macos/`
- **Linux**: `src-tauri/target/release/bundle/deb/` or `appimage/`
## Project Structure
* **`src/`**: Frontend source code (Vanilla HTML/CSS/JS).
* `index.html`: The main entry point of the app.
* `main.js`: Core logic, handles UI events and communication with the Tauri backend.
* `styles.css`: Application styling.
* `stations.json`: Configuration file for available radio streams.
* **`src-tauri/`**: Rust backend code.
* `src/main.rs`: The entry point for the Rust process. Handles Google Cast discovery and playback logic.
* `tauri.conf.json`: Configuration for the Tauri app (window size, permissions, package info).
## Customization
### Adding Radio Stations
To add new stations, edit the `src/stations.json` file. Add a new object to the array with a `name` and stream `url`:
```json
[
{
"name": "My New Station",
"url": "https://stream-url.com/stream"
}
]
```
### Adjusting Window Size
To change the default window size, edit `src-tauri/tauri.conf.json`:
```json
"windows": [
{
"width": 360, // Change width
"height": 720, // Change height
"resizable": false
}
]
```
## Troubleshooting
* **Command `tauri` not found**: Ensure you are running commands via `npm run tauri ...` or global install `@tauri-apps/cli`.
* **WebView2 Error (Windows)**: If the app doesn't start on Windows, ensure the [Microsoft Edge WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) is installed.
* **Build Failures**: Try running `cargo update` inside the `src-tauri` folder to update Rust dependencies.
## License
[Add License Information Here]
## Release v0.1
Initial public preview (v0.1) — a minimal, working RadioPlayer experience:
- Custom CAF Receiver UI (HTML/CSS/JS) in `receiver/` with branded artwork and playback status.
- Plays LIVE stream: `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`).
- Desktop sidecar (`sidecar/index.js`) launches the Default Media Receiver and sends LOAD commands; launch flow now retries if the device reports `NOT_ALLOWED` by stopping existing sessions first.
Included receiver files:
- `receiver/index.html`
- `receiver/receiver.js` (CAF Receiver initialization + LOAD interceptor for LIVE metadata)
- `receiver/styles.css`
- `receiver/assets/logo.svg`
Quick testing notes
- The receiver must be served over HTTPS for Cast devices to load it. For quick local testing you can use `mkcert` + a static HTTPS server:
```bash
# create local certs
mkcert -install
mkcert localhost
# serve the receiver folder over HTTPS
npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem
```
- Use the Default Media Receiver App ID while developing, or register a Custom Receiver App in the Cast Developer Console and point its URL to your hosted `index.html` for production.
Sidecar / troubleshoot
- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will now attempt to stop any existing sessions on the device and retry the launch (best-effort). Check sidecar logs for `Launch NOT_ALLOWED` and subsequent retry attempts.
- Note: the sidecar uses `castv2-client` (not the official Google sender SDK). Group/stereo behavior may vary across device types — for full sender capabilities consider adding an official sender implementation.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

@@ -0,0 +1,2 @@
#Wed Dec 31 09:53:51 CET 2025
gradle.version=8.9

View File

BIN
app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

256
layout2_plan.md Normal file
View File

@@ -0,0 +1,256 @@
# Beautify “Connect to Device” Popup (Tauri + HTML)
## Goal
Redesign the **Connect to Device** popup/modal to match the apps **glassmorphism + neon purple** style:
* Frosted glass modal
* Blurred/dimmed overlay background
* Clean device list with hover + selected states
* Premium rounded corners and soft glow
* Works smoothly inside **Tauri WebView** (desktop)
---
## Requirements
### Functional
* Modal opens/closes via existing app logic (do not change behavior).
* Device list supports:
* Hover highlight
* Single selected device (one at a time)
* Click selects device (and triggers existing handler)
* “Cancel” button closes modal (existing handler).
* Include “This Computer (Local Playback)” option at top.
### Visual
* Overlay blur + dark tint behind modal.
* Modal background: semi-transparent glass with blur.
* No harsh borders; use subtle border + soft shadow.
* Selected device: gradient accent + glow.
* Smooth animation on open (scale + fade).
* Desktop-friendly sizing (320420px wide).
### Tauri Safety / Performance
* Keep CSS lightweight.
* Avoid heavy SVG masks or expensive filters beyond `backdrop-filter: blur(...)`.
* Ensure modal area is `-webkit-app-region: no-drag` if you use draggable window headers.
---
## Target Structure
### HTML (keep semantic + simple)
Use or adapt this structure in the renderer HTML:
```html
<div id="deviceOverlay" class="overlay hidden" aria-hidden="true">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="deviceTitle">
<h2 id="deviceTitle">Connect to Device</h2>
<ul id="deviceList" class="device-list">
<!-- Render device items here -->
<!-- Example item:
<li class="device local selected" data-device="local">
<div class="device-main">This Computer</div>
<div class="device-sub">Local Playback</div>
</li>
-->
</ul>
<button id="deviceCancelBtn" class="btn cancel" type="button">Cancel</button>
</div>
</div>
```
Notes:
* Use `hidden` class to toggle visibility.
* Each `<li>` must have `data-device="<name-or-id>"` for click handling.
* Selected item uses `.selected`.
---
## CSS (Glassmorphism Modal)
Add/merge this CSS into your stylesheet:
```css
/* Overlay */
.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;
}
.hidden { display: none !important; }
/* 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: 0;
margin: 0 0 18px;
max-height: 360px;
overflow: 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;
}
.device:hover {
background: rgba(255,255,255,0.10);
transform: translateY(-1px);
}
.device .device-main {
font-size: 15px;
}
.device .device-sub {
margin-top: 3px;
font-size: 12px;
opacity: 0.7;
}
/* 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-sub {
opacity: 0.85;
}
/* Optional: disabled group devices */
.device.disabled {
opacity: 0.45;
pointer-events: none;
}
/* 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;
}
.btn.cancel:hover {
transform: scale(1.02);
}
```
---
## JavaScript Behavior (Minimal)
### Selection logic
* When a device row is clicked:
1. Remove `.selected` from all device `<li>`
2. Add `.selected` to clicked `<li>`
3. Call existing “connect/cast to device” handler with `data-device`
Pseudo-code (adapt to existing app code):
```js
deviceList.addEventListener("click", (e) => {
const item = e.target.closest(".device");
if (!item) return;
deviceList.querySelectorAll(".device.selected")
.forEach(el => el.classList.remove("selected"));
item.classList.add("selected");
const deviceName = item.dataset.device;
// call existing connect logic:
// connectToDevice(deviceName);
});
```
### Close modal
* Cancel button closes modal (existing function):
```js
deviceCancelBtn.addEventListener("click", closeDeviceModal);
```
### Optional: close on overlay click
* Only if app UX allows:
```js
deviceOverlay.addEventListener("click", (e) => {
if (e.target === deviceOverlay) closeDeviceModal();
});
```
---
## Acceptance Criteria
* Popup visually matches glassmorphism theme.
* Overlay dims and blurs background.
* Device list has hover + selected glow.
* Exactly one device can be selected.
* Cancel closes modal.
* Runs smoothly in Tauri WebView without lag.
---
## Notes / Future Enhancements (Optional)
* Mark “Speaker Groups” as `.disabled` if casting library cant support them.
* Add small icons per device type (local vs cast).
* Remember last selected device and preselect it on open.

212
layout_plan.md Normal file
View File

@@ -0,0 +1,212 @@
# Radio1 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.
The app is built with:
* **Tauri**
* **HTML / CSS / Vanilla JS**
* Desktop-first (Windows)
The UI must feel:
* Premium
* Lightweight
* Native
* Smooth
* Purple / neon themed
---
## Visual Reference
Style inspiration:
* Frosted glass cards
* Soft purple / blue gradient background
* Rounded corners everywhere
* Subtle glow instead of hard borders
* Floating controls
Target aesthetic keywords:
> glassmorphism, neon glow, frosted glass, modern music player, soft gradients
---
## Layout Structure
Single centered player card:
```
┌──────────────────────────────┐
│ Radio1 Player │
│ ● Playing / Ready │
│ │
│ [ Station Artwork / Logo ] │
│ │
│ Radio 1 MB │
│ Live Stream │
│ │
│ ────────●──────── │
│ │
│ ⏮ ▶ / ⏸ ⏭ │
│ │
│ 🔊 ─────●──── 50% │
└──────────────────────────────┘
```
---
## Required UI Elements
### Header
* Title: `Radio1 Player`
* Status indicator:
* `● Ready`
* `● Playing`
* `● Casting to <Device>`
### Artwork Area
* Square (1:1)
* Rounded corners (2024px)
* Gradient or station logo
* Soft glow around container
### Metadata
* Station name (Radio 1 MB)
* Subtitle (Live Stream / Casting Mode)
### Playback Controls
* Previous (optional, visual only)
* Play / Pause (primary accent button)
* Next (optional, visual only)
### Volume Control
* Speaker icon
* Horizontal slider
* Percentage text
* * / buttons optional
---
## Color Palette
```css
--background-gradient: linear-gradient(135deg, #7b7fd8, #b57cf2);
--glass-bg: rgba(255, 255, 255, 0.15);
--glass-border: rgba(255, 255, 255, 0.25);
--accent: #c77dff;
--accent-glow: rgba(199, 125, 255, 0.7);
--text-main: #ffffff;
--text-muted: rgba(255, 255, 255, 0.65);
--success: #7dffb3;
--danger: #ff5f5f;
```
---
## Styling Rules
* Use **glassmorphism**
* `backdrop-filter: blur(20px)`
* Semi-transparent backgrounds
* No hard borders
* Use glow (`box-shadow`) instead of outlines
* Large border radius (2028px)
* Smooth hover animations
* No external UI libraries
---
## CSS Requirements
* Must work inside **Tauri WebView**
* Avoid heavy filters or SVG masks
* Use `accent-color` for sliders
* Buttons should animate on hover (`scale`, `glow`)
---
## Tauri-Specific Enhancements
### Frameless Window
```ts
appWindow.setDecorations(false);
```
### Draggable Header
```css
header {
-webkit-app-region: drag;
}
button, input {
-webkit-app-region: no-drag;
}
```
---
## States to Support
### Playback States
* Ready
* Playing locally
* Casting to Google Speaker
* Stopped
### UI Feedback
* Change accent glow when playing
* Dim Play button when active
* Update status text dynamically
---
## Accessibility & UX
* Buttons ≥ 44px
* Clear contrast for text
* Keyboard accessible controls
* Volume slider supports mouse wheel
---
## Non-Goals
* No playlists
* No track seeking (live stream)
* No mobile layout
* No heavy animations
---
## Success Criteria
* UI visually matches glassmorphism inspiration
* Smooth performance in Tauri
* Looks premium and modern
* Clean HTML & CSS
* Easy to extend later
---
## Instructions for Copilot Agent
* Generate **HTML + CSS only**
* Keep JavaScript minimal
* Prioritize visual fidelity
* Avoid overengineering
* Comment code where helpful

240
package-lock.json generated Normal file
View File

@@ -0,0 +1,240 @@
{
"name": "radio-tauri",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "radio-tauri",
"version": "0.1.0",
"devDependencies": {
"@tauri-apps/cli": "^2",
"rcedit": "^1.1.2"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz",
"integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.9.6",
"@tauri-apps/cli-darwin-x64": "2.9.6",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6",
"@tauri-apps/cli-linux-arm64-gnu": "2.9.6",
"@tauri-apps/cli-linux-arm64-musl": "2.9.6",
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-gnu": "2.9.6",
"@tauri-apps/cli-linux-x64-musl": "2.9.6",
"@tauri-apps/cli-win32-arm64-msvc": "2.9.6",
"@tauri-apps/cli-win32-ia32-msvc": "2.9.6",
"@tauri-apps/cli-win32-x64-msvc": "2.9.6"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz",
"integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz",
"integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz",
"integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz",
"integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz",
"integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz",
"integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz",
"integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz",
"integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz",
"integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz",
"integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz",
"integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/rcedit": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.1.2.tgz",
"integrity": "sha512-z2ypB4gbINhI6wVe0JJMmdpmOpmNc4g90sE6/6JSuch5kYnjfz9CxvVPqqhShgR6GIkmtW3W2UlfiXhWljA0Fw==",
"dev": true,
"license": "MIT"
}
}
}

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "radio-tauri",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tauri dev",
"build": "node tools/copy-binaries.js && tauri build && node tools/post-build-rcedit.js",
"tauri": "node tools/copy-binaries.js && tauri"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"rcedit": "^1.1.2"
}
}

15
receiver/assets/logo.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#7b7fd8"/>
<stop offset="1" stop-color="#b57cf2"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" rx="24" fill="url(#g)" />
<g fill="white" transform="translate(32,32)">
<circle cx="48" cy="48" r="28" fill="rgba(255,255,255,0.15)" />
<path d="M24 48c6-10 16-16 24-16v8c-6 0-14 4-18 12s-2 12 0 12 6-2 10-6c4-4 10-6 14-6v8c-6 0-14 4-18 12s-2 12 0 12" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" />
<text x="96" y="98" font-family="sans-serif" font-size="18" fill="white" opacity="0.95">Radio</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 815 B

27
receiver/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Radio Player</title>
<!-- Google Cast Receiver SDK -->
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="app">
<h1>Radio Player</h1>
<p id="status">Ready</p>
<div id="artwork">
<img src="assets/logo.svg" alt="Radio Player" />
</div>
<p id="station">Radio 1 Live Stream</p>
</div>
<script src="receiver.js"></script>
</body>
</html>

73
receiver/receiver.js Normal file
View File

@@ -0,0 +1,73 @@
/* Receiver for "Radio Player" using CAF Receiver SDK */
(function () {
const STREAM_URL = 'https://live.radio1.si/Radio1MB';
function $(id) { return document.getElementById(id); }
document.addEventListener('DOMContentLoaded', () => {
const context = cast.framework.CastReceiverContext.getInstance();
const playerManager = context.getPlayerManager();
const statusEl = $('status');
const stationEl = $('station');
// Intercept LOAD to enforce correct metadata for LIVE audio
playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD,
(request) => {
if (!request || !request.media) return request;
request.media.contentId = request.media.contentId || STREAM_URL;
request.media.contentType = 'audio/mpeg';
request.media.streamType = cast.framework.messages.StreamType.LIVE;
request.media.metadata = request.media.metadata || {};
request.media.metadata.title = request.media.metadata.title || 'Radio 1';
request.media.metadata.images = request.media.metadata.images || [{ url: 'assets/logo.svg' }];
return request;
}
);
// Update UI on player state changes
playerManager.addEventListener(
cast.framework.events.EventType.PLAYER_STATE_CHANGED,
() => {
const state = playerManager.getPlayerState();
switch (state) {
case cast.framework.messages.PlayerState.PLAYING:
statusEl.textContent = 'Playing';
break;
case cast.framework.messages.PlayerState.PAUSED:
statusEl.textContent = 'Paused';
break;
case cast.framework.messages.PlayerState.IDLE:
statusEl.textContent = 'Stopped';
break;
default:
statusEl.textContent = state;
}
}
);
// When a new media is loaded, reflect metadata (station name, artwork)
playerManager.addEventListener(cast.framework.events.EventType.LOAD, (event) => {
const media = event && event.data && event.data.media;
if (media && media.metadata) {
if (media.metadata.title) stationEl.textContent = media.metadata.title;
if (media.metadata.images && media.metadata.images[0] && media.metadata.images[0].url) {
const img = document.querySelector('#artwork img');
img.src = media.metadata.images[0].url;
}
}
});
// Optional: reflect volume in title attribute
playerManager.addEventListener(cast.framework.events.EventType.VOLUME_CHANGED, (evt) => {
const level = evt && evt.data && typeof evt.data.level === 'number' ? evt.data.level : null;
if (level !== null) statusEl.title = `Volume: ${Math.round(level * 100)}%`;
});
// Start the cast receiver context
context.start({ statusText: 'Radio Player Ready' });
});
})();

58
receiver/styles.css Normal file
View File

@@ -0,0 +1,58 @@
html, body {
margin: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #7b7fd8, #b57cf2);
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: white;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
box-sizing: border-box;
}
#artwork {
width: 240px;
height: 240px;
margin: 20px 0;
border-radius: 24px;
overflow: hidden;
background: rgba(0,0,0,0.1);
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
#artwork img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
#status {
font-size: 18px;
opacity: 0.95;
margin: 6px 0 0 0;
}
#station {
font-size: 16px;
opacity: 0.85;
margin: 6px 0 0 0;
}
h1 {
font-size: 20px;
margin: 0 0 6px 0;
}
@media (max-width: 480px) {
#artwork { width: 160px; height: 160px; }
h1 { font-size: 18px; }
}

215
sidecar/index.js Normal file
View File

@@ -0,0 +1,215 @@
const { Client, DefaultMediaReceiver } = require('castv2-client');
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
let activeClient = null;
let activePlayer = null;
function isNotAllowedError(err) {
if (!err) return false;
const msg = (err.message || String(err)).toUpperCase();
return msg.includes('NOT_ALLOWED') || msg.includes('NOT ALLOWED');
}
function stopSessions(client, sessions, cb) {
if (!client || !sessions || sessions.length === 0) return cb();
const remaining = sessions.slice();
const stopNext = () => {
const session = remaining.shift();
if (!session) return cb();
client.stop(session, (err) => {
if (err) {
log(`Stop session failed (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
} else {
log(`Stopped session (${session.appId || 'unknown app'})`);
}
// Continue regardless; best-effort.
stopNext();
});
};
stopNext();
}
function log(msg) {
console.log(JSON.stringify({ type: 'log', message: msg }));
}
function error(msg) {
console.error(JSON.stringify({ type: 'error', message: msg }));
}
rl.on('line', (line) => {
try {
const data = JSON.parse(line);
const { command, args } = data;
switch (command) {
case 'play':
play(args.ip, args.url);
break;
case 'stop':
stop();
break;
case 'volume':
setVolume(args.level);
break;
default:
error(`Unknown command: ${command}`);
}
} catch (e) {
error(`Failed to parse line: ${e.message}`);
}
});
function play(ip, url) {
if (activeClient) {
try { activeClient.close(); } catch (e) { }
}
activeClient = new Client();
activeClient.connect(ip, () => {
log(`Connected to ${ip}`);
// First, check if DefaultMediaReceiver is already running
activeClient.getSessions((err, sessions) => {
if (err) return error(`GetSessions error: ${err.message}`);
// Log sessions for debugging (appId/sessionId if available)
try {
const sessInfo = sessions.map(s => ({ appId: s.appId, sessionId: s.sessionId, displayName: s.displayName }));
log(`Sessions: ${JSON.stringify(sessInfo)}`);
} catch (e) {
log('Sessions: (unable to stringify)');
}
// DefaultMediaReceiver App ID is CC1AD845
const session = sessions.find(s => s.appId === 'CC1AD845');
if (session) {
log('Session already running, joining...');
activeClient.join(session, DefaultMediaReceiver, (err, player) => {
if (err) {
log('Join failed, attempting launch...');
log(`Join error: ${err && err.message ? err.message : String(err)}`);
// Join can fail if the session is stale; stop it and retry launch.
stopSessions(activeClient, [session], () => launchPlayer(url, /*didStopFirst*/ true));
} else {
activePlayer = player;
loadMedia(url);
}
});
} else {
// If another app is running, stop it first to avoid NOT_ALLOWED.
if (sessions.length > 0) {
log('Non-media session detected, stopping before launch...');
stopSessions(activeClient, sessions, () => launchPlayer(url, /*didStopFirst*/ true));
} else {
launchPlayer(url, /*didStopFirst*/ false);
}
}
});
});
activeClient.on('error', (err) => {
error(`Client error: ${err.message}`);
try { activeClient.close(); } catch (e) { }
activeClient = null;
activePlayer = null;
});
}
function launchPlayer(url, didStopFirst) {
if (!activeClient) return;
activeClient.launch(DefaultMediaReceiver, (err, player) => {
if (err) {
const details = `Launch error: ${err && err.message ? err.message : String(err)}${err && err.code ? ` (code: ${err.code})` : ''}`;
// If launch fails with NOT_ALLOWED, the device may be busy with another app/session.
// Best-effort: stop existing sessions once, then retry launch.
if (!didStopFirst && isNotAllowedError(err)) {
log('Launch NOT_ALLOWED; attempting to stop existing sessions and retry...');
activeClient.getSessions((sessErr, sessions) => {
if (sessErr) {
error(`${details} | GetSessions error: ${sessErr.message || String(sessErr)}`);
return;
}
stopSessions(activeClient, sessions, () => {
activeClient.launch(DefaultMediaReceiver, (retryErr, retryPlayer) => {
if (retryErr) {
const retryDetails = `Launch retry error: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}${retryErr && retryErr.code ? ` (code: ${retryErr.code})` : ''}`;
error(retryDetails);
try { error(`Launch retry error full: ${JSON.stringify(retryErr)}`); } catch (e) { /* ignore */ }
return;
}
activePlayer = retryPlayer;
loadMedia(url);
});
});
});
return;
}
error(details);
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
return;
}
activePlayer = player;
loadMedia(url);
});
}
function loadMedia(url) {
if (!activePlayer) return;
const media = {
contentId: url,
contentType: 'audio/mpeg',
streamType: 'LIVE',
metadata: {
metadataType: 0,
title: 'Radio 1'
}
};
activePlayer.load(media, { autoplay: true }, (err, status) => {
if (err) return error(`Load error: ${err.message}`);
log('Media loaded, playing...');
});
activePlayer.on('status', (status) => {
// Optional: track status
});
}
function stop() {
if (activePlayer) {
try { activePlayer.stop(); } catch (e) { }
log('Stopped playback');
}
if (activeClient) {
try { activeClient.close(); } catch (e) { }
activeClient = null;
activePlayer = null;
}
}
function setVolume(level) {
if (activeClient && activePlayer) {
activeClient.setVolume({ level }, (err, status) => {
if (err) return error(`Volume error: ${err.message}`);
log(`Volume set to ${level}`);
});
} else {
log('Volume command ignored: Player not initialized');
}
}
log('Sidecar initialized and waiting for commands');

190
sidecar/package-lock.json generated Normal file
View File

@@ -0,0 +1,190 @@
{
"name": "radiocast-sidecar",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "radiocast-sidecar",
"version": "1.0.0",
"dependencies": {
"castv2-client": "^1.2.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/castv2": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/castv2/-/castv2-0.1.10.tgz",
"integrity": "sha512-3QWevHrjT22KdF08Y2a217IYCDQDP7vEJaY4n0lPBeC5UBYbMFMadDfVTsaQwq7wqsEgYUHElPGm3EO1ey+TNw==",
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"protobufjs": "^6.8.8"
}
},
"node_modules/castv2-client": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/castv2-client/-/castv2-client-1.2.0.tgz",
"integrity": "sha512-2diOsC0vSSxa3QEOgoGBy9fZRHzNXatHz464Kje2OpwQ7GM5vulyrD0gLFOQ1P4rgLAFsYiSGQl4gK402nEEuA==",
"license": "MIT",
"dependencies": {
"castv2": "~0.1.4",
"debug": "^2.2.0"
}
},
"node_modules/castv2/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/castv2/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "6.11.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
}
}
}

17
sidecar/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "radiocast-sidecar",
"version": "1.0.0",
"main": "index.js",
"bin": "index.js",
"dependencies": {
"castv2-client": "^1.2.0"
},
"pkg": {
"assets": [
"node_modules/castv2/lib/*.proto"
]
},
"scripts": {
"build": "pkg . --targets node18-win-x64 --output ../src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe"
}
}

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5626
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "radio-tauri"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "radio_tauri_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rust_cast = "0.19.0"
mdns-sd = "0.17.1"
tokio = { version = "1.48.0", features = ["full"] }
tauri-plugin-shell = "2.3.3"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

15
src-tauri/build_log.txt Normal file
View File

@@ -0,0 +1,15 @@
Compiling radio-tauri v0.1.0 (D:\Sites\Work\RadioCast\src-tauri)
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
--> src\lib.rs:33:36
|
33 | let returned_child = child.clone();
| ^^^^^ method not found in `CommandChild`
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
--> src\lib.rs:58:32
|
58 | let returned_child = child.clone();
| ^^^^^ method not found in `CommandChild`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `radio-tauri` (lib) due to 2 previous errors

View File

@@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-close",
"opener:default",
"shell:default"
]
}

29
src-tauri/check_log.txt Normal file
View File

@@ -0,0 +1,29 @@
Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri)
warning: variable does not need to be mutable
--> src\lib.rs:38:9
|
38 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
| ----^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
warning: variable does not need to be mutable
--> src\lib.rs:75:9
|
75 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
| ----^^^^^^
| |
| help: remove this `mut`
warning: variable does not need to be mutable
--> src\lib.rs:99:9
|
99 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
| ----^^^^^^
| |
| help: remove this `mut`
warning: `radio-tauri` (lib) generated 3 warnings (run `cargo fix --lib -p radio-tauri` to apply 3 suggestions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.78s

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

175
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,175 @@
use std::collections::HashMap;
use std::sync::Mutex;
use std::thread;
use mdns_sd::{ServiceDaemon, ServiceEvent};
use serde_json::json;
use tauri::{AppHandle, Manager, State};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
struct SidecarState {
child: Mutex<Option<CommandChild>>,
}
struct AppState {
known_devices: Mutex<HashMap<String, String>>,
}
#[tauri::command]
async fn list_cast_devices(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let devices = state.known_devices.lock().unwrap();
let mut list: Vec<String> = devices.keys().cloned().collect();
list.sort();
Ok(list)
}
#[tauri::command]
async fn cast_play(
app: AppHandle,
state: State<'_, AppState>,
sidecar_state: State<'_, SidecarState>,
device_name: String,
url: String,
) -> Result<(), String> {
let ip = {
let devices = state.known_devices.lock().unwrap();
devices
.get(&device_name)
.cloned()
.ok_or("Device not found")?
};
let mut lock = sidecar_state.child.lock().unwrap();
// Get or spawn child
let child = if let Some(ref mut child) = *lock {
child
} else {
println!("Spawning new sidecar...");
let sidecar_command = app
.shell()
.sidecar("radiocast-sidecar")
.map_err(|e| e.to_string())?;
let (mut rx, child) = sidecar_command.spawn().map_err(|e| e.to_string())?;
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
println!("Sidecar: {}", String::from_utf8_lossy(&line))
}
CommandEvent::Stderr(line) => {
eprintln!("Sidecar Error: {}", String::from_utf8_lossy(&line))
}
_ => {}
}
}
});
*lock = Some(child);
lock.as_mut().unwrap()
};
let play_cmd = json!({
"command": "play",
"args": { "ip": ip, "url": url }
});
child
.write(format!("{}\n", play_cmd.to_string()).as_bytes())
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn cast_stop(
_app: AppHandle,
sidecar_state: State<'_, SidecarState>,
_device_name: String,
) -> Result<(), String> {
let mut lock = sidecar_state.child.lock().unwrap();
if let Some(ref mut child) = *lock {
let stop_cmd = json!({ "command": "stop", "args": {} });
child
.write(format!("{}\n", stop_cmd.to_string()).as_bytes())
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
async fn cast_set_volume(
_app: AppHandle,
sidecar_state: State<'_, SidecarState>,
_device_name: String,
volume: f32,
) -> Result<(), String> {
let mut lock = sidecar_state.child.lock().unwrap();
if let Some(ref mut child) = *lock {
let vol_cmd = json!({ "command": "volume", "args": { "level": volume } });
child
.write(format!("{}\n", vol_cmd.to_string()).as_bytes())
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.setup(|app| {
app.manage(AppState {
known_devices: Mutex::new(HashMap::new()),
});
app.manage(SidecarState {
child: Mutex::new(None),
});
let handle = app.handle().clone();
thread::spawn(move || {
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
let receiver = mdns
.browse("_googlecast._tcp.local.")
.expect("Failed to browse");
while let Ok(event) = receiver.recv() {
match event {
ServiceEvent::ServiceResolved(info) => {
let name = info
.get_property_val_str("fn")
.or_else(|| Some(info.get_fullname()))
.unwrap()
.to_string();
let addresses = info.get_addresses();
let ip = addresses
.iter()
.find(|ip| ip.is_ipv4())
.or_else(|| addresses.iter().next());
if let Some(ip) = ip {
let state = handle.state::<AppState>();
let mut devices = state.known_devices.lock().unwrap();
let ip_str = ip.to_string();
if !devices.contains_key(&name) {
println!("Discovered Cast Device: {} at {}", name, ip_str);
devices.insert(name, ip_str);
}
}
}
_ => {}
}
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
list_cast_devices,
cast_play,
cast_stop,
cast_set_volume
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
radio_tauri_lib::run()
}

39
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,39 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "RadioPlayer",
"version": "0.1.0",
"identifier": "si.klevze.radioPlayer",
"build": {
"frontendDist": "../src"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"title": "RadioPlayer",
"width": 360,
"height": 720,
"resizable": false,
"decorations": false,
"transparent": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"externalBin": [
"binaries/RadioPlayer"
],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

BIN
src/assets/appIcon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

BIN
src/assets/favicon_io.zip Normal file

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"}

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