Compare commits

..

36 Commits

Author SHA1 Message Date
27fda0430b v1.1.23 2026-09-04 22:37:36 -07:00
8b49d679bd add macOS ATS exception domain for localhost webview requests 2026-09-04 22:37:15 -07:00
62883b3180 v1.1.22 2026-09-04 22:27:50 -07:00
3c6ae0125b use location.replace for WKWebView navigation 2026-09-04 22:27:24 -07:00
be0810ea6d fix: only check platform-matching node binary extension; bump to 1.1.20 2026-09-04 22:02:38 -07:00
dff55060e7 fix: only check platform-matching node binary extension 2026-09-04 22:02:16 -07:00
df767b5ffc v1.1.20 2026-09-04 21:54:10 -07:00
1b70c6784a resolve bundled resources under both _up_ and up folder names 2026-09-04 21:53:44 -07:00
e25e1c93f9 chore: bump version to 1.1.19 2026-09-04 21:37:32 -07:00
4c708ad97c preserve execute bit on bundled macOS node binary 2026-09-04 21:34:41 -07:00
b81a4e044b v1.1.18 2026-09-04 21:19:06 -07:00
f6aa2daa01 fixed (os error 13) 2026-09-04 21:13:11 -07:00
e03eb38adc v1.1.17 2026-09-04 21:04:44 -07:00
2b690b47c1 fixed typo 2026-09-04 20:58:53 -07:00
dffbdb92a5 v1.1.16 2026-09-04 20:52:21 -07:00
45d6515f2e fixed some issues...? 2026-09-04 20:46:08 -07:00
a3e6a278fe v1.1.15 2026-09-04 20:15:10 -07:00
7ffc20ee48 fixed harmless warning 2026-09-04 20:11:01 -07:00
ddf3abb314 add macOS Node runtime and appdata support 2026-09-04 20:08:27 -07:00
ec50c4e07f v1.1.14 2026-09-04 19:55:59 -07:00
49b2a7622b added debug text 2026-09-04 19:51:27 -07:00
b8e474cb92 Fix Mac tar extraction argument parsing 2026-09-04 19:35:21 -07:00
8b29c9e3c7 Fix Mac Node architecture and resources bundle 2026-09-04 19:30:40 -07:00
5dab61b359 Fix release installer path detection 2026-09-04 19:17:05 -07:00
eab843aa16 Detect and upload macOS DMG bundle 2026-09-04 19:10:21 -07:00
b757f741b7 Fix release log object and multi-platform extension replacement 2026-09-04 19:04:10 -07:00
5f4c47a07c Handle duplicate release asset links 2026-09-04 18:58:51 -07:00
32677c9fb2 Fix Tauri window builder for macOS 2026-09-04 18:53:56 -07:00
7eab57cfd9 Fix Rust PATH and lock release version 2026-09-04 18:50:17 -07:00
c8fc7da64d Configure free Mac cloud workflow 2026-09-04 18:47:30 -07:00
88c1824bf7 removed the gitlab yml 2026-09-04 18:45:22 -07:00
a50cb2b6b8 Fix Windows instance type 2026-09-04 18:45:15 -07:00
971eae4af1 Add Windows and macOS Codemagic workflows 2026-09-04 18:44:28 -07:00
1400d538f3 support mac user test 2026-09-04 18:32:40 -07:00
b1d01d94da v1.1.13 2026-09-04 14:38:43 -07:00
9d7e218ff6 v1.1.12 2026-09-01 20:20:53 -07:00
11 changed files with 526 additions and 251 deletions

View File

@@ -18,20 +18,23 @@ const REPO_CONFIG = {
'stream-api': { name: 'stream-api', branch: 'main' },
};
const clonedRepos = new Set();
function clonePrivateRepo(repoConfig, targetDir) {
if (fs.existsSync(targetDir)) {
console.log(`Removing stale ${repoConfig.name} before fresh clone...`);
fs.rmSync(targetDir, { recursive: true, force: true });
}
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
const auth = token ? `oauth2:${token}@` : '';
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
try {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
}
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
stdio: 'inherit'
});
clonedRepos.add(repoConfig.name);
} catch (error) {
throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`);
}
@@ -332,9 +335,11 @@ async function build() {
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
for (const repo of REPOS) {
const repoPath = path.join(__dirname, repo);
if (fs.existsSync(repoPath)) {
fs.rmSync(repoPath, { recursive: true, force: true });
if (clonedRepos.has(repo)) {
const repoPath = path.join(__dirname, repo);
if (fs.existsSync(repoPath)) {
fs.rmSync(repoPath, { recursive: true, force: true });
}
}
}

46
codemagic.yaml Normal file
View File

@@ -0,0 +1,46 @@
workflows:
macos-release:
name: Build macOS Release
max_build_duration: 60
instance_type: mac_mini_m2
environment:
groups:
- gitlab_credentials
node: 22.14.0
cache:
cache_paths:
- $HOME/.cargo/registry
- $HOME/.cargo/git
- src-tauri/target
scripts:
- name: Setup Rust toolchain
script: |
if ! command -v cargo &> /dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
fi
- name: Install dependencies
script: |
export PATH="$HOME/.cargo/bin:$PATH"
npm install
- name: Build & Publish macOS Release
script: |
export PATH="$HOME/.cargo/bin:$PATH"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
node release.js --installer --tag="v$PACKAGE_VERSION"
- name: Verify bundled node permissions
script: |
if [ -f "bin/node" ]; then
chmod +x bin/node
xattr -cr bin/node || true
ls -la bin/node
file bin/node
fi
APP_NODE=$(find src-tauri/target/release/bundle -name "node" -path "*bin*" 2>/dev/null | head -1)
if [ -n "$APP_NODE" ]; then
chmod +x "$APP_NODE"
xattr -cr "$APP_NODE" || true
ls -la "$APP_NODE"
fi
artifacts:
- src-tauri/target/release/bundle/dmg/*.dmg
- src-tauri/target/release/bundle/macos/*.app

54
main.js
View File

@@ -6,10 +6,19 @@ const ServiceManager = require('./service-manager');
const Patcher = require('./patcher');
const UpdateClient = require('./update-client');
const defaultAppData = path.join(os.homedir(), 'AppData', 'Roaming', 'vyla-home', 'vyla-home-files');
function getDefaultAppData() {
if (process.platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support', 'vyla-home', 'vyla-home-files');
}
if (process.platform === 'linux') {
return path.join(os.homedir(), '.local', 'share', 'vyla-home', 'vyla-home-files');
}
return path.join(os.homedir(), 'AppData', 'Roaming', 'vyla-home', 'vyla-home-files');
}
const APPDATA_DIR = (process.env.APPDATA_PATH && !process.env.APPDATA_PATH.includes('com.vyla.home'))
? process.env.APPDATA_PATH
: defaultAppData;
: getDefaultAppData();
const LOGS_DIR = path.join(APPDATA_DIR, 'logs');
try { fs.mkdirSync(LOGS_DIR, { recursive: true }); } catch (e) { }
@@ -203,10 +212,34 @@ async function start() {
try {
sendToTauri('status', { message: 'Inspecting local workspace and installed files...' });
const isFirstRun = !patcher.verifyExtraction();
const localVersion = patcher.getCurrentVersion();
const localSha = patcher.getCurrentSha();
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`);
let isFirstRun = !patcher.verifyExtraction();
let localVersion = patcher.getCurrentVersion();
let localSha = patcher.getCurrentSha();
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}, Local SHA: ${localSha}`);
const localZipPath = path.join(__dirname, 'update-payload.zip');
const localManifestPath = path.join(__dirname, 'manifest.json');
let localZipManifest = null;
if (fs.existsSync(localZipPath) && fs.existsSync(localManifestPath)) {
try { localZipManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8')); } catch (e) { }
}
if (localZipManifest && (isFirstRun || localZipManifest.sha256 !== localSha)) {
debugLog("New local payload detected! Applying local build...");
sendToTauri('status', { message: 'Extracting local development payload...' });
patcher.purgeOldCode();
patcher.extractZipPayload(localZipPath);
patcher.applyPatches(getReplacementsConfig());
patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion(localZipManifest.version, localZipManifest.sha256);
serviceManager.envConfig = getEnvConfig();
isFirstRun = false;
localVersion = localZipManifest.version;
localSha = localZipManifest.sha256;
debugLog("Local payload applied successfully.");
}
sendToTauri('status', { message: 'Connecting to update servers...' });
let release = null;
@@ -277,7 +310,14 @@ async function start() {
patcher.extractZipPayload(localZip);
patcher.applyPatches(getReplacementsConfig());
patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion("1.0.0");
let ver = "1.0.0";
try {
const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8'));
ver = man.version || ver;
} catch (e) { }
patcher.saveCurrentVersion(ver);
serviceManager.envConfig = getEnvConfig();
debugLog("Local payload extracted successfully.");
} else {

View File

@@ -1,6 +1,6 @@
{
"name": "vyla-home",
"version": "1.1.11",
"version": "1.1.23",
"description": "Vyla Home Desktop App Launcher",
"main": "main.js",
"scripts": {
@@ -54,4 +54,4 @@
"installerHeaderIcon": "build/icon.ico"
}
}
}
}

View File

@@ -78,12 +78,12 @@ function uploadFileToGitLabPackage(filePath, packageName, version, fileName) {
}
function createGitLabRelease(tag, releaseLinks) {
const sendRequest = (method, path, data = null) => {
const sendRequest = (method, reqPath, data = null) => {
return new Promise((resolve, reject) => {
const options = {
hostname: 'gitlab.com',
port: 443,
path: path,
path: reqPath,
method: method,
headers: {
'PRIVATE-TOKEN': TOKEN,
@@ -101,19 +101,9 @@ function createGitLabRelease(tag, releaseLinks) {
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(body ? JSON.parse(body) : {});
} catch (e) {
resolve({});
}
} else if (res.statusCode === 409 && method === 'POST') {
console.log(`Release ${tag} already exists, deleting and recreating...`);
sendRequest('DELETE', `/api/v4/projects/${PROJECT_ID}/releases/${encodeURIComponent(tag)}`)
.then(() => sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, data))
.then(resolve)
.catch(reject);
try { resolve(body ? JSON.parse(body) : {}); } catch (e) { resolve({}); }
} else {
reject(new Error(`Failed to publish release (HTTP ${res.statusCode}): ${body}`));
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
}
});
});
@@ -133,13 +123,37 @@ function createGitLabRelease(tag, releaseLinks) {
name: `Vyla Home Release ${tag}`,
tag_name: tag,
ref: currentRef,
description: `Automated local release ${tag}`,
description: `Automated Multi-Platform Release ${tag}`,
assets: {
links: releaseLinks
}
};
return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, releaseData);
return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases`, releaseData)
.catch(err => {
if (err.message.includes('HTTP 409')) {
console.log(`[CI Sync] Release ${tag} already exists. Appending our OS-specific files to it...`);
let promiseChain = Promise.resolve();
for (const link of releaseLinks) {
promiseChain = promiseChain.then(() => {
return sendRequest('POST', `/api/v4/projects/${PROJECT_ID}/releases/${encodeURIComponent(tag)}/assets/links`, {
name: link.name,
url: link.url,
link_type: link.link_type
}).catch(e => {
if (e.message.includes('already been taken') || e.message.includes('HTTP 409') || e.message.includes('HTTP 400')) {
console.log(`[CI Sync] Asset link '${link.name}' is already attached. Skipping.`);
return Promise.resolve();
}
throw e;
});
});
}
return promiseChain.then(() => ({ name: `Vyla Home Release ${tag}`, tag_name: tag }));
}
throw err;
});
}
async function main() {
@@ -166,7 +180,6 @@ async function main() {
if (isFullBuild) {
console.log('Ensuring bundled Node.js runtime...');
await ensureBundledNode();
await ensureWebView2Loader();
const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle');
@@ -174,20 +187,32 @@ async function main() {
fs.rmSync(bundleDir, { recursive: true, force: true });
}
console.log('Compiling Windows Tauri installer locally...');
execSync('npm run build', {
stdio: 'inherit'
});
console.log('Compiling native Tauri application...');
execSync('npm run build', { stdio: 'inherit' });
const nsisDir = path.join(__dirname, 'src-tauri/target/release/bundle/nsis');
if (fs.existsSync(nsisDir)) {
const targetVersion = version.startsWith('v') ? version.substring(1) : version;
const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe') && f.includes(targetVersion));
const targetVersion = version.startsWith('v') ? version.substring(1) : version;
if (files.length > 0) {
const originalName = files[0];
installerPath = path.join(nsisDir, originalName);
installerUploadName = originalName.replace(/\s+/g, '-');
if (process.platform === 'win32') {
const nsisDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle', 'nsis');
if (fs.existsSync(nsisDir)) {
const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe') && f.includes(targetVersion));
if (files.length > 0) {
installerPath = path.join(nsisDir, files[0]);
installerUploadName = files[0].replace(/\s+/g, '-');
}
}
} else {
const dmgDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle', 'dmg');
console.log(`Checking for Mac installer in: ${dmgDir}`);
if (fs.existsSync(dmgDir)) {
const files = fs.readdirSync(dmgDir).filter(f => f.endsWith('.dmg') && f.includes(targetVersion));
if (files.length > 0) {
installerPath = path.join(dmgDir, files[0]);
installerUploadName = files[0].replace(/\s+/g, '-');
console.log(`Found Mac installer: ${installerUploadName}`);
} else {
console.log(`[WARNING] No .dmg found containing version ${targetVersion}`);
}
}
}
}
@@ -208,19 +233,25 @@ async function main() {
if (installerPath && installerUploadName) {
console.log(`Uploading installer ${installerUploadName}...`);
const installerUnique = installerUploadName.replace('.exe', `-${timestamp}.exe`);
const ext = path.extname(installerUploadName);
const installerUnique = installerUploadName.replace(ext, `-${timestamp}${ext}`);
const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUnique);
releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' });
} else {
console.log(`[WARNING] No installer was selected for upload! (isFullBuild: ${isFullBuild})`);
}
console.log('[3/4] Publishing GitLab Release...');
const release = await createGitLabRelease(version, releaseLinks);
console.log(`[4/4] Release successfully created: ${release.name} (${release.tag_name})`);
await createGitLabRelease(version, releaseLinks);
console.log(`[4/4] Release process successfully completed for ${version}!`);
}
function ensureBundledNode() {
const os = require('os');
const binDir = path.join(__dirname, 'bin');
const nodeExePath = path.join(binDir, 'node.exe');
const isWin = os.platform() === 'win32';
const nodeExeName = isWin ? 'node.exe' : 'node';
const nodeExePath = path.join(binDir, nodeExeName);
const nodeVersion = 'v22.14.0';
const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`);
@@ -232,35 +263,65 @@ function ensureBundledNode() {
fs.mkdirSync(binDir, { recursive: true });
console.log(`Downloading standalone Node.js ${nodeVersion} runtime for bundling...`);
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`;
return new Promise((resolve, reject) => {
const fileStream = fs.createWriteStream(nodeExePath);
https.get(nodeUrl, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
https.get(res.headers.location, (redirectRes) => {
redirectRes.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
resolve();
});
}).on('error', reject);
return;
}
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
const download = (url, dest, cb) => {
https.get(url, (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
return download(res.headers.location, dest, cb);
}
const fileStream = fs.createWriteStream(dest);
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
cb();
});
}).on('error', reject);
};
if (isWin) {
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`;
download(nodeUrl, nodeExePath, () => {
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
resolve();
});
}).on('error', (err) => {
try { fs.unlinkSync(nodeExePath); } catch (e) { }
reject(err);
});
} else {
const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
const platform = os.platform() === 'darwin' ? 'darwin' : 'linux';
const tarName = `node-${nodeVersion}-${platform}-${arch}`;
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/${tarName}.tar.gz`;
const tarPath = path.join(binDir, 'node.tar.gz');
download(nodeUrl, tarPath, () => {
try {
execSync(`tar -xzf node.tar.gz ${tarName}/bin/node`, { cwd: binDir });
const extractedNode = path.join(binDir, tarName, 'bin', 'node');
const finalNode = path.join(binDir, 'node');
fs.renameSync(extractedNode, finalNode);
fs.rmSync(path.join(binDir, tarName), { recursive: true, force: true });
fs.unlinkSync(tarPath);
fs.chmodSync(finalNode, 0o755);
try {
execSync(`xattr -d com.apple.quarantine "${finalNode}"`);
} catch (_) { }
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
resolve();
} catch (e) {
reject(e);
}
});
}
});
}
function ensureWebView2Loader() {
const os = require('os');
if (os.platform() !== 'win32') return Promise.resolve();
const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release');
const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll');
fs.mkdirSync(releaseDir, { recursive: true });

2
src-tauri/Cargo.lock generated
View File

@@ -4222,7 +4222,7 @@ dependencies = [
[[package]]
name = "vyla-home"
version = "1.1.11"
version = "1.1.23"
dependencies = [
"base64 0.22.1",
"serde",

View File

@@ -1,6 +1,6 @@
[package]
name = "vyla-home"
version = "1.1.11"
version = "1.1.23"
description = "Vyla Home Desktop App Launcher"
edition = "2021"

View File

@@ -1,5 +1,20 @@
fn main() {
println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=tauri.conf.json");
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {
let mut target_dir = std::path::PathBuf::from(out_dir);
target_dir.pop();
target_dir.pop();
target_dir.pop();
let dll_src = std::path::Path::new(&manifest_dir).join("WebView2Loader.dll");
let dll_dest = target_dir.join("WebView2Loader.dll");
if dll_src.exists() {
let _ = std::fs::copy(dll_src, dll_dest);
}
}
}
tauri_build::build();
}

View File

@@ -7,26 +7,19 @@ use tauri::{Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
fn debug_log(msg: &str) {
let log_dir = if let Ok(app_data) = std::env::var("APPDATA") {
std::path::PathBuf::from(app_data)
.join("vyla-home")
.join("vyla-home-files")
.join("logs")
std::path::PathBuf::from(app_data).join("vyla-home").join("vyla-home-files").join("logs")
} else if let Ok(home) = std::env::var("HOME") {
std::path::PathBuf::from(home).join("Library").join("Logs").join("VylaHome")
} else if let Ok(user_profile) = std::env::var("USERPROFILE") {
std::path::PathBuf::from(user_profile)
.join(".vyla-home")
.join("logs")
std::path::PathBuf::from(user_profile).join(".vyla-home").join("logs")
} else {
std::path::PathBuf::from("logs")
std::env::temp_dir().join("vyla-home-logs")
};
let _ = std::fs::create_dir_all(&log_dir);
let log_path = log_dir.join("rust-debug.log");
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&log_path) {
use std::io::Write;
let _ = writeln!(file, "[RUST] {}", msg);
}
@@ -65,11 +58,23 @@ fn kill_child_process(child: &mut Child) {
fn find_node_binary(app: &tauri::App) -> Option<String> {
if let Ok(resource_dir) = app.path().resource_dir() {
#[cfg(windows)]
let candidates = [
resource_dir.join("bin").join("node.exe"),
resource_dir.join("node.exe"),
resource_dir.join("_up_").join("bin").join("node.exe"),
resource_dir.join("_up_").join("node.exe"),
resource_dir.join("up").join("bin").join("node.exe"),
resource_dir.join("up").join("node.exe"),
];
#[cfg(not(windows))]
let candidates = [
resource_dir.join("bin").join("node"),
resource_dir.join("node"),
resource_dir.join("_up_").join("bin").join("node"),
resource_dir.join("_up_").join("node"),
resource_dir.join("up").join("bin").join("node"),
resource_dir.join("up").join("node"),
];
for candidate in candidates {
if candidate.exists() {
@@ -82,9 +87,13 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
if let Some(parent) = exe_path.parent() {
let candidates = [
parent.join("bin").join("node.exe"),
parent.join("bin").join("node"),
parent.join("node.exe"),
parent.join("node"),
parent.join("resources").join("bin").join("node.exe"),
parent.join("resources").join("bin").join("node"),
parent.join("resources").join("node.exe"),
parent.join("resources").join("node"),
];
for candidate in candidates {
if candidate.exists() {
@@ -96,9 +105,13 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
if let Ok(path_var) = std::env::var("PATH") {
for p in std::env::split_paths(&path_var) {
let candidate = p.join("node.exe");
if candidate.exists() {
return Some(clean_path(&candidate));
let node_exe = p.join("node.exe");
if node_exe.exists() {
return Some(clean_path(&node_exe));
}
let node = p.join("node");
if node.exists() {
return Some(clean_path(&node));
}
}
}
@@ -127,6 +140,22 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
}
}
#[cfg(not(windows))]
{
let default_paths = [
"/usr/local/bin/node",
"/usr/bin/node",
"/opt/homebrew/bin/node",
"/opt/local/bin/node",
];
for path in default_paths {
let candidate = std::path::Path::new(path);
if candidate.exists() {
return Some(clean_path(candidate));
}
}
}
None
}
@@ -135,6 +164,7 @@ fn resolve_script_and_root(app: &tauri::App) -> (String, String) {
let candidates = vec![
resource_dir.join("main.js"),
resource_dir.join("_up_").join("main.js"),
resource_dir.join("up").join("main.js"),
];
for candidate in candidates {
@@ -568,13 +598,60 @@ fn main() {
.setup(|app| {
debug_log("Tauri setup started");
let app_data = app.path().app_data_dir().unwrap_or_default();
let app_data_str = clean_path(&app_data);
let vyla_files = if let Ok(appdata_env) = std::env::var("APPDATA") {
std::path::PathBuf::from(appdata_env)
.join("vyla-home")
.join("vyla-home-files")
} else if let Ok(home) = std::env::var("HOME") {
#[cfg(target_os = "macos")]
let base = std::path::PathBuf::from(&home)
.join("Library")
.join("Application Support");
#[cfg(target_os = "linux")]
let base = std::path::PathBuf::from(&home)
.join(".local")
.join("share");
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
let base = app_data.clone();
base.join("vyla-home").join("vyla-home-files")
} else {
app_data.join("vyla-home-files")
};
let version_file = vyla_files.join("version.json");
let current_app_version = app.package_info().version.to_string();
let mut version_mismatch = false;
if version_file.exists() {
if let Ok(content) = std::fs::read_to_string(&version_file) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
if version != current_app_version {
version_mismatch = true;
}
} else {
version_mismatch = true;
}
} else {
version_mismatch = true;
}
}
}
if version_mismatch {
debug_log(&format!("App version updated to {}. Cleaning old files.", current_app_version));
let dirs_to_remove = ["frontend", "player", "stream-api", "auth-proxy", "config"];
for d in dirs_to_remove {
let _ = std::fs::remove_dir_all(vyla_files.join(d));
}
let _ = std::fs::remove_file(&version_file);
}
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
.title("Vyla Home")
.inner_size(1280.0, 800.0)
.decorations(false)
.transparent(false)
.resizable(true)
.devtools(cfg!(debug_assertions))
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
@@ -582,23 +659,29 @@ fn main() {
.build()?;
let (main_js, root_dir) = resolve_script_and_root(app);
debug_log("Searching for node binary...");
let node_bin = match find_node_binary(app) {
Some(bin) => bin,
Some(bin) => {
debug_log(&format!("Found node binary: {}", bin));
bin
}
None => {
let _ = window.emit(
"error",
serde_json::json!({
"message": "Node.js runtime not found. Please ensure Node.js is installed or bundled correctly."
}),
);
debug_log("FATAL: No node binary found in bundle or PATH.");
std::thread::sleep(std::time::Duration::from_millis(600));
let _ = window.eval("document.getElementById('status-message').textContent='Error: Node runtime missing. Please reinstall.';");
let _ = window.eval("var b=document.getElementById('debug-log-box');b.style.display='block';b.textContent='Node binary not found. bin/node missing from app bundle or not executable.';");
return Ok(());
}
};
let vyla_files_str = clean_path(&vyla_files);
std::thread::sleep(std::time::Duration::from_millis(400));
let mut cmd = Command::new(&node_bin);
cmd.arg(&main_js)
.current_dir(&root_dir)
.env("APPDATA_PATH", &app_data_str)
.env("APPDATA_PATH", &vyla_files_str)
.env("NODE_EXE", &node_bin)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -618,11 +701,15 @@ fn main() {
child.id()
));
if let Some(stderr) = child.stderr.take() {
let w = window.clone();
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stderr);
for line in reader.lines().flatten() {
debug_log(&format!("[NODE STDERR] {}", line));
if line.contains("Error") || line.contains("Exception") || line.contains("MODULE_NOT_FOUND") {
let _ = w.emit("error", serde_json::json!({ "message": format!("Backend failed: {}", line) }));
}
}
});
}
@@ -786,12 +873,12 @@ fn main() {
}
Err(e) => {
debug_log(&format!("Failed to spawn node: {}", e));
let _ = window.emit(
"error",
serde_json::json!({
"message": format!("Failed to launch backend runtime: {}", e)
}),
let emsg = format!("Failed to spawn node: {}", e);
let js = format!(
"document.getElementById('status-message').textContent='Spawn Error';var b=document.getElementById('debug-log-box');b.style.display='block';b.textContent={};",
serde_json::to_string(&emsg).unwrap_or_default()
);
let _ = window.eval(&js);
}
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Vyla Home",
"version": "1.1.11",
"version": "1.1.23",
"identifier": "com.vyla.home",
"build": {
"frontendDist": "../ui"
@@ -22,6 +22,9 @@
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"exceptionDomain": "localhost"
},
"windows": {
"webviewInstallMode": {
"type": "downloadBootstrapper"
@@ -29,8 +32,9 @@
},
"resources": [
"WebView2Loader.dll",
"../bin/*",
"../bin/**/*",
"../main.js",
"../preload.js",
"../service-manager.js",
"../patcher.js",
"../crypto-utils.js",
@@ -40,4 +44,4 @@
"../node_modules/**/*"
]
}
}
}

View File

@@ -195,6 +195,9 @@
<div class="loader-container">
<div class="loader-card">
<div class="status-text" id="status-message">Starting runtime engine...</div>
<div id="debug-log-box"
style="display: none; width: 100%; max-height: 180px; overflow-y: auto; background: rgba(255, 50, 50, 0.12); border: 1px solid rgba(255, 100, 100, 0.3); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; color: #fca5a5; text-align: left; white-space: pre-wrap; word-break: break-all; margin-bottom: 20px; user-select: text;">
</div>
<div class="progress-track" id="progress-track" style="display: none;">
<div class="progress-fill" id="progress-fill"></div>
</div>
@@ -257,7 +260,7 @@
</div>
</div>
</div>
</div>
</div>
<script type="module">
@@ -268,61 +271,66 @@
console.warn("OGL Shader background failed to load (offline):", err);
}
if (!window.__OGL_LOADED__) {
document.body.style.background = "radial-gradient(circle at 80% 20%, #1e1b4b 0%, #050505 70%)";
} else {
const { Renderer, Program, Triangle, Mesh } = window.__OGL_LOADED__;
const rayContainer = document.getElementById("side-rays");
const settings = {
speed: 2.5,
rayColor1: "#EAB308",
rayColor2: "#96c8ff",
intensity: 2,
spread: 2,
origin: "top-right",
tilt: 0,
saturation: 1.5,
blend: 0.75,
falloff: 1.6,
opacity: 1
};
window.dispatchEvent(new Event('ogl-ready'));
</script>
function hexToRgb(hex) {
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!match) return [1, 1, 1];
return [
parseInt(match[1], 16) / 255,
parseInt(match[2], 16) / 255,
parseInt(match[3], 16) / 255
];
}
<script>
window.addEventListener('ogl-ready', () => {
if (!window.__OGL_LOADED__) {
document.body.style.background = "radial-gradient(circle at 80% 20%, #1e1b4b 0%, #050505 70%)";
} else {
const { Renderer, Program, Triangle, Mesh } = window.__OGL_LOADED__;
const rayContainer = document.getElementById("side-rays");
const settings = {
speed: 2.5,
rayColor1: "#EAB308",
rayColor2: "#96c8ff",
intensity: 2,
spread: 2,
origin: "top-right",
tilt: 0,
saturation: 1.5,
blend: 0.75,
falloff: 1.6,
opacity: 1
};
function originToFlip(origin) {
switch (origin) {
case "top-left": return [1, 0];
case "bottom-right": return [0, 1];
case "bottom-left": return [1, 1];
default: return [0, 0];
function hexToRgb(hex) {
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!match) return [1, 1, 1];
return [
parseInt(match[1], 16) / 255,
parseInt(match[2], 16) / 255,
parseInt(match[3], 16) / 255
];
}
}
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
alpha: true
});
const gl = renderer.gl;
gl.canvas.style.width = "100%";
gl.canvas.style.height = "100%";
rayContainer.appendChild(gl.canvas);
function originToFlip(origin) {
switch (origin) {
case "top-left": return [1, 0];
case "bottom-right": return [0, 1];
case "bottom-left": return [1, 1];
default: return [0, 0];
}
}
const vertexShader = `
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
alpha: true
});
const gl = renderer.gl;
gl.canvas.style.width = "100%";
gl.canvas.style.height = "100%";
rayContainer.appendChild(gl.canvas);
const vertexShader = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShader = `
const fragmentShader = `
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
@@ -383,77 +391,77 @@ void main() {
}
`;
const [flipX, flipY] = originToFlip(settings.origin);
const uniforms = {
iTime: { value: 0 },
iResolution: { value: [1, 1] },
iSpeed: { value: settings.speed },
iRayColor1: { value: hexToRgb(settings.rayColor1) },
iRayColor2: { value: hexToRgb(settings.rayColor2) },
iIntensity: { value: settings.intensity },
iSpread: { value: settings.spread },
iFlipX: { value: flipX },
iFlipY: { value: flipY },
iTilt: { value: settings.tilt },
iSaturation: { value: settings.saturation },
iBlend: { value: settings.blend },
iFalloff: { value: settings.falloff },
iOpacity: { value: settings.opacity }
};
const [flipX, flipY] = originToFlip(settings.origin);
const uniforms = {
iTime: { value: 0 },
iResolution: { value: [1, 1] },
iSpeed: { value: settings.speed },
iRayColor1: { value: hexToRgb(settings.rayColor1) },
iRayColor2: { value: hexToRgb(settings.rayColor2) },
iIntensity: { value: settings.intensity },
iSpread: { value: settings.spread },
iFlipX: { value: flipX },
iFlipY: { value: flipY },
iTilt: { value: settings.tilt },
iSaturation: { value: settings.saturation },
iBlend: { value: settings.blend },
iFalloff: { value: settings.falloff },
iOpacity: { value: settings.opacity }
};
const geometry = new Triangle(gl);
const program = new Program(gl, { vertex: vertexShader, fragment: fragmentShader, uniforms });
const mesh = new Mesh(gl, { geometry, program });
const geometry = new Triangle(gl);
const program = new Program(gl, { vertex: vertexShader, fragment: fragmentShader, uniforms });
const mesh = new Mesh(gl, { geometry, program });
function resize() {
const width = rayContainer.clientWidth;
const height = rayContainer.clientHeight;
renderer.dpr = Math.min(window.devicePixelRatio, 2);
renderer.setSize(width, height);
uniforms.iResolution.value = [width * renderer.dpr, height * renderer.dpr];
}
function resize() {
const width = rayContainer.clientWidth;
const height = rayContainer.clientHeight;
renderer.dpr = Math.min(window.devicePixelRatio, 2);
renderer.setSize(width, height);
uniforms.iResolution.value = [width * renderer.dpr, height * renderer.dpr];
}
window.addEventListener("resize", resize);
resize();
window.addEventListener("resize", resize);
resize();
function animate(time) {
uniforms.iTime.value = time * 0.001;
renderer.render({ scene: mesh });
function animate(time) {
uniforms.iTime.value = time * 0.001;
renderer.render({ scene: mesh });
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
function initLiquidButtons() {
const borderWidth = 0.16;
const brightness = 35;
const opacity = 0.55;
const blur = 4;
const displace = 1.2;
const borderRadius = 9999;
const distortionScale = -380;
const redOffset = -25;
const greenOffset = 0;
const blueOffset = 25;
const xChannel = "R";
const yChannel = "G";
const mixBlendMode = "screen";
function initLiquidButtons() {
const borderWidth = 0.16;
const brightness = 35;
const opacity = 0.55;
const blur = 4;
const displace = 1.2;
const borderRadius = 9999;
const distortionScale = -380;
const redOffset = -25;
const greenOffset = 0;
const blueOffset = 25;
const xChannel = "R";
const yChannel = "G";
const mixBlendMode = "screen";
document.querySelectorAll(".liquid-glass-btn").forEach((btn, index) => {
const filterId = `glass-filter-${index + 1}`;
btn.style.setProperty("--filter-id", `url(#${filterId})`);
document.querySelectorAll(".liquid-glass-btn").forEach((btn, index) => {
const filterId = `glass-filter-${index + 1}`;
btn.style.setProperty("--filter-id", `url(#${filterId})`);
const feImage = btn.querySelector(".fe-image");
const redChannel = btn.querySelector(".redchannel");
const greenChannel = btn.querySelector(".greenchannel");
const blueChannel = btn.querySelector(".bluechannel");
const gaussianBlur = btn.querySelector(".gaussianBlur");
const feImage = btn.querySelector(".fe-image");
const redChannel = btn.querySelector(".redchannel");
const greenChannel = btn.querySelector(".greenchannel");
const blueChannel = btn.querySelector(".bluechannel");
const gaussianBlur = btn.querySelector(".gaussianBlur");
function generateDisplacementMap() {
const rect = btn.getBoundingClientRect();
const actualWidth = rect.width || 140;
const actualHeight = rect.height || 48;
function generateDisplacementMap() {
const rect = btn.getBoundingClientRect();
const actualWidth = rect.width || 140;
const actualHeight = rect.height || 48;
const svgContent = `
const svgContent = `
<svg viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="red-grad-${index}" x1="100%" y1="0%" x2="0%" y2="0%">
@@ -470,36 +478,38 @@ void main() {
<rect width="${actualWidth}" height="${actualHeight}" rx="${borderRadius}" fill="url(#blue-grad-${index})" style="mix-blend-mode:${mixBlendMode}"/>
</svg>
`;
return "data:image/svg+xml," + encodeURIComponent(svgContent);
}
return "data:image/svg+xml," + encodeURIComponent(svgContent);
}
function update() {
feImage.setAttribute("href", generateDisplacementMap());
redChannel.setAttribute("scale", distortionScale + redOffset);
greenChannel.setAttribute("scale", distortionScale + greenOffset);
blueChannel.setAttribute("scale", distortionScale + blueOffset);
[redChannel, greenChannel, blueChannel].forEach((ch) => {
ch.setAttribute("xChannelSelector", xChannel);
ch.setAttribute("yChannelSelector", yChannel);
});
gaussianBlur.setAttribute("stdDeviation", displace);
}
function update() {
feImage.setAttribute("href", generateDisplacementMap());
redChannel.setAttribute("scale", distortionScale + redOffset);
greenChannel.setAttribute("scale", distortionScale + greenOffset);
blueChannel.setAttribute("scale", distortionScale + blueOffset);
[redChannel, greenChannel, blueChannel].forEach((ch) => {
ch.setAttribute("xChannelSelector", xChannel);
ch.setAttribute("yChannelSelector", yChannel);
});
gaussianBlur.setAttribute("stdDeviation", displace);
}
update();
btn.classList.remove("glass-surface--fallback");
btn.classList.add("glass-surface--svg");
update();
btn.classList.remove("glass-surface--fallback");
btn.classList.add("glass-surface--svg");
const observer = new ResizeObserver(() => setTimeout(update, 0));
observer.observe(btn);
});
const observer = new ResizeObserver(() => setTimeout(update, 0));
observer.observe(btn);
});
}
initLiquidButtons();
}
initLiquidButtons();
}
});
</script>
<script>
const statusEl = document.getElementById("status-message");
const debugBox = document.getElementById("debug-log-box");
const progressTrack = document.getElementById("progress-track");
const progressFill = document.getElementById("progress-fill");
const actionsEl = document.getElementById("update-actions");
@@ -524,9 +534,24 @@ void main() {
});
}
function showDebug(msg) {
if (!debugBox) return;
debugBox.style.display = "block";
debugBox.textContent = msg;
}
async function initTauriEvents() {
const eventApi = window.__TAURI__?.event || window.__TAURI_INTERNALS__;
if (!eventApi || typeof eventApi.listen !== 'function') {
let eventApi = null;
if (window.__TAURI__?.event && typeof window.__TAURI__.event.listen === 'function') {
eventApi = window.__TAURI__.event;
} else if (window.__TAURI_INTERNALS__ && typeof window.__TAURI_INTERNALS__.listen === 'function') {
eventApi = window.__TAURI_INTERNALS__;
} else if (window.__TAURI__ && typeof window.__TAURI__.listen === 'function') {
eventApi = window.__TAURI__;
}
if (!eventApi) {
setTimeout(initTauriEvents, 50);
return;
}
@@ -562,28 +587,20 @@ void main() {
const data = e.payload || e;
if (data && data.url) {
setStatus("Launching...");
window.location.href = data.url;
window.location.replace(data.url);
}
});
eventApi.listen("error", (e) => {
setStatus("Cleaning up background processes...", "#fca5a5");
try {
const core = window.__TAURI__?.core;
if (core) {
core.invoke("kill_orphans").finally(() => {
setTimeout(() => {
core.invoke("win_close").catch(() => {});
}, 500);
});
}
} catch (err) { }
const data = e.payload || e;
const msg = data && data.message ? data.message : JSON.stringify(data);
setStatus("Runtime Error", "#fca5a5");
showDebug(msg);
});
}
btnYes.addEventListener("click", () => {
if (actionsEl) actionsEl.style.display = "none";
if (actionsEl) actionsEl.style.display = "none";
setStatus("Starting update installation...");
fetch("http://127.0.0.1:4891/update/yes", { method: "POST" }).catch(() => { });
});