31 Commits

Author SHA1 Message Date
83c9bcf12e cast info 2026-01-14 18:42:16 +01:00
ed2e660d34 fixed cast play 2026-01-14 17:55:18 +01:00
efdba35b77 chore: refactor async device discovery, tracing, and player Arc state 2026-01-13 17:15:59 +01:00
ab3a86041a feat: async mDNS discovery; emit device events; auto-build sidecar in npm dev 2026-01-13 16:17:53 +01:00
91e55fa37c fixed package 2026-01-13 13:32:51 +01:00
bbb767cd20 build fix 2026-01-13 13:18:46 +01:00
a69b4c0bcb Merge branch 'develop' of https://git.klevze.si/klevze/RadioPlayer into develop 2026-01-13 11:00:17 +01:00
4bd22a2009 Merge branch 'ci/add-ffmpeg-preflight' into develop 2026-01-13 10:59:46 +01:00
fd08aaffdf Merge pull request 'ci: add FFmpeg preflight workflow and helpers' (#1) from ci/add-ffmpeg-preflight into develop
Reviewed-on: #1
2026-01-13 10:59:15 +01:00
c954bf25d4 Merge branch 'feature/RewriteUIPlan' into develop 2026-01-13 08:44:51 +01:00
7b88022b66 ci: add FFmpeg preflight workflow and helpers 2026-01-13 08:44:17 +01:00
98a6ba88fc chore: remove tracked large binaries and generated assets (keep local copies) 2026-01-13 07:31:40 +01:00
916cc7764a chore: add .gitignore to ignore build artifacts and binaries 2026-01-13 07:31:18 +01:00
694f335408 tools: add sync-version.js to sync package.json -> Tauri files
- Add tools/sync-version.js script to read root package.json version
  and update src-tauri/tauri.conf.json and src-tauri/Cargo.toml.
- Update only the [package] version line in Cargo.toml to preserve formatting.
- Include JSON read/write helpers and basic error handling/reporting.
2026-01-13 07:21:51 +01:00
abb7cafaed fix 2026-01-11 19:25:02 +01:00
d45fe0fbde removing html5 audio 2026-01-11 13:42:34 +01:00
c4020615d2 ffmpeg implemented 2026-01-11 13:40:01 +01:00
34c3f0dc89 first step 2026-01-11 10:30:54 +01:00
f9b9ce0994 fix 2026-01-11 09:53:28 +01:00
9c7f04d197 Merge branch 'feature/webApp' into develop 2026-01-11 09:02:37 +01:00
ab95d124bc fix 2026-01-11 09:02:21 +01:00
bdd3e30f14 webapp 2026-01-11 08:19:27 +01:00
f2732b36f2 update 2026-01-02 20:31:15 +01:00
7c0a202f16 fix 2026-01-02 19:38:16 +01:00
cb01a59051 Display current song 2026-01-02 19:37:08 +01:00
c3f594d102 fixed visually 2026-01-02 17:16:53 +01:00
a2753bcf66 add current song 2026-01-02 16:58:58 +01:00
e36bb1ab55 Visually fix 2026-01-02 13:25:10 +01:00
c5dc6b9dd4 Added Radio City 2026-01-02 11:38:54 +01:00
c09b05b7e7 fix 2026-01-01 20:57:03 +01:00
b99d9ce524 andorid app 2026-01-01 10:47:57 +01:00
159 changed files with 193397 additions and 458 deletions

170
.ai/tauris-agent.md Normal file
View File

@@ -0,0 +1,170 @@
# ROLE: Senior Desktop Audio Engineer & Tauri Architect
You are an expert in:
- Tauri (Rust backend + system WebView frontend)
- Native audio streaming (FFmpeg, GStreamer, CPAL, Rodio)
- Desktop media players
- Chromecast / casting architectures
- Incremental refactors of production apps
You are working on an existing project named **Taurus RadioPlayer**.
---
## PROJECT CONTEXT (IMPORTANT)
This is a **Tauri desktop application**, NOT Electron.
### Current architecture
- Frontend: Vanilla HTML / CSS / JS served in WebView
- Backend: Rust (Tauri commands)
- Audio: **Native player (FFmpeg decode + CPAL output)** via Tauri commands (`player_play/stop/set_volume/get_state`)
- Casting: Google Cast via Node.js sidecar (`castv2-client`)
- Stations: JSON file + user-defined stations in `localStorage`
- Platforms: Windows, Linux, macOS
### Critical limitation
Browser/HTML5 audio is insufficient for:
- stable radio streaming
- buffering control
- reconnection
- unified local + cast playback
---
## PRIMARY GOAL
Upgrade the application by:
1. **Removing HTML5 Audio completely**
2. **Implementing a native audio streaming engine**
3. **Keeping the existing HTML/CSS UI unchanged**
4. **Preserving the current station model and UX**
5. **Maintaining cross-platform compatibility**
6. **Avoiding unnecessary rewrites**
This is an **incremental upgrade**, not a rewrite.
---
## TARGET ARCHITECTURE
- UI remains WebView-based (HTML/CSS/JS)
- JS communicates only via Tauri `invoke()`
- Audio decoding and playback are handled natively
- Local playback: FFmpeg decodes to PCM and CPAL outputs to speakers
- Casting (preferred): backend starts a **cast tap** that reuses the already-decoded PCM stream and re-encodes it to an MP3 HTTP stream (`-listen 1`) on the LAN; the sidecar casts that local URL
- Casting (fallback): backend can still run a standalone URL→MP3 proxy when the tap cannot be started
- Casting logic may remain temporarily in the sidecar
Note: “Reuse decoded audio” here means: one FFmpeg decode → PCM → fan-out to CPAL (local) and FFmpeg encode/listen (cast).
---
## TECHNICAL DIRECTIVES (MANDATORY)
### 1. Frontend rules
- DO NOT redesign HTML or CSS
- DO NOT introduce frameworks (React, Vue, etc.)
- Keep playback controlled via backend commands (no `new Audio()` usage)
- All playback must go through backend commands
### 2. Backend rules
- Prefer **Rust-native solutions**
- Acceptable audio stacks:
- FFmpeg + CPAL / Rodio
- GStreamer (if justified)
- Implement commands such as:
- `player_play(url)`
- `player_stop()`
- `player_set_volume(volume)`
- `player_get_state()`
- Handle:
- buffering
- reconnect on stream drop
- clean shutdown
- thread safety
### 3. Casting rules
- Do not break existing Chromecast support
- Prefer reusing backend-controlled audio where possible (e.g., Cast via local proxy instead of sending station URL directly)
- Do not introduce browser-based casting
- Sidecar removal is OPTIONAL, not required now
---
## MIGRATION STRATEGY (VERY IMPORTANT)
You must:
- Work in **small, safe steps**
- Clearly explain what files change and why
- Never delete working functionality without replacement
- Prefer additive refactors over destructive ones
Each response should:
1. Explain intent
2. Show concrete code
3. State which file is modified
4. Preserve compatibility
---
## WHAT YOU SHOULD PRODUCE
You may generate:
- Rust code (Tauri commands, audio engine)
- JS changes (invoke-based playback)
- Architecture explanations
- Migration steps
- TODO lists
- Warnings about pitfalls
You MUST NOT:
- Suggest Electron or Flutter
- Suggest full rewrites
- Ignore existing sidecar or station model
- Break the current UX
---
## ENGINEERING PHILOSOPHY
This app should evolve into:
> “A native audio engine with a web UI shell”
The WebView is a **control surface**, not a media engine.
---
## COMMUNICATION STYLE
- Be precise
- Be pragmatic
- Be production-oriented
- Prefer correctness over novelty
- Assume this is a real app with users
---
## FIRST TASK WHEN STARTING
Begin by:
1. Identifying all HTML5 Audio usage
2. Proposing the native audio engine design
3. Defining the minimal command interface
4. Planning the replacement step-by-step
Do NOT write all code at once.

47
.github/FFMPEG_GUIDE.md vendored Normal file
View File

@@ -0,0 +1,47 @@
# FFmpeg CI Guide
This file describes how to provide a vetted FFmpeg build to the CI workflow and how the workflow expects archive layouts.
## Secrets (recommended)
- `FFMPEG_URL` — primary URL the workflow will download. Use a stable URL to a signed/hosted FFmpeg build.
- `FFMPEG_URL_LINUX` — optional override for Linux runners.
- `FFMPEG_URL_WINDOWS` — optional override for Windows runners.
- `FFMPEG_URL_MACOS` — optional override for macOS runners.
If per-OS secrets are present, they take precedence over `FFMPEG_URL`.
## Recommended FFmpeg sources
- Use official static builds from a trusted provider (example):
- Windows (ffmpeg.exe): https://www.gyan.dev/ffmpeg/builds/
- Linux (static): https://johnvansickle.com/ffmpeg/
- macOS (static): https://evermeet.cx/ffmpeg/
Prefer hosting a copy in your own artifact store (S3, GitHub Releases) so you control the binary used in CI.
## Expected archive layouts
The workflow will attempt to extract common archive formats. Recommended layouts:
- Zip containing `ffmpeg.exe` at the archive root
- Example: `ffmpeg-2025-01-01.zip` -> `ffmpeg.exe` (root)
- Tar.gz or tar.xz containing an `ffmpeg` binary at the archive root or inside a single top-level folder
- Example: `ffmpeg-2025/ffmpeg` or `ffmpeg`
- Raw binary: a direct link to the `ffmpeg` executable is also supported (the workflow will make it executable).
If your archive nests the binary deep inside several folders, consider publishing a trimmed archive that places `ffmpeg` at the root for easier CI extraction.
## Verifying locally
To test the workflow steps locally, download your chosen archive and ensure running the binary prints version information:
```bash
# on Linux/macOS
./ffmpeg -version
# on Windows (PowerShell)
.\ffmpeg.exe -version
```
## Notes for maintainers
- If you need the workflow to handle a custom archive layout, I can update the extraction step (`.github/workflows/ffmpeg-preflight.yml`) to locate the binary path inside the archive and move it to `src-tauri/resources/ffmpeg(.exe)`.
- After adding secrets, open a PR to trigger the workflow and verify the `FFmpeg preflight OK` message in the CI logs.

129
.github/workflows/ffmpeg-preflight.yml vendored Normal file
View File

@@ -0,0 +1,129 @@
name: FFmpeg Preflight and Build
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
preflight:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
# Provide a fallback URL via repository secret `FFMPEG_URL_{OS}` or `FFMPEG_URL`.
FFMPEG_URL: ${{ secrets.FFMPEG_URL }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Set up Rust
uses: dtolnay/gh-actions-rs@stable
- name: Determine OS-specific ffmpeg URL
id: ffmpeg-url
shell: bash
run: |
echo "RUNNER_OS=${RUNNER_OS}"
if [[ "${RUNNER_OS}" == "Windows" ]]; then
echo "url=${{ secrets.FFMPEG_URL_WINDOWS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
elif [[ "${RUNNER_OS}" == "macOS" ]]; then
echo "url=${{ secrets.FFMPEG_URL_MACOS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
else
echo "url=${{ secrets.FFMPEG_URL_LINUX || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
fi
- name: Create resources dir
run: mkdir -p src-tauri/resources
- name: Download and install FFmpeg into resources
if: steps.ffmpeg-url.outputs.url != ''
shell: bash
run: |
set -euo pipefail
URL="${{ steps.ffmpeg-url.outputs.url }}"
echo "Downloading ffmpeg from: $URL"
FNAME="${RUNNER_TEMP}/ffmpeg_bundle"
if [[ "${RUNNER_OS}" == "Windows" ]]; then
powershell -Command "(New-Object Net.WebClient).DownloadFile('$URL', '$FNAME.zip')"
powershell -Command "Expand-Archive -Path '$FNAME.zip' -DestinationPath '${{ github.workspace }}\\src-tauri\\resources'"
else
curl -sL "$URL" -o "$FNAME"
# Attempt to extract common archive formats
if file "$FNAME" | grep -q 'Zip archive'; then
unzip -q "$FNAME" -d src-tauri/resources
elif file "$FNAME" | grep -q 'gzip compressed data'; then
tar -xzf "$FNAME" -C src-tauri/resources
elif file "$FNAME" | grep -q 'XZ compressed'; then
tar -xJf "$FNAME" -C src-tauri/resources
else
# Assume raw binary
mv "$FNAME" src-tauri/resources/ffmpeg
chmod +x src-tauri/resources/ffmpeg
fi
fi
- name: List resources
run: ls -la src-tauri/resources || true
- name: Locate ffmpeg binary (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
# Try to find an ffmpeg executable anywhere under resources
BINPATH=$(find src-tauri/resources -type f -iname ffmpeg -print -quit || true)
if [ -z "$BINPATH" ]; then
BINPATH=$(find src-tauri/resources -type f -iname 'ffmpeg*' -print -quit || true)
fi
if [ -n "$BINPATH" ]; then
echo "Found ffmpeg at $BINPATH"
cp "$BINPATH" src-tauri/resources/ffmpeg
chmod +x src-tauri/resources/ffmpeg
else
echo "ffmpeg binary not found in resources"
ls -R src-tauri/resources || true
exit 1
fi
- name: Locate ffmpeg binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter ffmpeg.exe -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $found) {
$found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter '*ffmpeg*' -ErrorAction SilentlyContinue | Select-Object -First 1
}
if ($found) {
Write-Host "Found ffmpeg at $($found.FullName)"
Copy-Item $found.FullName -Destination 'src-tauri\resources\ffmpeg.exe' -Force
} else {
Write-Host "ffmpeg not found in src-tauri/resources"
Get-ChildItem src-tauri\resources -Recurse | Format-List
exit 1
}
- name: Install npm deps
run: npm ci
- name: Copy project FFmpeg helpers
run: node tools/copy-ffmpeg.js || true
- name: Build Rust and run ffmpeg preflight check
working-directory: src-tauri
run: |
set -e
cargo build --release
cargo run --release --bin check_ffmpeg
- name: Optional frontend build
run: npm run build --if-present || true

34
.gitignore vendored
View File

@@ -1,3 +1,37 @@
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
# Tauri / Rust
/target/
/src-tauri/binaries/
/src-tauri/target/
# Local build artifacts
/dist/
/build/
# FFmpeg / downloaded binaries
/ffmpeg/bin/
# Editor / OS files
.vscode/
.DS_Store
Thumbs.db
# Logs and temp
*.log
*.tmp
# Generated by tools
/tools/*.cache
# Misc
*.tgz
.env
# Logs
logs
*.log

View File

@@ -36,7 +36,13 @@ Before you begin, ensure you have the following installed on your machine:
To start the application in development mode (with hot-reloading for frontend changes):
```bash
npm run tauri dev
npm run dev
```
If you want FFmpeg to be bundled into `src-tauri/resources/` for local/native playback during dev, use:
```bash
npm run dev:native
```
This command will:
@@ -50,7 +56,7 @@ To create an optimized, standalone executable for your operating system:
1. **Run the build command**:
```bash
npm run tauri build
npm run build
```
2. **Locate the artifacts**:
@@ -67,7 +73,9 @@ To create an optimized, standalone executable for your operating system:
* `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.
* `src/lib.rs`: Tauri command layer (native player commands, Cast commands, utility HTTP helpers).
* `src/player.rs`: Native audio engine (FFmpeg decode → PCM ring buffer → CPAL output).
* `src/main.rs`: Rust entry point (wires the Tauri app; most command logic lives in `lib.rs`).
* `tauri.conf.json`: Configuration for the Tauri app (window size, permissions, package info).
## Customization
@@ -103,18 +111,46 @@ To change the default window size, edit `src-tauri/tauri.conf.json`:
* **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.
## FFmpeg (Optional) for Native Playback
Local/native playback uses an external **FFmpeg** binary to decode radio streams.
### How the app finds FFmpeg
At runtime it searches in this order:
1. `RADIOPLAYER_FFMPEG` environment variable (absolute or relative path)
2. Next to the application executable (Windows: `ffmpeg.exe`, macOS/Linux: `ffmpeg`)
3. Common bundle resource folders relative to the executable:
- `resources/ffmpeg(.exe)`
- `Resources/ffmpeg(.exe)`
- `../resources/ffmpeg(.exe)`
- `../Resources/ffmpeg(.exe)`
4. Your system `PATH`
### Optional: download FFmpeg automatically (Windows)
This is **opt-in** (it is not run automatically during build/run). It downloads a prebuilt FFmpeg zip and extracts `ffmpeg.exe` into `tools/ffmpeg/bin/ffmpeg.exe`.
```bash
npm run ffmpeg:download
```
Then run `npm run dev:native` (or `npm run build`) to copy FFmpeg into `src-tauri/resources/` for bundling.
## License
[Add License Information Here]
## Release v0.1
## Release v0.2
Initial public preview (v0.1) — a minimal, working RadioPlayer experience:
Public beta (v0.2) — updates since v0.1:
- 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.
- **Android build support:** Project includes Android build scripts and Gradle wrappers. See [scripts/build-android.sh](scripts/build-android.sh) and [build-android.ps1](build-android.ps1). Prebuilt native helper binaries are available in `src-tauri/binaries/` for convenience.
- **Web receiver & webapp:** The `receiver/` folder contains a Custom CAF Receiver UI (HTML/CSS/JS) and the `webapp/` folder provides a standalone web distribution for hosting the app in browsers or PWAs.
- **Sidecar improvements:** `sidecar/index.js` now retries launches when devices return `NOT_ALLOWED` by attempting to stop existing sessions before retrying. Check sidecar logs for `Launch NOT_ALLOWED` messages and retry attempts.
- **LIVE stream:** The app continues to support the LIVE stream `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`).
Included receiver files:
@@ -140,6 +176,6 @@ npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem
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.
- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will 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.

343
TECHNICAL_DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,343 @@
# RadioPlayer — Technical Documentation (Tauri + Desktop)
This document describes the desktop (Tauri) application architecture, build pipeline, backend commands, and how the UI maps to that backend.
## High-level architecture
- **Frontend (WebView)**: Vanilla HTML/CSS/JS in [src/index.html](src/index.html), [src/main.js](src/main.js), [src/styles.css](src/styles.css)
- **Tauri host (Rust)**: Command layer + device discovery in [src-tauri/src/lib.rs](src-tauri/src/lib.rs)
- **Native audio engine (Rust)**: FFmpeg decode + CPAL output in [src-tauri/src/player.rs](src-tauri/src/player.rs)
- **Cast sidecar (Node executable)**: Google Cast control via `castv2-client` in [sidecar/index.js](sidecar/index.js)
- **Packaging utilities**:
- Sidecar binary copy/rename step: [tools/copy-binaries.js](tools/copy-binaries.js)
- Windows EXE icon patch: [tools/post-build-rcedit.js](tools/post-build-rcedit.js)
- Optional FFmpeg bundling helper: [tools/copy-ffmpeg.js](tools/copy-ffmpeg.js) (see [tools/ffmpeg/README.md](tools/ffmpeg/README.md))
Data flow:
1. UI actions call JS functions in `main.js`.
2. JS calls Tauri commands via `window.__TAURI__.core.invoke()` (for both local playback and casting).
3. In **Local mode**, Rust spawns FFmpeg and plays decoded PCM via CPAL.
4. In **Cast mode**, the Rust backend discovers Cast devices via mDNS and stores `{ deviceName -> ip }`.
5. On `cast_play/stop/volume`, Rust spawns (or reuses) a **sidecar process**, then sends newline-delimited JSON commands to the sidecar stdin.
## Running and building
### Prerequisites
- Node.js (project uses ESM at the root; see [package.json](package.json))
- Rust toolchain (via rustup)
- Platform build tools (Windows: Visual Studio C++ Build Tools)
- Tauri prerequisites (WebView2 runtime on Windows)
### Dev
From repo root:
- `npm install`
- `npm run dev`
This runs `tauri dev` (see [package.json](package.json)).
### Production build (Windows MSI/NSIS, etc.)
From repo root:
- `npm run build`
What it does (see [package.json](package.json)):
1. `node tools/copy-binaries.js` — ensures the expected bundled binary name exists.
2. `tauri build` — builds the Rust host and generates platform bundles.
3. `node tools/post-build-rcedit.js` — patches the Windows EXE icon using the locally installed `rcedit` binary.
Artifacts typically land under:
- `src-tauri/target/release/bundle/`
### Building the sidecar
The sidecar is built separately using `pkg` (see [sidecar/package.json](sidecar/package.json)):
- `cd sidecar`
- `npm install`
- `npm run build`
This outputs:
- `src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe`
## Tauri configuration
### App config
Defined in [src-tauri/tauri.conf.json](src-tauri/tauri.conf.json):
- **build.frontendDist**: `../src`
- The desktop app serves the static files in `src/`.
- **window**:
- `width: 360`, `height: 720`, `resizable: false`
- `decorations: false`, `transparent: true` (frameless / custom UI)
- **security.csp**: `null` (CSP disabled)
- **bundle.targets**: `"all"`
- **bundle.externalBin**: includes external binaries shipped with the bundle.
### Capabilities and permissions
Defined in [src-tauri/capabilities/default.json](src-tauri/capabilities/default.json):
- `core:default`
- `core:window:allow-close` (allows JS to call window close)
- `opener:default`
- `shell:default` (required for spawning the sidecar)
## Rust backend (Tauri commands)
All commands are in [src-tauri/src/lib.rs](src-tauri/src/lib.rs) and registered via `invoke_handler`.
### Shared state
- `AppState.known_devices: HashMap<String, String>`
- maps **device name****IP string**
- `SidecarState.child: Option<CommandChild>`
- stores a single long-lived sidecar child process
### mDNS discovery
In `.setup()` the backend spawns a thread that browses:
- `_googlecast._tcp.local.`
When a device is resolved:
- Name is taken from the `fn` TXT record if present, otherwise `fullname`.
- First IPv4 address is preferred.
- New devices are inserted into `known_devices` and logged.
### Commands
### Native player commands (local playback)
Local playback is handled by the Rust engine in [src-tauri/src/player.rs](src-tauri/src/player.rs). The UI controls it using these commands:
#### `player_play(url: String) -> Result<(), String>`
- Starts native playback of the provided stream URL.
- Internally spawns FFmpeg to decode into `s16le` PCM and feeds a ring buffer consumed by a CPAL output stream.
- Reports `buffering``playing` based on buffer fill/underrun.
#### `player_stop() -> Result<(), String>`
- Stops the native pipeline and updates state.
#### `player_set_volume(volume: f32) -> Result<(), String>`
- Sets volume in range `[0, 1]`.
#### `player_get_state() -> Result<PlayerState, String>`
- Returns `{ status, url, volume, error }`.
- Used by the UI to keep status text and play/stop button in sync.
#### `list_cast_devices() -> Result<Vec<String>, String>`
- Returns the sorted list of discovered Cast device names.
- Used by the UI when opening the Cast picker overlay.
#### `cast_play(device_name: String, url: String) -> Result<(), String>`
- Resolves `device_name``ip` from `known_devices`.
- Spawns the sidecar if it doesnt exist yet:
- `app.shell().sidecar("radiocast-sidecar")`
- Sidecar stdout/stderr are forwarded to the Rust process logs.
- Writes a JSON line to the sidecar stdin:
```json
{ "command": "play", "args": { "ip": "<ip>", "url": "<streamUrl>" } }
```
#### `cast_stop(device_name: String) -> Result<(), String>`
- If the sidecar process exists, writes:
```json
{ "command": "stop", "args": {} }
```
#### `cast_set_volume(device_name: String, volume: f32) -> Result<(), String>`
- If the sidecar process exists, writes:
```json
{ "command": "volume", "args": { "level": 0.0 } }
```
Notes:
- `volume` is passed from the UI in the range `[0, 1]`.
#### `fetch_url(url: String) -> Result<String, String>`
- Performs a server-side HTTP GET using `reqwest`.
- Returns response body as text.
- Used by the UI to bypass browser CORS limitations when calling 3rd-party endpoints.
## Sidecar protocol and behavior
Implementation: [sidecar/index.js](sidecar/index.js)
### Input protocol (stdin)
The sidecar reads **newline-delimited JSON objects**:
- `{"command":"play","args":{"ip":"...","url":"..."}}`
- `{"command":"stop","args":{}}`
- `{"command":"volume","args":{"level":0.5}}`
### Output protocol (stdout/stderr)
Logs are JSON objects:
- `{"type":"log","message":"..."}` to stdout
- `{"type":"error","message":"..."}` to stderr
### Cast launch logic
- Connects to the device IP.
- Reads existing sessions via `getSessions()`.
- If Default Media Receiver (`appId === "CC1AD845"`) exists, tries to join.
- If other sessions exist, attempts to stop them to avoid `NOT_ALLOWED`.
- On `NOT_ALLOWED` launch, retries once after stopping sessions (best-effort).
## Frontend behavior
### Station data model
Stations are loaded from [src/stations.json](src/stations.json) and normalized in [src/main.js](src/main.js) into:
```js
{ id, name, url, logo, enabled, raw }
```
Normalization rules (important for `stations.json` format compatibility):
- `name`: `title || id || name || "Unknown"`
- `url`: `liveAudio || liveVideo || liveStream || url || ""`
- `logo`: `logo || poster || ""`
- Stations with `enabled === false` or without a URL are filtered out.
User-defined stations are stored in `localStorage` under `userStations` and appended after file stations.
The last selected station is stored under `localStorage.lastStationId`.
### Playback modes
State is tracked in JS:
- `currentMode`: `"local"` or `"cast"`
- `currentCastDevice`: string or `null`
- `isPlaying`: boolean
#### Local mode
- Uses backend invokes: `player_play`, `player_stop`, `player_set_volume`.
- The UI polls `player_get_state` to reflect `buffering/playing/stopped/error`.
#### Cast mode
- Uses backend invokes: `cast_play`, `cast_stop`, `cast_set_volume`.
### Current song (“Now Playing”) polling
- For the currently selected station only, the app polls a station endpoint every 10s.
- It prefers `raw.currentSong`, otherwise uses `raw.lastSongs`.
- Remote URLs are fetched via the Tauri backend `fetch_url` to bypass CORS.
- If the provider returns timing fields (`playTimeStart*`, `playTimeLength*`), the UI schedules a single refresh near song end.
### Overlays
The element [src/index.html](src/index.html) `#cast-overlay` is reused for two different overlays:
- Cast device picker (`openCastOverlay()`)
- Station grid chooser (`openStationsOverlay()`)
The content is switched by:
- Toggling the `stations-grid` class on `#device-list`
- Replacing `#device-list` contents dynamically
## UI controls (button-by-button)
All UI IDs below are in [src/index.html](src/index.html) and are wired in [src/main.js](src/main.js).
### Window / header
- `#close-btn`
- Calls `getCurrentWindow().close()` (requires `core:window:allow-close`).
- `#cast-toggle-btn`
- Opens the Cast overlay and lists discovered devices (`invoke('list_cast_devices')`).
- `#edit-stations-btn`
- Opens the Stations Editor overlay (user stations stored in `localStorage.userStations`).
Note:
- `#cast-toggle-btn` and `#edit-stations-btn` appear twice in the HTML header. Duplicate IDs are invalid HTML and only the first element returned by `getElementById()` will be wired.
### Coverflow (station carousel inside artwork)
- `#artwork-prev`
- Selects previous station via `setStationByIndex()`.
- `#artwork-next`
- Selects next station via `setStationByIndex()`.
- `#artwork-coverflow` (drag/wheel area)
- Pointer drag changes station when movement exceeds a threshold.
- Wheel scroll changes station with a short debounce.
- Coverflow card click
- Selects that station.
- Coverflow card double-click (on the selected station)
- Opens the station grid overlay.
### Transport controls
- `#play-btn`
- Toggles play/stop (`togglePlay()`):
- Local mode: `invoke('player_play')` / `invoke('player_stop')`.
- Cast mode: `invoke('cast_play')` / `invoke('cast_stop')`.
- `#prev-btn`
- Previous station (`playPrev()``setStationByIndex()`).
- `#next-btn`
- Next station (`playNext()``setStationByIndex()`).
### Volume
- `#volume-slider`
- Local: `invoke('player_set_volume')`.
- Cast: `invoke('cast_set_volume')`.
- Persists `localStorage.volume`.
- `#mute-btn`
- Present in the UI but currently not wired to a handler in `main.js`.
### Cast overlay
- `#close-overlay`
- Closes the overlay (`closeCastOverlay()`).
### Stations editor overlay
- `#editor-close-btn`
- Closes the editor overlay.
- `#add-station-form` submit
- Adds/updates a station in `localStorage.userStations`.
- Triggers a full station reload (`loadStations()`).
## Service worker / PWA pieces
- Service worker file: [src/sw.js](src/sw.js)
- Caches core app assets for offline-ish behavior.
- Web manifest: [src/manifest.json](src/manifest.json)
- Name/icons/theme for installable PWA (primarily relevant for the web build; harmless in Tauri).
## Known sharp edges / notes
- **Duplicate IDs in HTML header**: only one of the duplicates will receive JS event listeners.
- **Sidecar bundling name**: the build pipeline copies `radiocast-sidecar-...` to `RadioPlayer-...` (see [tools/copy-binaries.js](tools/copy-binaries.js)); ensure the bundled binary name matches what `shell.sidecar("radiocast-sidecar")` expects for your target.

View File

@@ -1,8 +1,8 @@
# Radio1 Player Glassmorphism UI Redesign (Tauri + HTML)
# Radio Player Glassmorphism UI Redesign (Tauri + HTML)
## Objective
Redesign the **Radio1 Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps.
Redesign the **Radio Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps.
The app is built with:
@@ -42,7 +42,7 @@ Single centered player card:
```
┌──────────────────────────────┐
│ Radio1 Player │
│ Radio Player
│ ● Playing / Ready │
│ │
│ [ Station Artwork / Logo ] │
@@ -52,7 +52,7 @@ Single centered player card:
│ │
│ ────────●──────── │
│ │
│ ⏮ ▶ / ⏸ ⏭
│ ⏮ ▶ / ⏸ ⏭ │
│ │
│ 🔊 ─────●──── 50% │
└──────────────────────────────┘
@@ -64,7 +64,7 @@ Single centered player card:
### Header
* Title: `Radio1 Player`
* Title: `RadioPlayer`
* Status indicator:
* `● Ready`

1548
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,22 @@
{
"name": "radio-tauri",
"private": true,
"version": "0.1.0",
"version": "0.1.1",
"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"
"build:sidecar": "npm --prefix sidecar install && npm --prefix sidecar run build",
"dev": "npm run build:sidecar && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri dev",
"dev:native": "node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri dev",
"ffmpeg:download": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/download-ffmpeg.ps1",
"version:sync": "node tools/sync-version.js",
"build": "node tools/sync-version.js && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && node tools/write-build-flag.js set && tauri build && node tools/post-build-rcedit.js && node tools/write-build-flag.js clear",
"build:devlike": "node tools/sync-version.js && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && node tools/write-build-flag.js set --debug && cross-env RADIO_DEBUG_DEVTOOLS=1 tauri build && node tools/post-build-rcedit.js && node tools/write-build-flag.js clear",
"tauri": "node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"cross-env": "^7.0.3",
"npx": "^3.0.0",
"rcedit": "^1.1.2"
}
}

View File

@@ -1,15 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 815 B

View File

@@ -1,27 +0,0 @@
<!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>

View File

@@ -1,73 +0,0 @@
/* 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' });
});
})();

View File

@@ -1,58 +0,0 @@
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; }
}

View File

@@ -0,0 +1,71 @@
param(
[string]$Url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
[string]$OutDir = "tools/ffmpeg/bin",
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
$isWindows = $env:OS -eq 'Windows_NT'
if (-not $isWindows) {
Write-Host "This script is intended for Windows (ffmpeg.exe)." -ForegroundColor Yellow
exit 1
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$outDirAbs = (Resolve-Path (Join-Path $repoRoot $OutDir) -ErrorAction SilentlyContinue)
if (-not $outDirAbs) {
$outDirAbs = Join-Path $repoRoot $OutDir
New-Item -ItemType Directory -Force -Path $outDirAbs | Out-Null
} else {
$outDirAbs = $outDirAbs.Path
}
$ffmpegDest = Join-Path $outDirAbs "ffmpeg.exe"
# If already present, do nothing.
if (Test-Path $ffmpegDest) {
Write-Host "FFmpeg already present: $ffmpegDest"
exit 0
}
if ($DryRun) {
Write-Host "Dry run:" -ForegroundColor Cyan
Write-Host " Would download: $Url"
Write-Host " Would install to: $ffmpegDest"
exit 0
}
Write-Host "About to download a prebuilt FFmpeg package:" -ForegroundColor Cyan
Write-Host " $Url"
Write-Host "You are responsible for reviewing the FFmpeg license/compliance for your use case." -ForegroundColor Yellow
$tempRoot = Join-Path $env:TEMP ("radioplayer-ffmpeg-" + [guid]::NewGuid().ToString("N"))
$zipPath = Join-Path $tempRoot "ffmpeg.zip"
$extractDir = Join-Path $tempRoot "extract"
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
try {
Write-Host "Downloading..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $Url -OutFile $zipPath -UseBasicParsing
Write-Host "Extracting..." -ForegroundColor Cyan
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
$candidate = Get-ChildItem -Path $extractDir -Recurse -Filter "ffmpeg.exe" | Where-Object {
$_.FullName -match "\\bin\\ffmpeg\.exe$"
} | Select-Object -First 1
if (-not $candidate) {
throw "Could not find ffmpeg.exe under extracted content. The archive layout may have changed."
}
Copy-Item -Force -Path $candidate.FullName -Destination $ffmpegDest
Write-Host "Installed FFmpeg to: $ffmpegDest" -ForegroundColor Green
Write-Host "Next: run 'node tools/copy-ffmpeg.js' (or 'npm run dev:native' / 'npm run build') to bundle it into src-tauri/resources/." -ForegroundColor Green
} finally {
try { Remove-Item -Recurse -Force -Path $tempRoot -ErrorAction SilentlyContinue } catch {}
}

View File

@@ -23,15 +23,21 @@ function stopSessions(client, sessions, cb) {
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.
try {
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();
});
} catch (err) {
// Some devices/library versions may throw synchronously; just log and continue.
log(`Stop session threw (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
stopNext();
});
}
};
stopNext();
@@ -52,7 +58,7 @@ rl.on('line', (line) => {
switch (command) {
case 'play':
play(args.ip, args.url);
play(args.ip, args.url, args.metadata);
break;
case 'stop':
stop();
@@ -68,12 +74,13 @@ rl.on('line', (line) => {
}
});
function play(ip, url) {
function play(ip, url, metadata) {
if (activeClient) {
try { activeClient.close(); } catch (e) { }
}
activeClient = new Client();
activeClient._playMetadata = metadata || {};
activeClient.connect(ip, () => {
log(`Connected to ${ip}`);
@@ -100,20 +107,18 @@ function play(ip, url) {
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));
stopSessions(activeClient, [session], () => launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ true));
} else {
activePlayer = player;
loadMedia(url);
loadMedia(url, activeClient._playMetadata);
}
});
} else {
// If another app is running, stop it first to avoid NOT_ALLOWED.
// Backdrop or other non-media session present: skip stopping to avoid platform sender crash, just launch.
if (sessions.length > 0) {
log('Non-media session detected, stopping before launch...');
stopSessions(activeClient, sessions, () => launchPlayer(url, /*didStopFirst*/ true));
} else {
launchPlayer(url, /*didStopFirst*/ false);
log('Non-media session detected; skipping stop and launching DefaultMediaReceiver...');
}
launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ false);
}
});
});
@@ -126,7 +131,7 @@ function play(ip, url) {
});
}
function launchPlayer(url, didStopFirst) {
function launchPlayer(url, metadata, didStopFirst) {
if (!activeClient) return;
activeClient.launch(DefaultMediaReceiver, (err, player) => {
@@ -150,7 +155,7 @@ function launchPlayer(url, didStopFirst) {
return;
}
activePlayer = retryPlayer;
loadMedia(url);
loadMedia(url, metadata);
});
});
});
@@ -162,20 +167,23 @@ function launchPlayer(url, didStopFirst) {
return;
}
activePlayer = player;
loadMedia(url);
loadMedia(url, metadata);
});
}
function loadMedia(url) {
function loadMedia(url, metadata) {
if (!activePlayer) return;
const meta = metadata || {};
const media = {
contentId: url,
contentType: 'audio/mpeg',
streamType: 'LIVE',
metadata: {
metadataType: 0,
title: 'Radio 1'
title: meta.title || 'RadioPlayer',
subtitle: meta.artist || meta.station || undefined,
images: meta.image ? [{ url: meta.image }] : undefined
}
};

View File

@@ -9,6 +9,9 @@
"version": "1.0.0",
"dependencies": {
"castv2-client": "^1.2.0"
},
"bin": {
"radiocast-sidecar": "index.js"
}
},
"node_modules/@protobufjs/aspromise": {

1468
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
[package]
name = "radio-tauri"
version = "0.1.0"
version = "0.1.1"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
default-run = "radio-tauri"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -24,6 +25,17 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
rust_cast = "0.19.0"
mdns-sd = "0.17.1"
agnostic-mdns = { version = "0.4", features = ["tokio"], optional = true }
async-channel = "2.5.0"
tokio = { version = "1.48.0", features = ["full"] }
tauri-plugin-shell = "2.3.3"
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
base64 = "0.22"
cpal = "0.15"
ringbuf = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
[features]
use_agnostic_mdns = ["agnostic-mdns"]

View File

@@ -1,4 +1,4 @@
Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri)
Checking radio-tauri v0.1.0 (D:\Sites\Work\RadioPlayer\radio-tauri\src-tauri)
warning: variable does not need to be mutable
--> src\lib.rs:38:9
|

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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