From bac6665b3d739ff08353b46d02a91bda82b247d9 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Sun, 14 Jun 2026 10:04:27 +0800 Subject: [PATCH] Add Windows support via a Tauri (WebView2) shell zero-native has no Windows target yet, so add a parallel Tauri shell for Windows that reuses the same frontend: - src-tauri/: Rust commands (yt_search, yt_streams, store_load, store_save) mirroring the Zig bridge; spawns bundled yt-dlp.exe with CREATE_NO_WINDOW; data stored in app_data_dir - frontend bridge now auto-detects window.__TAURI__ vs window.zero and routes accordingly; CSP updated for Tauri IPC - tauri.conf.json bundles yt-dlp.exe as a resource and builds nsis/msi - scripts/make-icon.js generates appicon.png for 'tauri icon' - README: native Windows build steps + WSLg fallback note --- .gitignore | 6 + README.md | 57 +++++- appicon.png | Bin 0 -> 3309 bytes frontend/app.js | 24 ++- frontend/index.html | 2 +- package.json | 13 +- scripts/make-icon.js | 92 +++++++++ src-tauri/Cargo.toml | 22 +++ src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 7 + src-tauri/src/main.rs | 280 ++++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 39 ++++ 12 files changed, 527 insertions(+), 18 deletions(-) create mode 100644 appicon.png create mode 100644 scripts/make-icon.js create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json diff --git a/.gitignore b/.gitignore index 20b18fe..a94cdcb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,12 @@ build.zig build.zig.zon src/runner.zig +# tauri / rust build artifacts +src-tauri/target/ +src-tauri/gen/ +# generated by `npm run tauri icon ./appicon.png` +src-tauri/icons/ + # node node_modules/ diff --git a/README.md b/README.md index 2746a55..f23d5bc 100644 --- a/README.md +++ b/README.md @@ -24,21 +24,32 @@ or Node runtime. ## Architecture +The **same frontend** drives two native shells: + +- **Linux / macOS** → zero-native (Zig + system WebView) +- **Windows** → Tauri (Rust + WebView2) — see [Run on Windows](#run-on-windows-tauri) + ``` -frontend/ Static web UI (no framework, no build step) +frontend/ Static web UI (no framework, no build step) — shared index.html styles.css - app.js Player engine + UI; calls window.zero.invoke(...) -src/ + app.js Player engine + UI; auto-detects Tauri vs zero-native +src/ zero-native (Linux/macOS) shell main.zig App definition + bridge handler registration bridge.zig Native handlers: spawn yt-dlp, slim its JSON, local store +src-tauri/ Tauri (Windows) shell + src/main.rs Same handlers as Rust commands + tauri.conf.json Window, CSP, bundle (icons, yt-dlp resource) + Cargo.toml scripts/ setup-ytdlp.js Downloads the standalone yt-dlp binary into ./bin + make-icon.js Generates appicon.png for `tauri icon` bin/ yt-dlp lands here (gitignored) -app.zon App manifest (window, engine, permissions, frontend dir) +app.zon zero-native manifest ``` -The web UI talks to the Zig side over the zero-native bridge: +The web UI talks to the native side over whichever bridge is present +(`window.__TAURI__.core.invoke` on Windows, `window.zero.invoke` elsewhere): | `window.zero.invoke(...)` | Native handler | Returns | |---|---|---| @@ -58,8 +69,44 @@ and return only the compact fields the UI needs. - No system `yt-dlp` needed — the setup script bundles it. (On Linux/macOS the bundled build is self-contained; Python is not required.) +## Run on Windows (Tauri) + +zero-native does **not** target Windows yet, so the Windows build uses a Tauri +shell (Rust + the WebView2 runtime that ships with Windows 10/11). The result is +a small standalone `.exe`/installer — no bundled Chromium or Node. + +**Build natively on the Windows machine** (cross-compiling a WebView2 app from +WSL/Linux is unreliable, so do this on Windows): + +```powershell +# Prerequisites (one time): +# • Rust https://rustup.rs (MSVC toolchain) +# • Microsoft C++ Build Tools (Desktop development with C++) +# • WebView2 runtime — preinstalled on Win11; on Win10 grab the Evergreen runtime +# • Node.js (to run the helper scripts + Tauri CLI) + +npm install # installs the Tauri CLI (@tauri-apps/cli) +npm run setup # downloads yt-dlp.exe into .\bin +npm run make-icon # writes appicon.png +npm run tauri icon .\appicon.png # expands it into src-tauri\icons\* + +npm run tauri:dev # hot dev window +npm run tauri:build # produces the installer (see below) +``` + +The installer lands in +`src-tauri\target\release\bundle\` (`nsis\*-setup.exe` and `msi\*.msi`). +`yt-dlp.exe` is bundled as an app resource, so the installed app is self-contained. + +> Already in WSL and just want it running fast? Your WSL is WSLg-enabled, so you +> can instead build the **Linux** (zero-native) target and its window appears on +> your Windows desktop — see [Setup & run](#setup--run). That needs WSL running +> each time; the Tauri build above is a true standalone Windows app. + ## Setup & run +> Linux / macOS (zero-native). For Windows see [Run on Windows](#run-on-windows-tauri). + ```bash # 1. Download the yt-dlp binary into ./bin npm run setup # or: node scripts/setup-ytdlp.js diff --git a/appicon.png b/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..498405a325a9caa1c673dc8f2f5d4368eab52db2 GIT binary patch literal 3309 zcmZ`+dr(tn7C-mqMw9SLpf!S4!()6b_&|||kL5~nEh7&r%&McCU~w^9na17didx_f zRUY+)+loOHGppma^-(EWRzXOY8g)T&#vNdNG#8ly?sO$xEd+^S&wUUGt^efa+PGP7Yf6Rs?H3Ra%TZWS1;e>d z-$(Kocz{|qOV}Id+$c;Lbo|O)9<|wYk4EFol%@7-L6j(^cM)8C1Ikti>=0ao|58cT zV5?sP^d$69(93bZCTBSFvAif|u@9q;9(6`V*xN^9^lYwrJg$~x2b_;(y-(BBP{R`!3B=U)zsjHnk z3Ce!|_dQ#$Fx^eNeCfi@XYFgf^sa@iWba8}(&P|~Gb-~PcTsUSea77sGgXir0l!t~ zvlq4cPp<1s(`}e-uJ~zbdggfwS|VXDPNF7uB164p<*39{u z3L{krv=_%KVFtyGnIDW|C`L~36)3U7*#Jm8F~?ke1S$A;|7HZHSsE4HEwktSBf_V# z;JvGi)f#V`-BN@aG-JU^J?YMv0x`$D^yh0(T+>C4yil_gWO8PqG^$)lHW(dcV^Q!8 ztE=3i&_@^>^8DpIS&m2y~A=fSMa{5Gec6gcFC!Sp+DVy-!I=!amo zVABJ^DsE7A2xpZgS;qS&Cloa?Q5pw^~iQVj4CIX7Qy|yje3Sp4ndB#tLQTSHz{fFBogib`{&WIaR11Ho;=3O?&g6 z>0q8KJ$nUjJDm+DBFvGMTA9NzILyl;J&^d>2_RqiLwnk!d!`*~!nO6!!Th9)k9b)L zqO5yK;Ou@`iW)v0v~jcC(z%i=WOVSs)O_5;w$4=OTdpm3x&Gjq$eA%UpBGX*oeM_t zolj@LA@n7s$3CPR{E*8qI+DB@ooZ4H{7e7LdTW7U-WD_1hG z)c(O^Ci3Sze-w2g&q-eTmS=g6S2JHM=d_AKgoNVhs5pGTG%9=gTlvEzAqWwiDJ}uW z_eXD#LCuwYRT&(;CO_^s&Z+bVFKJZU^Ui_1rh)*pacTiSE1ks$pkw1Kb9Jh3qapBF z5|-QM5R3B2&ar`G(-(xjtvi7@cyMP!n(0F4g*#dJ?zDCFJ>0vVN6jzsW}0s*^f_V0 zwpy8SCi)fY*N+L>yTvmGN*CVx!u%P$!L8HA56tJ{;*%8fw(qGC)PzL zg7dvDJ(2RHe&?+)Zi}rqC}A2!Ntpp}+`ARfr&fY;5_~rYH{e--d*J}AD>%=bt!(Q1 z9XX%@^J%dA5(I1_*jEJZr=(?%gaU57R2VNe;o|-9RW*6Mx^)SOFNlRAEp<3!n6r~? znFnpt=9mNTbPmjA;sRj)Ul9&hJ;Du2csS!kKDEb(hieVw=~PAXbe3+QfnE){XDxaO zd-nkft~tI%e&F%8o+qKws8d7sJw=v88JOOM+b%K66*abv$vjVF*_O zY1l)?8qIF18URKsVZZTcIw$~cN3*{WNn#RLhHFtOp+o24+~0_8?-#hc98C!1d&FTd zZL|xP+m{3T4 invoke('yt.search', { query }), - getStreams: (videoId) => invoke('yt.streams', { videoId }), - loadData: () => invoke('store.load', {}), + search: (query) => call('yt.search', 'yt_search', { query }), + getStreams: (videoId) => call('yt.streams', 'yt_streams', { videoId }), + loadData: () => call('store.load', 'store_load', {}), // data is sent pre-stringified so the native side can write it verbatim. - saveData: (data) => invoke('store.save', { data: JSON.stringify(data) }), + saveData: (data) => call('store.save', 'store_save', { data: JSON.stringify(data) }), }; // ---------- State ---------- diff --git a/frontend/index.html b/frontend/index.html index 866043c..d06ada4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ YT Player diff --git a/package.json b/package.json index 8275cfb..0bc3b18 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,18 @@ { "name": "ytplayer", "version": "1.0.0", - "description": "Ad-free YouTube player with local playlists. No login, no ads, direct streams via yt-dlp. Built on zero-native (Zig + system WebView).", + "description": "Ad-free YouTube player with local playlists. No login, no ads, direct streams via yt-dlp. zero-native (Linux/macOS) and Tauri (Windows) shells.", "scripts": { "setup": "node scripts/setup-ytdlp.js", - "update-ytdlp": "node scripts/setup-ytdlp.js --force" + "update-ytdlp": "node scripts/setup-ytdlp.js --force", + "make-icon": "node scripts/make-icon.js", + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" }, "author": "", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "@tauri-apps/cli": "^2" + } } diff --git a/scripts/make-icon.js b/scripts/make-icon.js new file mode 100644 index 0000000..d7b923f --- /dev/null +++ b/scripts/make-icon.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Generates appicon.png (512x512) — a vermilion tile with a white play glyph. + * No image libraries: encodes a PNG by hand with Node's zlib. + * + * After running this, expand it into all platform sizes with the Tauri CLI: + * npm run tauri icon ./appicon.png + */ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); + +const SIZE = 512; + +// CRC32 (PNG chunk checksums) +const CRC_TABLE = (() => { + const t = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + t[n] = c >>> 0; + } + return t; +})(); +function crc32(buf) { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const typeBuf = Buffer.from(type, 'ascii'); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0); + return Buffer.concat([len, typeBuf, data, crc]); +} + +// --- paint pixels (RGBA) --- +const px = Buffer.alloc(SIZE * SIZE * 4); +function set(x, y, r, g, b, a = 255) { + const i = (y * SIZE + x) * 4; + px[i] = r; px[i + 1] = g; px[i + 2] = b; px[i + 3] = a; +} + +// Background: subtle vertical vermilion gradient (#ff6a52 -> #d4321d) +for (let y = 0; y < SIZE; y++) { + const t = y / SIZE; + const r = Math.round(0xff + (0xd4 - 0xff) * t); + const g = Math.round(0x6a + (0x32 - 0x6a) * t); + const b = Math.round(0x52 + (0x1d - 0x52) * t); + for (let x = 0; x < SIZE; x++) set(x, y, r, g, b); +} + +// White play triangle, centered, slightly right-nudged for optical balance. +const ax = 196, ay = 150, bx = 196, by = 362, cx = 384, cy = 256; +function edge(px1, py1, px2, py2, x, y) { + return (x - px1) * (py2 - py1) - (y - py1) * (px2 - px1); +} +for (let y = 120; y < 392; y++) { + for (let x = 170; x < 400; x++) { + const w0 = edge(bx, by, cx, cy, x, y); + const w1 = edge(cx, cy, ax, ay, x, y); + const w2 = edge(ax, ay, bx, by, x, y); + if ((w0 <= 0 && w1 <= 0 && w2 <= 0) || (w0 >= 0 && w1 >= 0 && w2 >= 0)) { + set(x, y, 255, 255, 255); + } + } +} + +// --- encode --- +const raw = Buffer.alloc((SIZE * 4 + 1) * SIZE); +for (let y = 0; y < SIZE; y++) { + raw[y * (SIZE * 4 + 1)] = 0; // filter: none + px.copy(raw, y * (SIZE * 4 + 1) + 1, y * SIZE * 4, (y + 1) * SIZE * 4); +} +const ihdr = Buffer.alloc(13); +ihdr.writeUInt32BE(SIZE, 0); +ihdr.writeUInt32BE(SIZE, 4); +ihdr[8] = 8; // bit depth +ihdr[9] = 6; // color type RGBA +const png = Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk('IHDR', ihdr), + chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), + chunk('IEND', Buffer.alloc(0)), +]); + +const out = path.join(__dirname, '..', 'appicon.png'); +fs.writeFileSync(out, png); +console.log(`Wrote ${out} (${SIZE}x${SIZE}).`); +console.log('Now run: npm run tauri icon ./appicon.png'); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..ebecd46 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ytplayer" +version = "1.0.0" +description = "Ad-free YouTube player with local playlists" +authors = ["Jonathan Sykes"] +edition = "2021" +rust-version = "1.77" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true +panic = "abort" +codegen-units = 1 diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..25544c6 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Core capabilities for the main window. App-defined commands (yt_search, yt_streams, store_load, store_save) are not gated by the ACL.", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..f4fef48 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,280 @@ +// YT Player — Tauri (Windows / WebView2) backend. +// +// Mirrors the zero-native Zig bridge, but as Tauri commands. The same frontend +// in ../frontend calls these via window.__TAURI__.core.invoke(...). Returns the +// exact JSON shapes the UI expects: +// yt_search { query } -> { ok, results:[…] } +// yt_streams { videoId } -> { ok, data:{ meta, audioUrl, qualities[] } } +// store_load {} -> playlists / history / settings +// store_save { data } -> { ok } +// +// Unlike the size-limited zero-native bridge, Tauri's IPC has no fixed buffer, +// but we still return only the slim fields the UI needs. + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use serde_json::{json, Value}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::process::Command; +use tauri::Manager; + +const SEARCH_LIMIT: u32 = 25; + +/// Locate yt-dlp: prefer the bundled resource copy, then a dev ./bin, then PATH. +fn ytdlp_path(app: &tauri::AppHandle) -> PathBuf { + let name = if cfg!(windows) { "yt-dlp.exe" } else { "yt-dlp" }; + if let Ok(res) = app.path().resource_dir() { + let p = res.join("bin").join(name); + if p.exists() { + return p; + } + } + let dev = PathBuf::from("bin").join(name); + if dev.exists() { + return dev; + } + PathBuf::from(name) +} + +fn run_ytdlp(app: &tauri::AppHandle, args: &[&str]) -> Result { + let exe = ytdlp_path(app); + let mut cmd = Command::new(&exe); + cmd.args(args); + + // Don't flash a console window when spawning yt-dlp on Windows. + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + + let output = cmd + .output() + .map_err(|e| format!("failed to launch yt-dlp ({}): {e}", exe.display()))?; + + if !output.status.success() { + let err = String::from_utf8_lossy(&output.stderr); + return Err(if err.trim().is_empty() { + "yt-dlp failed".to_string() + } else { + err.trim().to_string() + }); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +fn str_or<'a>(v: &'a Value, key: &str, fallback: &'a str) -> &'a str { + v.get(key).and_then(|x| x.as_str()).unwrap_or(fallback) +} + +#[tauri::command] +async fn yt_search(app: tauri::AppHandle, query: String) -> Value { + let q = query.trim(); + if q.is_empty() { + return json!({ "ok": false, "error": "empty query" }); + } + let search = format!("ytsearch{}:{}", SEARCH_LIMIT, q); + let out = match run_ytdlp( + &app, + &[ + &search, + "--dump-json", + "--flat-playlist", + "--no-warnings", + "--ignore-errors", + ], + ) { + Ok(o) => o, + Err(e) => return json!({ "ok": false, "error": e }), + }; + + let mut results = Vec::new(); + for line in out.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let j: Value = match serde_json::from_str(line) { + Ok(v) => v, + Err(_) => continue, + }; + let id = str_or(&j, "id", ""); + if id.is_empty() { + continue; + } + let channel = { + let c = str_or(&j, "channel", ""); + if c.is_empty() { + str_or(&j, "uploader", "") + } else { + c + } + }; + results.push(json!({ + "id": id, + "title": str_or(&j, "title", "(untitled)"), + "channel": channel, + "duration": j.get("duration").and_then(|x| x.as_f64()).unwrap_or(0.0), + "thumbnail": format!("https://i.ytimg.com/vi/{}/mqdefault.jpg", id), + })); + } + + json!({ "ok": true, "results": results }) +} + +#[tauri::command] +async fn yt_streams(app: tauri::AppHandle, video_id: String) -> Value { + if video_id.is_empty() { + return json!({ "ok": false, "error": "missing videoId" }); + } + let url = format!("https://www.youtube.com/watch?v={}", video_id); + let out = match run_ytdlp(&app, &["-J", "--no-warnings", &url]) { + Ok(o) => o, + Err(e) => return json!({ "ok": false, "error": e }), + }; + let info: Value = match serde_json::from_str(&out) { + Ok(v) => v, + Err(e) => return json!({ "ok": false, "error": format!("parse error: {e}") }), + }; + + let empty: Vec = Vec::new(); + let formats = info.get("formats").and_then(|f| f.as_array()).unwrap_or(&empty); + + // Best audio-only stream (prefer m4a/mp4a, then by bitrate). + let mut best_audio_url = String::new(); + let mut best_audio_score = -1.0_f64; + for f in formats { + let vcodec = str_or(f, "vcodec", "none"); + let acodec = str_or(f, "acodec", "none"); + let furl = str_or(f, "url", ""); + if furl.is_empty() { + continue; + } + let has_video = vcodec != "none" && !vcodec.is_empty(); + let has_audio = acodec != "none" && !acodec.is_empty(); + if !has_video && has_audio { + let mut score = f.get("abr").and_then(|x| x.as_f64()).unwrap_or(0.0); + if acodec.contains("mp4a") { + score += 1000.0; + } + if score > best_audio_score { + best_audio_score = score; + best_audio_url = furl.to_string(); + } + } + } + + // Quality list: adaptive video-only first, then progressive; dedupe by height. + let mut qualities = Vec::new(); + let mut seen: HashSet = HashSet::new(); + for want_progressive in [false, true] { + for f in formats { + let vcodec = str_or(f, "vcodec", "none"); + let acodec = str_or(f, "acodec", "none"); + let furl = str_or(f, "url", ""); + if furl.is_empty() { + continue; + } + let has_video = vcodec != "none" && !vcodec.is_empty(); + let has_audio = acodec != "none" && !acodec.is_empty(); + if !has_video { + continue; + } + let is_progressive = has_audio; + if is_progressive != want_progressive { + continue; + } + let height = f.get("height").and_then(|x| x.as_i64()).unwrap_or(0); + if height <= 0 || seen.contains(&height) { + continue; + } + seen.insert(height); + qualities.push(json!({ + "label": format!("{}p", height), + "height": height, + "hasAudio": is_progressive, + "url": furl, + "ext": str_or(f, "ext", ""), + })); + } + } + qualities.sort_by(|a, b| { + b["height"] + .as_i64() + .unwrap_or(0) + .cmp(&a["height"].as_i64().unwrap_or(0)) + }); + + let channel = { + let c = str_or(&info, "channel", ""); + if c.is_empty() { + str_or(&info, "uploader", "") + } else { + c + } + }; + + json!({ + "ok": true, + "data": { + "meta": { + "id": video_id, + "title": str_or(&info, "title", "(untitled)"), + "channel": channel, + "duration": info.get("duration").and_then(|x| x.as_f64()).unwrap_or(0.0), + "thumbnail": format!("https://i.ytimg.com/vi/{}/hqdefault.jpg", video_id), + }, + "audioUrl": if best_audio_url.is_empty() { Value::Null } else { Value::String(best_audio_url) }, + "qualities": qualities, + } + }) +} + +fn data_file(app: &tauri::AppHandle) -> Result { + let dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + std::fs::create_dir_all(&dir).ok(); + Ok(dir.join("ytplayer-data.json")) +} + +#[tauri::command] +fn store_load(app: tauri::AppHandle) -> Value { + let default = json!({ + "playlists": [], + "history": [], + "settings": { "quality": "auto", "volume": 1, "audioOnly": false } + }); + let path = match data_file(&app) { + Ok(p) => p, + Err(_) => return default, + }; + match std::fs::read_to_string(&path) { + Ok(s) => serde_json::from_str::(&s).unwrap_or(default), + Err(_) => default, + } +} + +#[tauri::command] +fn store_save(app: tauri::AppHandle, data: String) -> Value { + let path = match data_file(&app) { + Ok(p) => p, + Err(e) => return json!({ "ok": false, "error": e }), + }; + match std::fs::write(&path, data) { + Ok(_) => json!({ "ok": true }), + Err(e) => json!({ "ok": false, "error": e.to_string() }), + } +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + yt_search, + yt_streams, + store_load, + store_save + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..f5a019f --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "YT Player", + "version": "1.0.0", + "identifier": "com.ytplayer.app", + "build": { + "frontendDist": "../frontend" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "YT Player", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "resizable": true + } + ], + "security": { + "csp": "default-src 'self'; img-src 'self' https: data: asset: http://asset.localhost; media-src 'self' https: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com data:; script-src 'self'; connect-src 'self' https: ipc: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": ["nsis", "msi"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "resources": { + "../bin/yt-dlp.exe": "bin/yt-dlp.exe" + } + } +}