mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-13 21:57:21 -05:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d799da3d2a | |||
| f4aaf2fa7c | |||
| 8ce4edc251 | |||
| bef6fd2326 | |||
| 774f9a5e03 | |||
| 250e7972f2 | |||
| 5082d51ebc | |||
| ed66b894c1 | |||
| 11d6c346e8 | |||
| c986194e64 | |||
| 33e285953c | |||
| f46869f967 | |||
| b19e6b21c9 | |||
| c8ea2598b9 | |||
| e3257b8a3a | |||
| fbb5a4437d | |||
| dbaab9a00e | |||
| fc49bb93a7 | |||
| 9229f0c2dd | |||
| 843abf7df4 | |||
| 27fda0430b | |||
| 8b49d679bd | |||
| 62883b3180 | |||
| 3c6ae0125b | |||
| be0810ea6d | |||
| dff55060e7 | |||
| df767b5ffc | |||
| 1b70c6784a | |||
| e25e1c93f9 | |||
| 4c708ad97c | |||
| b81a4e044b | |||
| f6aa2daa01 | |||
| e03eb38adc | |||
| 2b690b47c1 | |||
| dffbdb92a5 | |||
| 45d6515f2e | |||
| a3e6a278fe | |||
| 7ffc20ee48 | |||
| ddf3abb314 | |||
| ec50c4e07f | |||
| 49b2a7622b | |||
| b8e474cb92 | |||
| 8b29c9e3c7 | |||
| 5dab61b359 | |||
| eab843aa16 | |||
| b757f741b7 | |||
| 5f4c47a07c | |||
| 32677c9fb2 | |||
| 7eab57cfd9 | |||
| c8fc7da64d | |||
| 88c1824bf7 | |||
| a50cb2b6b8 | |||
| 971eae4af1 | |||
| 1400d538f3 | |||
| b1d01d94da | |||
| 9d7e218ff6 | |||
| c19e1e2cf7 | |||
| 6b06bc8170 | |||
| 769c0dadcd | |||
| 414ef73e1d | |||
| 01c04111bd | |||
| a729bb81a3 | |||
| 547eb59709 | |||
| d774bed69b |
1
DOCUEMENTATION/CLEAR.bash
Normal file
1
DOCUEMENTATION/CLEAR.bash
Normal file
@@ -0,0 +1 @@
|
||||
bash -c 'pkill -9 -x node 2>/dev/null; pkill -9 -x "Vyla Home" 2>/dev/null; pkill -9 -x "vyla-home" 2>/dev/null; rm -rf "$HOME/Library/Application Support/com.vyla.home" "$HOME/Library/Application Support/vyla-home" "$HOME/Library/Application Support/Vyla Home" "$HOME/Library/Caches/com.vyla.home" "$HOME/Library/Caches/vyla-home" "$HOME/Library/Caches/Vyla Home" "$HOME/Library/WebKit/com.vyla.home" "$HOME/Library/WebKit/vyla-home" "$HOME/Library/WebKit/Vyla Home" "$HOME/Library/Logs/Vyla Home" "$HOME/Library/Preferences/com.vyla.home.plist" "$HOME/.vyla-home" "/Applications/Vyla Home.app" "/Applications/vyla-home.app" "$HOME/Applications/Vyla Home.app" "$HOME/Applications/vyla-home.app" "src-tauri/target" "dist-build" "temp-validation-extract" "dist" "frontend" "player" "auth-proxy" "stream-api" "logs" "*.log" "download-temp.zip" "update-payload.zip" "manifest.json" "validation-report.json" "config/encrypted-credentials.json" "config/credentials.json"; echo "Vyla Home cleanup complete."'
|
||||
3
DOCUEMENTATION/FIXES.md
Normal file
3
DOCUEMENTATION/FIXES.md
Normal file
@@ -0,0 +1,3 @@
|
||||
If someone get's a Webview2 error, they need to download the WebView2Loader.dll and drag it in bin
|
||||
|
||||
bin\WebView2Loader.dll
|
||||
38
README.md
38
README.md
@@ -1,30 +1,24 @@
|
||||
# Vyla Home Engine
|
||||
# Vyla Home
|
||||
|
||||
A high-performance, locally executed environment for the Vyla media ecosystem. Built on the Tauri v2 framework, Vyla Home acts as a secure native bridge between local microservices and a lightweight WebView2 rendering engine.
|
||||
A desktop application that provides a fast, secure, and private way to access the Vyla media ecosystem. Vyla Home runs everything locally on your computer, giving you quick access to streaming services without relying on external servers.
|
||||
|
||||
## Architecture & Security
|
||||
## What It Does
|
||||
|
||||
Vyla Home is designed around complete local isolation to maximize speed, privacy, and reliability:
|
||||
Vyla Home is a native desktop app that:
|
||||
|
||||
- **Zero External Reverse-Proxies**: All routing, API endpoints, and authentication middleware run strictly on local loopback interfaces (`127.0.0.1`), eliminating ISP throttling and data-center IP bans.
|
||||
- **Native Memory Management**: Background Node.js lifecycle states are managed directly via OS-level memory handles (bypassing slow shell wrappers) for instant startup and teardown.
|
||||
- **Cryptographic Obfuscation**: Client configurations and credentials are secured on-disk via hardware-derived AES-256-GCM encryption.
|
||||
- **Automated Integrity Patching**: Payloads are dynamically verified using SHA-256 checksums and patched in-memory via C-level buffer scans.
|
||||
- **Runs locally** - All services run on your computer for maximum privacy and speed
|
||||
- **Manages streaming services** - Handles authentication and streaming APIs automatically
|
||||
- **Updates automatically** - Keeps itself up to date with the latest features and fixes
|
||||
- **Works offline** - Core functionality doesn't require constant internet connectivity
|
||||
- **Keeps your data secure** - Your credentials and settings are encrypted and stored locally
|
||||
|
||||
## Build Requirements
|
||||
## How It Works
|
||||
|
||||
- [Rust](https://www.rust-lang.org/) (Latest Stable)
|
||||
- [Node.js](https://nodejs.org/) (v20+ LTS)
|
||||
- Tauri CLI (`npm install -g @tauri-apps/cli`)
|
||||
When you launch Vyla Home, it:
|
||||
|
||||
## Compilation
|
||||
1. Starts the necessary background services (media player server)
|
||||
2. Loads your saved settings and credentials (encrypted for security)
|
||||
3. Checks for updates and installs them if available
|
||||
4. Opens the user interface in a secure, embedded browser window
|
||||
|
||||
The build pipeline has been optimized with Fat LTO and aggressive panic aborts to generate the smallest possible binary footprint.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
|
||||
npm run build:payload
|
||||
|
||||
npm run build
|
||||
```
|
||||
Everything happens on your machine - your data never leaves your computer except when communicating directly with streaming services.
|
||||
@@ -1,12 +1,11 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const JavaScriptObfuscator = require('javascript-obfuscator');
|
||||
const AdmZip = require('adm-zip');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const BUILD_DIR = path.join(__dirname, 'dist-build');
|
||||
const REPOS = ['frontend', 'player', 'auth-proxy', 'stream-api'];
|
||||
const REPOS = ['frontend', 'player'];
|
||||
|
||||
const TEMP_EXTRACT_DIR = path.join(__dirname, 'temp-validation-extract');
|
||||
|
||||
@@ -15,24 +14,25 @@ const GITLAB_GROUP = 'vyla-entertainment';
|
||||
const REPO_CONFIG = {
|
||||
'frontend': { name: 'frontend', branch: 'main' },
|
||||
'player': { name: 'player', branch: 'main' },
|
||||
'auth-proxy': { name: 'auth-proxy', branch: 'main' },
|
||||
'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}`);
|
||||
}
|
||||
@@ -73,23 +73,6 @@ function cleanBuildDir() {
|
||||
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function obfuscateFile(filePath, isEsm = false) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const result = JavaScriptObfuscator.obfuscate(content, {
|
||||
compact: true,
|
||||
controlFlowFlattening: false,
|
||||
deadCodeInjection: false,
|
||||
debugProtection: false,
|
||||
numbersToExpressions: false,
|
||||
simplify: true,
|
||||
stringArray: true,
|
||||
stringArrayThreshold: 0.4,
|
||||
stringArrayEncoding: [],
|
||||
module: isEsm
|
||||
});
|
||||
fs.writeFileSync(filePath, result.getObfuscatedCode(), 'utf8');
|
||||
}
|
||||
|
||||
function encryptCredentialsFile() {
|
||||
const CryptoUtils = require('./crypto-utils');
|
||||
|
||||
@@ -170,13 +153,6 @@ function processFolder(src, dest) {
|
||||
if (pkg.type === 'module') isEsm = true;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
for (const filePath of jsFiles) {
|
||||
try {
|
||||
obfuscateFile(filePath, isEsm);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFileChecksum(filePath) {
|
||||
@@ -196,12 +172,10 @@ function validatePayload(zipPath) {
|
||||
zip.extractAllTo(TEMP_EXTRACT_DIR, true);
|
||||
|
||||
const requiredFiles = [
|
||||
'player/index.html',
|
||||
'auth-proxy/server.js',
|
||||
'stream-api/server.js',
|
||||
'player/server.js',
|
||||
'frontend/index.html'
|
||||
];
|
||||
|
||||
|
||||
const validationResults = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
@@ -357,9 +331,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
46
codemagic.yaml
Normal 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
|
||||
@@ -1,16 +1,5 @@
|
||||
{
|
||||
"player": {
|
||||
"TMDB_KEY": "YOUR_TMDB_KEY_HERE",
|
||||
"API_KEY": "YOUR_API_KEY_HERE"
|
||||
},
|
||||
"auth-proxy": {
|
||||
"VITE_API_KEY": "YOUR_VITE_API_KEY_HERE",
|
||||
"PROXY_VITE_API_KEY": "YOUR_PROXY_VITE_API_KEY_HERE"
|
||||
},
|
||||
"stream-api": {
|
||||
"TMDB_API_KEY": "YOUR_TMDB_API_KEY_HERE",
|
||||
"GA_MEASUREMENT_ID": "YOUR_GA_MEASUREMENT_ID_HERE",
|
||||
"GA_API_SECRET": "YOUR_GA_API_SECRET_HERE",
|
||||
"TOKEN_SECRET": "YOUR_TOKEN_SECRET_HERE"
|
||||
"TMDB_KEY": "YOUR_TMDB_KEY_HERE"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"player": {
|
||||
"PORT": "3001",
|
||||
"BASE_URL": "http://localhost:7860",
|
||||
"BYPASS_AUTH": "true"
|
||||
},
|
||||
"auth-proxy": {
|
||||
"PORT": "3000",
|
||||
"VITE_BASE_URL": "http://localhost:7860",
|
||||
"DEV_BYPASS_AUTH": "true"
|
||||
},
|
||||
"stream-api": {
|
||||
"PORT": "7860",
|
||||
"PROXY_URL": "https://txt.primebox.workers.dev",
|
||||
"PROXY_STREAMS": "true",
|
||||
"ENABLE_DEBUG_ROUTE": "true",
|
||||
"BYPASS_AUTH": "true",
|
||||
"TOKEN_SECRET": "vyla_default_secure_token_secret_local_dev_32chars"
|
||||
"PORT": "3000"
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,17 @@
|
||||
"frontend": [
|
||||
{
|
||||
"find": "https://player.vyla.cc",
|
||||
"replace": "http://localhost:3001"
|
||||
"replace": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"find": "http://player.vyla.cc",
|
||||
"replace": "http://localhost:3001"
|
||||
},
|
||||
{
|
||||
"find": "https://api.vyla.cc",
|
||||
"replace": "http://localhost:7860"
|
||||
},
|
||||
{
|
||||
"find": "https://auth.vyla.cc",
|
||||
"replace": "http://localhost:3000"
|
||||
}
|
||||
],
|
||||
"player": [
|
||||
{
|
||||
"find": "https://api.vyla.cc",
|
||||
"replace": "http://localhost:7860"
|
||||
},
|
||||
{
|
||||
"find": "https://auth.vyla.cc",
|
||||
"replace": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"find": "https://player.vyla.cc",
|
||||
"replace": "http://localhost:4890"
|
||||
}
|
||||
],
|
||||
"auth-proxy": [
|
||||
{
|
||||
"find": "https://player.vyla.cc",
|
||||
"replace": "http://localhost:3001"
|
||||
}
|
||||
],
|
||||
"stream-api": []
|
||||
]
|
||||
}
|
||||
@@ -4,9 +4,7 @@ const path = require('path');
|
||||
|
||||
const REPOS = {
|
||||
frontend: { branch: 'main' },
|
||||
player: { branch: 'main' },
|
||||
'stream-api': { branch: 'main' },
|
||||
'auth-proxy': { branch: 'main' }
|
||||
player: { branch: 'main' }
|
||||
};
|
||||
|
||||
const GITLAB_GROUP = 'vyla-entertainment';
|
||||
|
||||
236
main.js
236
main.js
@@ -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) { }
|
||||
@@ -25,8 +34,6 @@ debugLog("--- Node.js Process Started ---");
|
||||
let serviceManager;
|
||||
let expressApp;
|
||||
let expressServer;
|
||||
let playerExpressApp;
|
||||
let playerExpressServer;
|
||||
|
||||
debugLog(`AppData Dir: ${APPDATA_DIR}`);
|
||||
|
||||
@@ -61,6 +68,9 @@ window.open = function(url) {
|
||||
</script></head>`
|
||||
);
|
||||
res.set('Content-Type', 'text/html');
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
|
||||
res.set('Expires', '-1');
|
||||
res.set('Pragma', 'no-cache');
|
||||
res.send(injected);
|
||||
});
|
||||
};
|
||||
@@ -81,11 +91,20 @@ cmdApp.get('/__tauri_cmd/:cmd', (req, res) => {
|
||||
});
|
||||
cmdApp.listen(4892, '127.0.0.1', () => debugLog("[CMD-SERVER] Dedicated command server listening on 127.0.0.1:4892"));
|
||||
|
||||
process.stdin.resume();
|
||||
process.stdin.on('end', () => {
|
||||
debugLog('stdin closed - parent died. Self-terminating...');
|
||||
process.exit(0);
|
||||
});
|
||||
process.stdin.on('error', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
debugLog(`CRITICAL UNCAUGHT EXCEPTION: ${err.stack || err.message}`);
|
||||
if (err.code === 'EPIPE') {
|
||||
if (err.code === 'EPIPE' || err.code === 'EOF') {
|
||||
debugLog('Pipe to parent closed - parent process gone, exiting');
|
||||
process.exit(1);
|
||||
process.exit(0);
|
||||
}
|
||||
try { sendToTauri('error', { message: err.message }); } catch (e) { }
|
||||
});
|
||||
@@ -96,8 +115,21 @@ process.on('unhandledRejection', (reason) => {
|
||||
sendToTauri('error', { message: reason && reason.message ? reason.message : String(reason) });
|
||||
});
|
||||
|
||||
const envConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'config/env.json'), 'utf8'));
|
||||
const replacementsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'config/replacements.json'), 'utf8'));
|
||||
function getEnvConfig() {
|
||||
try {
|
||||
const appPath = path.join(APPDATA_DIR, 'config/env.json');
|
||||
if (fs.existsSync(appPath)) return JSON.parse(fs.readFileSync(appPath, 'utf8'));
|
||||
return JSON.parse(fs.readFileSync(path.join(__dirname, 'config/env.json'), 'utf8'));
|
||||
} catch (e) { return {}; }
|
||||
}
|
||||
|
||||
function getReplacementsConfig() {
|
||||
try {
|
||||
const appPath = path.join(APPDATA_DIR, 'config/replacements.json');
|
||||
if (fs.existsSync(appPath)) return JSON.parse(fs.readFileSync(appPath, 'utf8'));
|
||||
return JSON.parse(fs.readFileSync(path.join(__dirname, 'config/replacements.json'), 'utf8'));
|
||||
} catch (e) { return {}; }
|
||||
}
|
||||
|
||||
const updateClient = new UpdateClient(APPDATA_DIR);
|
||||
|
||||
@@ -106,10 +138,13 @@ async function start() {
|
||||
sendToTauri('status', { message: 'Loading core configuration files...' });
|
||||
|
||||
const patcher = new Patcher(APPDATA_DIR);
|
||||
serviceManager = new ServiceManager(APPDATA_DIR, envConfig);
|
||||
serviceManager = new ServiceManager(APPDATA_DIR, getEnvConfig());
|
||||
|
||||
const installBuild = async (release) => {
|
||||
const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip');
|
||||
if (fs.existsSync(tempZipPath)) {
|
||||
try { fs.unlinkSync(tempZipPath); } catch (e) { }
|
||||
}
|
||||
let attempts = 0;
|
||||
let downloadSuccess = false;
|
||||
|
||||
@@ -119,9 +154,13 @@ async function start() {
|
||||
debugLog(`Attempting to download update (Attempt ${attempts})...`);
|
||||
sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` });
|
||||
|
||||
let lastPercent = -1;
|
||||
await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => {
|
||||
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
||||
sendToTauri('download-progress', { downloaded, total, percent });
|
||||
if (percent !== lastPercent) {
|
||||
lastPercent = percent;
|
||||
sendToTauri('download-progress', { downloaded, total, percent });
|
||||
}
|
||||
});
|
||||
|
||||
debugLog('Download complete. Verifying checksum...');
|
||||
@@ -142,12 +181,14 @@ async function start() {
|
||||
|
||||
debugLog('Extraction complete. Applying patches...');
|
||||
sendToTauri('status', { message: 'Applying configuration patches...' });
|
||||
patcher.applyPatches(replacementsConfig);
|
||||
patcher.applyPatches(getReplacementsConfig());
|
||||
|
||||
sendToTauri('status', { message: 'Migrating security credentials...' });
|
||||
patcher.bridgeCredentials(serviceManager.credentialManager);
|
||||
patcher.saveCurrentVersion(release.version, release.sha256);
|
||||
|
||||
serviceManager.envConfig = getEnvConfig();
|
||||
|
||||
if (fs.existsSync(tempZipPath)) {
|
||||
fs.unlinkSync(tempZipPath);
|
||||
}
|
||||
@@ -169,10 +210,58 @@ 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}`);
|
||||
|
||||
function findLocalZip() {
|
||||
const searchRoots = [__dirname, process.cwd()];
|
||||
for (const root of searchRoots) {
|
||||
let cur = root;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const zp = path.join(cur, 'update-payload.zip');
|
||||
if (fs.existsSync(zp)) {
|
||||
const mp = path.join(cur, 'manifest.json');
|
||||
return { zipPath: zp, manifestPath: fs.existsSync(mp) ? mp : null };
|
||||
}
|
||||
const parent = path.dirname(cur);
|
||||
if (parent === cur) break;
|
||||
cur = parent;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const localFound = findLocalZip();
|
||||
let localZipPath = localFound ? localFound.zipPath : null;
|
||||
let localManifestPath = localFound ? localFound.manifestPath : null;
|
||||
let localZipManifest = null;
|
||||
|
||||
if (localManifestPath) {
|
||||
try {
|
||||
localZipManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8'));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (localZipPath && (isFirstRun || (localZipManifest && localZipManifest.sha256 !== localSha))) {
|
||||
debugLog(`Applying local development payload from: ${localZipPath}`);
|
||||
sendToTauri('status', { message: 'Extracting local development payload...' });
|
||||
patcher.purgeOldCode();
|
||||
patcher.extractZipPayload(localZipPath);
|
||||
patcher.applyPatches(getReplacementsConfig());
|
||||
patcher.bridgeCredentials(serviceManager.credentialManager);
|
||||
|
||||
const ver = localZipManifest ? localZipManifest.version : '1.1.32';
|
||||
const sha = localZipManifest ? localZipManifest.sha256 : null;
|
||||
patcher.saveCurrentVersion(ver, sha);
|
||||
serviceManager.envConfig = getEnvConfig();
|
||||
|
||||
isFirstRun = false;
|
||||
localVersion = ver;
|
||||
localSha = sha;
|
||||
debugLog("Local payload applied successfully.");
|
||||
}
|
||||
|
||||
sendToTauri('status', { message: 'Connecting to update servers...' });
|
||||
let release = null;
|
||||
@@ -180,18 +269,43 @@ async function start() {
|
||||
debugLog("Fetching updates from updateClient...");
|
||||
release = await updateClient.checkForUpdates();
|
||||
debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`);
|
||||
|
||||
let appPkgVer = '0.0.0';
|
||||
try {
|
||||
appPkgVer = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version || '0.0.0';
|
||||
} catch (e) { }
|
||||
|
||||
if (release && release.version) {
|
||||
const rVer = release.version.replace('v', '').split('.').map(Number);
|
||||
const aVer = appPkgVer.split('.').map(Number);
|
||||
let isOlder = false;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((rVer[i] || 0) < (aVer[i] || 0)) { isOlder = true; break; }
|
||||
if ((rVer[i] || 0) > (aVer[i] || 0)) { break; }
|
||||
}
|
||||
if (isOlder) {
|
||||
debugLog(`Remote version ${release.version} is older than local package ${appPkgVer}. Ignoring remote release.`);
|
||||
release = null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(`Update check failed (offline?): ${e.message}`);
|
||||
sendToTauri('status', { message: `Update server check skipped (${e.message})` });
|
||||
}
|
||||
|
||||
const normalizeVer = (v) => (v || '').toString().replace(/^v/, '').trim();
|
||||
|
||||
if (release) {
|
||||
const hasUpdate = release.version !== localVersion || (release.sha256 && release.sha256 !== localSha);
|
||||
const isNewerSha = release.sha256 && localSha && release.sha256 !== localSha;
|
||||
const isDifferentVer = normalizeVer(release.version) !== normalizeVer(localVersion);
|
||||
const hasUpdate = (isDifferentVer || isNewerSha) && !localZipPath;
|
||||
|
||||
if (isFirstRun) {
|
||||
debugLog("First run. Installing remote build...");
|
||||
debugLog("First run with no local payload. Installing remote build...");
|
||||
sendToTauri('status', { message: 'First run detected. Preparing initial installation...' });
|
||||
await installBuild(release);
|
||||
} else if (hasUpdate) {
|
||||
|
||||
debugLog("Update available. Prompting user...");
|
||||
sendToTauri('status', { message: `New update available (${release.version}). Awaiting confirmation...` });
|
||||
|
||||
@@ -241,9 +355,17 @@ async function start() {
|
||||
sendToTauri('status', { message: 'Extracting local baseline payload...' });
|
||||
patcher.purgeOldCode();
|
||||
patcher.extractZipPayload(localZip);
|
||||
patcher.applyPatches(replacementsConfig);
|
||||
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 {
|
||||
debugLog("No local payload found!");
|
||||
@@ -252,43 +374,21 @@ async function start() {
|
||||
|
||||
sendToTauri('status', { message: 'Validating critical components...' });
|
||||
if (!patcher.verifyExtraction()) {
|
||||
throw new Error('No application files found. Check your internet connection and try again.');
|
||||
throw new Error('No application files found. Ensure you run "npm run build:payload" to generate the local development payload.');
|
||||
}
|
||||
|
||||
sendToTauri('status', { message: 'Loading encrypted credentials...' });
|
||||
serviceManager.credentialManager.loadCredentials();
|
||||
|
||||
sendToTauri('status', { message: 'Starting background services (auth-proxy, stream-api)...' });
|
||||
sendToTauri('status', { message: 'Starting background services (player)...' });
|
||||
await serviceManager.startServices([
|
||||
{ name: 'auth-proxy', relativeDir: 'auth-proxy' },
|
||||
{ name: 'stream-api', relativeDir: 'stream-api' },
|
||||
{ name: 'player', relativeDir: 'player', startScript: 'server.js' }
|
||||
]);
|
||||
debugLog("Backend services spawn requested.");
|
||||
|
||||
sendToTauri('status', { message: 'Binding player web server on port 3001...' });
|
||||
const playerDir = path.join(APPDATA_DIR, 'player');
|
||||
playerExpressApp = express();
|
||||
playerExpressApp.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', '*');
|
||||
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
||||
next();
|
||||
});
|
||||
playerExpressApp.get('/__tauri_cmd/:cmd', (req, res) => {
|
||||
debugLog(`[PLAYER-SERVER:3001] Command received: ${req.params.cmd} (window: ${req.query.window || 'main'})`);
|
||||
sendToTauri('tauri-cmd', { cmd: req.params.cmd, url: req.query.url, window: req.query.window });
|
||||
res.sendStatus(200);
|
||||
});
|
||||
playerExpressApp.use(sameTabNavMiddleware(playerDir));
|
||||
playerExpressApp.use(express.static(playerDir, { maxAge: '1d', etag: true }));
|
||||
playerExpressApp.get('*', (req, res) => { res.sendFile(path.join(playerDir, 'index.html')); });
|
||||
playerExpressServer = playerExpressApp.listen(3001, '127.0.0.1');
|
||||
debugLog("Player Express running on 3001");
|
||||
|
||||
sendToTauri('status', { message: 'Waiting for microservices to bind ports (3000, 3001, 7860)...' });
|
||||
await serviceManager.waitForPorts([3000, 3001, 7860]);
|
||||
debugLog("Ports 3000, 3001, 7860 are ready.");
|
||||
sendToTauri('status', { message: 'Waiting for microservices to bind port 3000...' });
|
||||
await serviceManager.waitForPorts([3000]);
|
||||
debugLog("Port 3000 is ready.");
|
||||
|
||||
sendToTauri('status', { message: 'Binding UI server on port 4890...' });
|
||||
const frontendDir = path.join(APPDATA_DIR, 'frontend');
|
||||
@@ -305,17 +405,29 @@ async function start() {
|
||||
sendToTauri('tauri-cmd', { cmd: req.params.cmd, url: req.query.url, window: req.query.window });
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
expressApp.use(sameTabNavMiddleware(frontendDir));
|
||||
expressApp.use(express.static(frontendDir, { maxAge: '1d', etag: true }));
|
||||
expressApp.get('*', (req, res) => { res.sendFile(path.join(frontendDir, 'index.html')); });
|
||||
expressApp.use(express.static(frontendDir, {
|
||||
etag: false,
|
||||
setHeaders: (res) => {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
|
||||
}
|
||||
}));
|
||||
expressApp.use(express.static(path.join(frontendDir, 'src'), {
|
||||
etag: false,
|
||||
setHeaders: (res) => {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
|
||||
}
|
||||
}));
|
||||
expressApp.get('*', (req, res) => {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
|
||||
res.sendFile(path.join(frontendDir, 'index.html'));
|
||||
});
|
||||
|
||||
const FRONTEND_PORT = 4890;
|
||||
expressApp.use('/build', express.static(path.join(__dirname, 'build')));
|
||||
if (playerExpressApp) {
|
||||
playerExpressApp.use('/build', express.static(path.join(__dirname, 'build')));
|
||||
}
|
||||
|
||||
expressServer = expressApp.listen(FRONTEND_PORT, '127.0.0.1', () => {
|
||||
expressServer = expressApp.listen(FRONTEND_PORT, () => {
|
||||
sendToTauri('status', { message: 'All backend services running and healthy.' });
|
||||
debugLog("Frontend Express running on 4890. Sending READY to Tauri.");
|
||||
sendToTauri('ready', { url: `http://localhost:${FRONTEND_PORT}` });
|
||||
@@ -331,17 +443,23 @@ process.on('exit', () => {
|
||||
if (serviceManager) {
|
||||
for (const name of Object.keys(serviceManager.processes)) {
|
||||
const proc = serviceManager.processes[name];
|
||||
if (proc && proc.child) {
|
||||
try { proc.child.kill('SIGKILL'); } catch (e) { }
|
||||
if (proc && proc.child && proc.child.pid) {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
require('child_process').spawnSync('taskkill', ['/F', '/T', '/PID', proc.child.pid.toString()], { stdio: 'ignore', windowsHide: true });
|
||||
} else {
|
||||
process.kill(-proc.child.pid, 'SIGKILL');
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
if (expressServer) expressServer.close();
|
||||
if (playerExpressServer) playerExpressServer.close();
|
||||
if (serviceManager) await serviceManager.stopAll();
|
||||
process.on('SIGINT', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vyla-home",
|
||||
"version": "1.1.7",
|
||||
"version": "1.1.33",
|
||||
"description": "Vyla Home Desktop App Launcher",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -20,8 +20,7 @@
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"javascript-obfuscator": "^4.1.0"
|
||||
"@tauri-apps/cli": "^2.0.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.vyla.home",
|
||||
@@ -55,4 +54,4 @@
|
||||
"installerHeaderIcon": "build/icon.ico"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
patcher.js
63
patcher.js
@@ -2,7 +2,6 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const AdmZip = require('adm-zip');
|
||||
const JavaScriptObfuscator = require('javascript-obfuscator');
|
||||
const CryptoUtils = require('./crypto-utils');
|
||||
|
||||
class Patcher {
|
||||
@@ -64,17 +63,24 @@ class Patcher {
|
||||
const fullPath = path.join(this.appDataPath, dir);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
const trashPath = path.join(this.appDataPath, `.trash_${dir}_${Date.now()}`);
|
||||
fs.renameSync(fullPath, trashPath);
|
||||
fs.rmSync(trashPath, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
try {
|
||||
const trashPath = fullPath + '.trash.' + Date.now();
|
||||
fs.renameSync(fullPath, trashPath);
|
||||
} catch (renameErr) {
|
||||
console.warn(`Could not remove or rename ${fullPath}. It will be overwritten.`);
|
||||
}
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
} catch (err2) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
const files = fs.readdirSync(this.appDataPath);
|
||||
for (const file of files) {
|
||||
if (file.startsWith('.trash_')) {
|
||||
fs.rmSync(path.join(this.appDataPath, file), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
extractZipPayload(zipPath) {
|
||||
@@ -93,9 +99,7 @@ class Patcher {
|
||||
|
||||
verifyExtraction() {
|
||||
const requiredFiles = [
|
||||
'player/index.html',
|
||||
'auth-proxy/server.js',
|
||||
'stream-api/server.js',
|
||||
'player/server.js',
|
||||
'frontend/index.html'
|
||||
];
|
||||
|
||||
@@ -141,43 +145,6 @@ class Patcher {
|
||||
}
|
||||
}
|
||||
|
||||
if (ext === '.js') {
|
||||
try {
|
||||
content = JavaScriptObfuscator.obfuscate(content, {
|
||||
compact: true,
|
||||
controlFlowFlattening: true,
|
||||
controlFlowFlatteningThreshold: 1,
|
||||
deadCodeInjection: true,
|
||||
deadCodeInjectionThreshold: 0.4,
|
||||
debugProtection: false,
|
||||
disableConsoleOutput: false,
|
||||
identifierNamesGenerator: 'hexadecimal',
|
||||
log: false,
|
||||
numbersToExpressions: true,
|
||||
renameGlobals: false,
|
||||
selfDefending: true,
|
||||
simplify: true,
|
||||
splitStrings: true,
|
||||
splitStringsChunkLength: 5,
|
||||
stringArray: true,
|
||||
stringArrayCallsTransform: true,
|
||||
stringArrayCallsTransformThreshold: 1,
|
||||
stringArrayEncoding: ['rc4'],
|
||||
stringArrayIndexShift: true,
|
||||
stringArrayRotate: true,
|
||||
stringArrayShuffle: true,
|
||||
stringArrayWrappersCount: 5,
|
||||
stringArrayWrappersChainedCalls: true,
|
||||
stringArrayWrappersParametersMaxCount: 5,
|
||||
stringArrayWrappersType: 'function',
|
||||
stringArrayThreshold: 1,
|
||||
transformObjectKeys: true,
|
||||
unicodeEscapeSequence: false
|
||||
}).getObfuscatedCode();
|
||||
modified = true;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
}
|
||||
@@ -186,7 +153,7 @@ class Patcher {
|
||||
}
|
||||
|
||||
applyPatches(replacements) {
|
||||
const targetDirs = ['frontend', 'player', 'auth-proxy', 'stream-api'];
|
||||
const targetDirs = ['frontend', 'player'];
|
||||
for (const dirName of targetDirs) {
|
||||
const dirPath = path.join(this.appDataPath, dirName);
|
||||
if (!fs.existsSync(dirPath)) continue;
|
||||
|
||||
169
release.js
169
release.js
@@ -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 });
|
||||
|
||||
@@ -40,18 +40,16 @@ class ServiceManager {
|
||||
}
|
||||
|
||||
let tokenSecret = process.env.TOKEN_SECRET ||
|
||||
process.env.STREAM_API_TOKEN_SECRET ||
|
||||
this.credentialManager.getCredential('stream-api', 'TOKEN_SECRET');
|
||||
this.credentialManager.getCredential('player', 'TOKEN_SECRET');
|
||||
|
||||
if (!tokenSecret || tokenSecret.length < 32) {
|
||||
tokenSecret = require('crypto').randomBytes(32).toString('hex');
|
||||
this.credentialManager.setCredential('stream-api', 'TOKEN_SECRET', tokenSecret);
|
||||
this.credentialManager.setCredential('player', 'TOKEN_SECRET', tokenSecret);
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
TOKEN_SECRET: tokenSecret,
|
||||
STREAM_API_TOKEN_SECRET: tokenSecret,
|
||||
...(this.envConfig[name] || {}),
|
||||
...(this.credentialManager.getServiceCredentials(name) || {})
|
||||
};
|
||||
|
||||
69
src-tauri/Cargo.lock
generated
69
src-tauri/Cargo.lock
generated
@@ -270,7 +270,7 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -762,7 +762,7 @@ dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2079,7 +2079,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"keyboard-types",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
@@ -2157,6 +2157,22 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
|
||||
dependencies = [
|
||||
"objc-sys",
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@@ -2175,7 +2191,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -2187,7 +2203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2197,7 +2213,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2209,7 +2225,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2220,7 +2236,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
@@ -2231,7 +2247,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2241,7 +2257,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2252,7 +2268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
@@ -2280,7 +2296,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -2291,7 +2307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -2302,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -2315,7 +2331,7 @@ checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
@@ -2334,7 +2350,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2346,7 +2362,7 @@ checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
@@ -3211,7 +3227,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"js-sys",
|
||||
"ndk",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
@@ -3385,7 +3401,7 @@ dependencies = [
|
||||
"log",
|
||||
"ndk",
|
||||
"ndk-sys",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
@@ -3441,7 +3457,7 @@ dependencies = [
|
||||
"log",
|
||||
"mime",
|
||||
"muda",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
@@ -3612,7 +3628,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"raw-window-handle",
|
||||
@@ -3636,7 +3652,7 @@ dependencies = [
|
||||
"http",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -4051,7 +4067,7 @@ dependencies = [
|
||||
"dirs",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
@@ -4222,9 +4238,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vyla-home"
|
||||
version = "1.1.7"
|
||||
version = "1.1.33"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"objc2 0.5.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -4475,7 +4492,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
@@ -4930,7 +4947,7 @@ dependencies = [
|
||||
"jni",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vyla-home"
|
||||
version = "1.1.7"
|
||||
version = "1.1.33"
|
||||
description = "Vyla Home Desktop App Launcher"
|
||||
edition = "2021"
|
||||
|
||||
@@ -16,6 +16,9 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
base64 = "0.22"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.5"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
|
||||
13
src-tauri/Info.plist
Normal file
13
src-tauri/Info.plist
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,5 +1,32 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let node_path = std::path::Path::new("../bin/node");
|
||||
if node_path.exists() {
|
||||
let _ = std::fs::set_permissions(
|
||||
node_path,
|
||||
std::fs::Permissions::from_mode(0o755),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
@@ -11,12 +11,17 @@ fn debug_log(msg: &str) {
|
||||
.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")
|
||||
} else {
|
||||
std::path::PathBuf::from("logs")
|
||||
std::env::temp_dir().join("vyla-home-logs")
|
||||
};
|
||||
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
@@ -50,17 +55,38 @@ fn clean_path(path: &std::path::Path) -> String {
|
||||
}
|
||||
|
||||
fn kill_child_process(child: &mut Child) {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let _ = Command::new("taskkill")
|
||||
.args(&["/F", "/T", "/PID", &child.id().to_string()])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -73,9 +99,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() {
|
||||
@@ -87,9 +117,14 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +153,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
|
||||
}
|
||||
|
||||
@@ -126,8 +177,8 @@ 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 {
|
||||
if candidate.exists() {
|
||||
let root_dir = candidate.parent().unwrap();
|
||||
@@ -196,17 +247,6 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
#vyla-nav-filter-svg {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
pointer-events: none !important;
|
||||
opacity: 0 !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
#vyla-floating-nav {
|
||||
position: fixed !important;
|
||||
top: 12px !important;
|
||||
@@ -218,7 +258,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
background: rgba(255, 255, 255, 0.04) !important;
|
||||
backdrop-filter: url(#vyla-glass-filter) blur(16px) saturate(1.8) contrast(1.1) !important;
|
||||
backdrop-filter: blur(16px) saturate(1.8) contrast(1.1) !important;
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.8) contrast(1.1) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45) !important;
|
||||
transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1), padding 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important;
|
||||
@@ -228,12 +268,12 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
pointer-events: auto !important;
|
||||
-webkit-app-region: no-drag !important;
|
||||
}
|
||||
|
||||
|
||||
#vyla-floating-nav:hover {
|
||||
width: 82px !important;
|
||||
padding-right: 6px !important;
|
||||
}
|
||||
|
||||
|
||||
.vyla-nav-logo-wrap {
|
||||
width: 42px !important;
|
||||
min-width: 42px !important;
|
||||
@@ -248,7 +288,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
pointer-events: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.vyla-nav-logo-wrap img {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
@@ -258,7 +298,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
pointer-events: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
.vyla-nav-links {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
@@ -271,13 +311,13 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
z-index: 2 !important;
|
||||
margin-left: 2px !important;
|
||||
}
|
||||
|
||||
|
||||
#vyla-floating-nav:hover .vyla-nav-links {
|
||||
opacity: 1 !important;
|
||||
transform: translateX(0) !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
|
||||
.vyla-nav-btn {
|
||||
width: 32px !important;
|
||||
min-width: 32px !important;
|
||||
@@ -296,11 +336,8 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
opacity: 0.85 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.vyla-nav-btn:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
||||
.vyla-nav-btn:hover { opacity: 1 !important; }
|
||||
.vyla-nav-btn svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
@@ -308,7 +345,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
pointer-events: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
|
||||
#tauri-custom-titlebar {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
@@ -326,7 +363,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.tauri-drag-region {
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
@@ -334,7 +371,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
cursor: default !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
|
||||
.tauri-window-controls {
|
||||
display: flex !important;
|
||||
height: 32px !important;
|
||||
@@ -343,7 +380,7 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
pointer-events: auto !important;
|
||||
z-index: 2147483648 !important;
|
||||
}
|
||||
|
||||
|
||||
button.tauri-btn {
|
||||
width: 46px !important;
|
||||
min-width: 46px !important;
|
||||
@@ -358,33 +395,13 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
cursor: pointer !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
||||
button.tauri-btn:hover { background: rgba(255, 255, 255, 0.2) !important; }
|
||||
button#tauri-btn-close:hover { background: #e81123 !important; color: #ffffff !important; }
|
||||
button.tauri-btn svg { width: 10px !important; height: 10px !important; fill: currentColor !important; pointer-events: none !important; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const filterSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
filterSvg.id = 'vyla-nav-filter-svg';
|
||||
filterSvg.innerHTML = `
|
||||
<defs>
|
||||
<filter id="vyla-glass-filter" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%">
|
||||
<feImage id="vyla-nav-fe-img" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
|
||||
<feDisplacementMap id="vyla-nav-red" in="SourceGraphic" in2="map" result="dispRed" />
|
||||
<feColorMatrix in="dispRed" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" />
|
||||
<feDisplacementMap id="vyla-nav-green" in="SourceGraphic" in2="map" result="dispGreen" />
|
||||
<feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" />
|
||||
<feDisplacementMap id="vyla-nav-blue" in="SourceGraphic" in2="map" result="dispBlue" />
|
||||
<feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue" />
|
||||
<feBlend in="red" in2="green" mode="screen" result="rg" />
|
||||
<feBlend in="rg" in2="blue" mode="screen" result="output" />
|
||||
<feGaussianBlur in="output" stdDeviation="0.8" />
|
||||
</filter>
|
||||
</defs>
|
||||
`;
|
||||
document.body.appendChild(filterSvg);
|
||||
|
||||
const nav = document.createElement('div');
|
||||
nav.id = 'vyla-floating-nav';
|
||||
nav.innerHTML = `
|
||||
@@ -417,54 +434,6 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
};
|
||||
}
|
||||
|
||||
const feImage = document.getElementById('vyla-nav-fe-img');
|
||||
const redChannel = document.getElementById('vyla-nav-red');
|
||||
const greenChannel = document.getElementById('vyla-nav-green');
|
||||
const blueChannel = document.getElementById('vyla-nav-blue');
|
||||
|
||||
function generateNavMap() {
|
||||
const rect = nav.getBoundingClientRect();
|
||||
const w = Math.round(rect.width) || 42;
|
||||
const h = Math.round(rect.height) || 42;
|
||||
const edgeSize = Math.min(w, h) * 0.08;
|
||||
const svgContent = `
|
||||
<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="vyla-nav-red-g" x1="100%" y1="0%" x2="0%" y2="0%">
|
||||
<stop offset="0%" stop-color="#000000" stop-opacity="0"/>
|
||||
<stop offset="100%" stop-color="red"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="vyla-nav-blue-g" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#000000" stop-opacity="0"/>
|
||||
<stop offset="100%" stop-color="blue"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${w}" height="${h}" fill="black"/>
|
||||
<rect width="${w}" height="${h}" rx="9999" fill="url(#vyla-nav-red-g)"/>
|
||||
<rect width="${w}" height="${h}" rx="9999" fill="url(#vyla-nav-blue-g)" style="mix-blend-mode: screen;"/>
|
||||
<rect x="${edgeSize}" y="${edgeSize}" width="${w - edgeSize * 2}" height="${h - edgeSize * 2}" rx="9999" fill="rgba(255,255,255,0.4)" style="filter: blur(4px);"/>
|
||||
</svg>
|
||||
`;
|
||||
return "data:image/svg+xml," + encodeURIComponent(svgContent);
|
||||
}
|
||||
|
||||
function updateNavGlass() {
|
||||
if (!feImage) return;
|
||||
feImage.setAttribute("href", generateNavMap());
|
||||
const distortionScale = -180;
|
||||
redChannel.setAttribute("scale", distortionScale - 10);
|
||||
greenChannel.setAttribute("scale", distortionScale);
|
||||
blueChannel.setAttribute("scale", distortionScale + 10);
|
||||
[redChannel, greenChannel, blueChannel].forEach((ch) => {
|
||||
ch.setAttribute("xChannelSelector", "R");
|
||||
ch.setAttribute("yChannelSelector", "G");
|
||||
});
|
||||
}
|
||||
|
||||
updateNavGlass();
|
||||
const obs = new ResizeObserver(() => setTimeout(updateNavGlass, 0));
|
||||
obs.observe(nav);
|
||||
|
||||
const tb = document.createElement('div');
|
||||
tb.id = 'tauri-custom-titlebar';
|
||||
tb.innerHTML = `
|
||||
@@ -495,11 +464,12 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', injectLiquidNavBar);
|
||||
} else {
|
||||
injectLiquidNavBar();
|
||||
}
|
||||
|
||||
const navObserver = new MutationObserver(() => {
|
||||
if (!document.getElementById('vyla-floating-nav')) {
|
||||
injectLiquidNavBar();
|
||||
@@ -543,9 +513,9 @@ fn open_new_window(app: tauri::AppHandle, url: String) -> Result<(), String> {
|
||||
let final_url = if url.starts_with("http") || url.starts_with("tauri://") {
|
||||
url.clone()
|
||||
} else if url.starts_with('/') {
|
||||
format!("http://localhost:4890{}", url)
|
||||
format!("http://127.0.0.1:4890{}", url)
|
||||
} else {
|
||||
format!("http://localhost:4890/{}", url)
|
||||
format!("http://127.0.0.1:4890/{}", url)
|
||||
};
|
||||
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
@@ -561,6 +531,30 @@ fn open_new_window(app: tauri::AppHandle, url: String) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn kill_orphans() -> Result<(), String> {
|
||||
debug_log("[KILL] User requested orphan process kill");
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(&["/F", "/T", "/IM", "node.exe"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = std::process::Command::new("pkill")
|
||||
.args(&["-9", "-f", "node"])
|
||||
.output();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> {
|
||||
use std::io::Read;
|
||||
@@ -594,6 +588,123 @@ fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> {
|
||||
Err("icon_white.png not found".to_string())
|
||||
}
|
||||
|
||||
fn handle_tauri_cmd(w: &tauri::WebviewWindow, msg: &BackendMessage) {
|
||||
let window_label = msg
|
||||
.data
|
||||
.get("window")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("main");
|
||||
|
||||
debug_log(&format!("[MAIN-DEBUG] Finding window by label: {}", window_label));
|
||||
|
||||
let cmd_window = match w.app_handle().get_webview_window(window_label) {
|
||||
Some(win) => win,
|
||||
None => {
|
||||
debug_log(&format!("[TAURI-CMD] Window '{}' NOT FOUND!", window_label));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let cmd = match msg.data.get("cmd").and_then(|c| c.as_str()) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
debug_log(&format!("[TAURI-CMD] Executing action: {}", cmd));
|
||||
|
||||
match cmd {
|
||||
"win_minimize" => {
|
||||
let res = cmd_window.minimize();
|
||||
debug_log(&format!("[TAURI-CMD] Minimize result: {:?}", res));
|
||||
}
|
||||
"win_maximize" => {
|
||||
let is_max = cmd_window.is_maximized().unwrap_or(false);
|
||||
debug_log(&format!("[TAURI-CMD] Currently maximized: {}", is_max));
|
||||
let res = if is_max {
|
||||
cmd_window.unmaximize()
|
||||
} else {
|
||||
cmd_window.maximize()
|
||||
};
|
||||
debug_log(&format!("[TAURI-CMD] Maximize/Unmaximize result: {:?}", res));
|
||||
}
|
||||
"win_close" => {
|
||||
let res = cmd_window.close();
|
||||
debug_log(&format!("[TAURI-CMD] Close result: {:?}", res));
|
||||
}
|
||||
"win_drag" => {
|
||||
let res = cmd_window.start_dragging();
|
||||
debug_log(&format!("[TAURI-CMD] Drag result: {:?}", res));
|
||||
}
|
||||
"open_new_window" => {
|
||||
debug_log("[TAURI-CMD-DEBUG] open_new_window command received");
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] Full message data: {}",
|
||||
serde_json::to_string(&msg.data).unwrap_or_default()
|
||||
));
|
||||
|
||||
let url = match msg.data.get("url").and_then(|u| u.as_str()) {
|
||||
Some(u) => u.to_string(),
|
||||
None => {
|
||||
debug_log("[TAURI-CMD-DEBUG] ERROR: No URL found in message data");
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] Available keys: {:?}",
|
||||
msg.data.as_object().map(|o| o.keys().collect::<Vec<_>>())
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug_log(&format!("[TAURI-CMD-DEBUG] URL extracted: {}", url));
|
||||
debug_log(&format!("[TAURI-CMD-DEBUG] URL length: {}", url.len()));
|
||||
debug_log("[TAURI-CMD-DEBUG] Attempting to dispatch to main thread");
|
||||
|
||||
let app_handle = w.app_handle().clone();
|
||||
let closure_handle = app_handle.clone();
|
||||
|
||||
match app_handle.run_on_main_thread(move || {
|
||||
debug_log("[TAURI-CMD-DEBUG] Now on main thread, calling open_new_window");
|
||||
match open_new_window(closure_handle, url) {
|
||||
Ok(_) => debug_log("[TAURI-CMD-DEBUG] open_new_window succeeded"),
|
||||
Err(e) => debug_log(&format!("[TAURI-CMD-DEBUG] open_new_window error: {}", e)),
|
||||
}
|
||||
}) {
|
||||
Ok(_) => debug_log("[TAURI-CMD-DEBUG] Main thread dispatch succeeded"),
|
||||
Err(e) => debug_log(&format!("[TAURI-CMD-DEBUG] Main thread dispatch failed: {}", e)),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_ready(w: &tauri::WebviewWindow, msg: &BackendMessage) {
|
||||
let url_str = match msg.data.get("url").and_then(|u| u.as_str()) {
|
||||
Some(u) => u,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let url_parsed = match url_str.parse::<tauri::Url>() {
|
||||
Ok(u) => u,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use objc2::msg_send;
|
||||
use objc2::runtime::AnyObject;
|
||||
debug_log("Attempting with_webview on ready event");
|
||||
let result = w.with_webview(|webview| unsafe {
|
||||
debug_log("Inside with_webview closure");
|
||||
let wk_webview = webview.inner() as *mut AnyObject;
|
||||
let config: *mut AnyObject = msg_send![wk_webview, configuration];
|
||||
let _: () = msg_send![config, setUpgradeKnownHostsToHTTPS: false];
|
||||
debug_log("setUpgradeKnownHostsToHTTPS set to false");
|
||||
});
|
||||
debug_log(&format!("with_webview result: {:?}", result));
|
||||
}
|
||||
|
||||
let _ = w.navigate(url_parsed);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
debug_log("=== Vyla Home Executable Started ===");
|
||||
|
||||
@@ -607,44 +718,72 @@ fn main() {
|
||||
win_close,
|
||||
win_drag,
|
||||
open_new_window,
|
||||
get_logo_base64
|
||||
get_logo_base64,
|
||||
kill_orphans
|
||||
])
|
||||
.manage(NodeServices(Mutex::new(None)))
|
||||
.setup(|app| {
|
||||
.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 _ = std::fs::create_dir_all(&vyla_files);
|
||||
|
||||
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
|
||||
.title("Vyla Home")
|
||||
.inner_size(1280.0, 800.0)
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.resizable(true)
|
||||
.devtools(cfg!(debug_assertions))
|
||||
.devtools(true)
|
||||
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
|
||||
.initialization_script(LINK_HANDLER_SCRIPT)
|
||||
.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())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
@@ -661,12 +800,23 @@ fn main() {
|
||||
"Node child process spawned successfully, PID: {}",
|
||||
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) }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -678,144 +828,22 @@ fn main() {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().flatten() {
|
||||
debug_log(&format!("[NODE STDOUT] {}", line));
|
||||
if let Ok(msg) = serde_json::from_str::<BackendMessage>(&line) {
|
||||
debug_log(&format!("[MAIN-DEBUG] Received event: {}", msg.event));
|
||||
debug_log(&format!(
|
||||
"[MAIN-DEBUG] Event data: {}",
|
||||
serde_json::to_string(&msg.data).unwrap_or_default()
|
||||
));
|
||||
if msg.event == "tauri-cmd" {
|
||||
let window_label = msg
|
||||
.data
|
||||
.get("window")
|
||||
.and_then(|val| val.as_str())
|
||||
.unwrap_or("main");
|
||||
debug_log(&format!(
|
||||
"[MAIN-DEBUG] Finding window by label: {}",
|
||||
window_label
|
||||
));
|
||||
|
||||
if let Some(cmd_window) =
|
||||
w.app_handle().get_webview_window(window_label)
|
||||
{
|
||||
if let Some(cmd) =
|
||||
msg.data.get("cmd").and_then(|c| c.as_str())
|
||||
{
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Executing action: {}",
|
||||
cmd
|
||||
));
|
||||
match cmd {
|
||||
"win_minimize" => {
|
||||
let res = cmd_window.minimize();
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Minimize result: {:?}",
|
||||
res
|
||||
));
|
||||
}
|
||||
"win_maximize" => {
|
||||
let is_max = cmd_window
|
||||
.is_maximized()
|
||||
.unwrap_or(false);
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Currently maximized: {}",
|
||||
is_max
|
||||
));
|
||||
let res = if is_max {
|
||||
cmd_window.unmaximize()
|
||||
} else {
|
||||
cmd_window.maximize()
|
||||
};
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Maximize/Unmaximize result: {:?}",
|
||||
res
|
||||
));
|
||||
}
|
||||
"win_close" => {
|
||||
let res = cmd_window.close();
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Close result: {:?}",
|
||||
res
|
||||
));
|
||||
}
|
||||
"win_drag" => {
|
||||
let res = cmd_window.start_dragging();
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Drag result: {:?}",
|
||||
res
|
||||
));
|
||||
}
|
||||
"open_new_window" => {
|
||||
debug_log("[TAURI-CMD-DEBUG] open_new_window command received");
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] Full message data: {}",
|
||||
serde_json::to_string(&msg.data)
|
||||
.unwrap_or_default()
|
||||
));
|
||||
if let Some(url) = msg
|
||||
.data
|
||||
.get("url")
|
||||
.and_then(|u| u.as_str())
|
||||
{
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] URL extracted: {}",
|
||||
url
|
||||
));
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] URL length: {}",
|
||||
url.len()
|
||||
));
|
||||
debug_log("[TAURI-CMD-DEBUG] Attempting to dispatch to main thread");
|
||||
let app_handle = w.app_handle().clone();
|
||||
let closure_handle = app_handle.clone();
|
||||
let url_owned = url.to_string();
|
||||
let dispatch_result = app_handle
|
||||
.run_on_main_thread(move || {
|
||||
debug_log("[TAURI-CMD-DEBUG] Now on main thread, calling open_new_window");
|
||||
match open_new_window(
|
||||
closure_handle,
|
||||
url_owned,
|
||||
) {
|
||||
Ok(_) => {
|
||||
debug_log("[TAURI-CMD-DEBUG] open_new_window succeeded");
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] open_new_window error: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
match dispatch_result {
|
||||
Ok(_) => debug_log(
|
||||
"[TAURI-CMD-DEBUG] Main thread dispatch succeeded",
|
||||
),
|
||||
Err(e) => debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] Main thread dispatch failed: {}",
|
||||
e
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
debug_log("[TAURI-CMD-DEBUG] ERROR: No URL found in message data");
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD-DEBUG] Available keys: {:?}",
|
||||
msg.data
|
||||
.as_object()
|
||||
.map(|o| o.keys().collect::<Vec<_>>())
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug_log(&format!(
|
||||
"[TAURI-CMD] Window '{}' NOT FOUND!",
|
||||
window_label
|
||||
));
|
||||
}
|
||||
} else {
|
||||
let msg = match serde_json::from_str::<BackendMessage>(&line) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
debug_log(&format!("[MAIN-DEBUG] Received event: {}", msg.event));
|
||||
debug_log(&format!(
|
||||
"[MAIN-DEBUG] Event data: {}",
|
||||
serde_json::to_string(&msg.data).unwrap_or_default()
|
||||
));
|
||||
|
||||
match msg.event.as_str() {
|
||||
"tauri-cmd" => handle_tauri_cmd(&w, &msg),
|
||||
"ready" => handle_ready(&w, &msg),
|
||||
_ => {
|
||||
let _ = w.emit(&msg.event, msg.data.clone());
|
||||
}
|
||||
}
|
||||
@@ -830,18 +858,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
if window.label() == "main" {
|
||||
let state: State<NodeServices> = window.state();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Vyla Home",
|
||||
"version": "1.1.7",
|
||||
"version": "1.1.33",
|
||||
"identifier": "com.vyla.home",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
@@ -9,7 +9,7 @@
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": "default-src 'self' http://localhost:4890 http://127.0.0.1:4890; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:4890 http://127.0.0.1:4890 https://esm.sh; style-src 'self' 'unsafe-inline' http://localhost:4890 http://127.0.0.1:4890; img-src 'self' data: https: http://localhost:4890 http://127.0.0.1:4890; font-src 'self' data: http://localhost:4890 http://127.0.0.1:4890; connect-src 'self' http://localhost: http://127.0.0.1:"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -22,6 +22,10 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"macOS": {
|
||||
"exceptionDomain": "localhost",
|
||||
"infoPlist": "Info.plist"
|
||||
},
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "downloadBootstrapper"
|
||||
@@ -29,8 +33,9 @@
|
||||
},
|
||||
"resources": [
|
||||
"WebView2Loader.dll",
|
||||
"../bin/*",
|
||||
"../bin/**/*",
|
||||
"../main.js",
|
||||
"../preload.js",
|
||||
"../service-manager.js",
|
||||
"../patcher.js",
|
||||
"../crypto-utils.js",
|
||||
|
||||
311
ui/index.html
311
ui/index.html
@@ -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>
|
||||
@@ -256,23 +259,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="view-error" class="view-state">
|
||||
<div class="container">
|
||||
<svg class="icon icon-error" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="7" x2="12" y2="13" />
|
||||
<circle cx="12" cy="17" r="1" />
|
||||
</svg>
|
||||
|
||||
<h1>Couldn't start Vyla Home</h1>
|
||||
<p id="error-msg"></p>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="primary" onclick="location.reload()">Try Again</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,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;
|
||||
@@ -399,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%">
|
||||
@@ -486,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");
|
||||
@@ -540,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;
|
||||
}
|
||||
@@ -560,13 +569,14 @@ void main() {
|
||||
if (progressTrack) progressTrack.style.display = "block";
|
||||
const percent = data.percent || 0;
|
||||
if (progressFill) progressFill.style.width = percent + "%";
|
||||
setStatus(`Downloading update... ${percent}%`);
|
||||
setStatus(`Downloading update (${percent}%)...`);
|
||||
}
|
||||
});
|
||||
|
||||
eventApi.listen("update-available", (e) => {
|
||||
const data = e.payload || e;
|
||||
if (data) {
|
||||
if (progressTrack) progressTrack.style.display = "none";
|
||||
setStatus(`Update Available: ${data.latest} (Current: ${data.current})`);
|
||||
if (actionsEl) actionsEl.style.display = "flex";
|
||||
}
|
||||
@@ -575,16 +585,17 @@ void main() {
|
||||
eventApi.listen("ready", (e) => {
|
||||
const data = e.payload || e;
|
||||
if (data && data.url) {
|
||||
if (progressTrack) progressTrack.style.display = "none";
|
||||
if (actionsEl) actionsEl.style.display = "none";
|
||||
setStatus("Launching...");
|
||||
window.location.href = data.url;
|
||||
}
|
||||
});
|
||||
|
||||
eventApi.listen("error", (e) => {
|
||||
const data = e.payload || e;
|
||||
if (data && data.message) {
|
||||
setStatus(`Error: ${data.message}`, "#ef4444");
|
||||
}
|
||||
const msg = data && data.message ? data.message : JSON.stringify(data);
|
||||
setStatus("Runtime Error", "#fca5a5");
|
||||
showDebug(msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,8 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
async checkForUpdates() {
|
||||
const releases = await this.fetchJson(RELEASES_API);
|
||||
const cacheBuster = `?cb=${Date.now()}`;
|
||||
const releases = await this.fetchJson(RELEASES_API + cacheBuster);
|
||||
if (!releases || releases.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user