mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-14 06:07:23 -05:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
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."'
|
||||||
@@ -18,20 +18,23 @@ const REPO_CONFIG = {
|
|||||||
'stream-api': { name: 'stream-api', branch: 'main' },
|
'stream-api': { name: 'stream-api', branch: 'main' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clonedRepos = new Set();
|
||||||
|
|
||||||
function clonePrivateRepo(repoConfig, targetDir) {
|
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 token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
|
||||||
const auth = token ? `oauth2:${token}@` : '';
|
const auth = token ? `oauth2:${token}@` : '';
|
||||||
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
|
const repoUrl = `https://${auth}gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
|
||||||
|
|
||||||
try {
|
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}`, {
|
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
|
||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
});
|
});
|
||||||
|
clonedRepos.add(repoConfig.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`);
|
throw new Error(`Failed to clone ${repoConfig.name}: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -332,9 +335,11 @@ async function build() {
|
|||||||
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
fs.writeFileSync(path.join(__dirname, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
||||||
|
|
||||||
for (const repo of REPOS) {
|
for (const repo of REPOS) {
|
||||||
const repoPath = path.join(__dirname, repo);
|
if (clonedRepos.has(repo)) {
|
||||||
if (fs.existsSync(repoPath)) {
|
const repoPath = path.join(__dirname, repo);
|
||||||
fs.rmSync(repoPath, { recursive: true, force: true });
|
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
|
||||||
94
main.js
94
main.js
@@ -6,10 +6,19 @@ const ServiceManager = require('./service-manager');
|
|||||||
const Patcher = require('./patcher');
|
const Patcher = require('./patcher');
|
||||||
const UpdateClient = require('./update-client');
|
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'))
|
const APPDATA_DIR = (process.env.APPDATA_PATH && !process.env.APPDATA_PATH.includes('com.vyla.home'))
|
||||||
? process.env.APPDATA_PATH
|
? process.env.APPDATA_PATH
|
||||||
: defaultAppData;
|
: getDefaultAppData();
|
||||||
|
|
||||||
const LOGS_DIR = path.join(APPDATA_DIR, 'logs');
|
const LOGS_DIR = path.join(APPDATA_DIR, 'logs');
|
||||||
try { fs.mkdirSync(LOGS_DIR, { recursive: true }); } catch (e) { }
|
try { fs.mkdirSync(LOGS_DIR, { recursive: true }); } catch (e) { }
|
||||||
@@ -84,11 +93,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"));
|
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) => {
|
process.on('uncaughtException', (err) => {
|
||||||
debugLog(`CRITICAL UNCAUGHT EXCEPTION: ${err.stack || err.message}`);
|
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');
|
debugLog('Pipe to parent closed - parent process gone, exiting');
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
try { sendToTauri('error', { message: err.message }); } catch (e) { }
|
try { sendToTauri('error', { message: err.message }); } catch (e) { }
|
||||||
});
|
});
|
||||||
@@ -126,6 +144,9 @@ async function start() {
|
|||||||
|
|
||||||
const installBuild = async (release) => {
|
const installBuild = async (release) => {
|
||||||
const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip');
|
const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip');
|
||||||
|
if (fs.existsSync(tempZipPath)) {
|
||||||
|
try { fs.unlinkSync(tempZipPath); } catch (e) { }
|
||||||
|
}
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let downloadSuccess = false;
|
let downloadSuccess = false;
|
||||||
|
|
||||||
@@ -135,9 +156,13 @@ async function start() {
|
|||||||
debugLog(`Attempting to download update (Attempt ${attempts})...`);
|
debugLog(`Attempting to download update (Attempt ${attempts})...`);
|
||||||
sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` });
|
sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` });
|
||||||
|
|
||||||
|
let lastPercent = -1;
|
||||||
await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => {
|
await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => {
|
||||||
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
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...');
|
debugLog('Download complete. Verifying checksum...');
|
||||||
@@ -187,10 +212,34 @@ async function start() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
sendToTauri('status', { message: 'Inspecting local workspace and installed files...' });
|
sendToTauri('status', { message: 'Inspecting local workspace and installed files...' });
|
||||||
const isFirstRun = !patcher.verifyExtraction();
|
let isFirstRun = !patcher.verifyExtraction();
|
||||||
const localVersion = patcher.getCurrentVersion();
|
let localVersion = patcher.getCurrentVersion();
|
||||||
const localSha = patcher.getCurrentSha();
|
let localSha = patcher.getCurrentSha();
|
||||||
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`);
|
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}, Local SHA: ${localSha}`);
|
||||||
|
|
||||||
|
const localZipPath = path.join(__dirname, 'update-payload.zip');
|
||||||
|
const localManifestPath = path.join(__dirname, 'manifest.json');
|
||||||
|
let localZipManifest = null;
|
||||||
|
|
||||||
|
if (fs.existsSync(localZipPath) && fs.existsSync(localManifestPath)) {
|
||||||
|
try { localZipManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8')); } catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localZipManifest && (isFirstRun || localZipManifest.sha256 !== localSha)) {
|
||||||
|
debugLog("New local payload detected! Applying local build...");
|
||||||
|
sendToTauri('status', { message: 'Extracting local development payload...' });
|
||||||
|
patcher.purgeOldCode();
|
||||||
|
patcher.extractZipPayload(localZipPath);
|
||||||
|
patcher.applyPatches(getReplacementsConfig());
|
||||||
|
patcher.bridgeCredentials(serviceManager.credentialManager);
|
||||||
|
patcher.saveCurrentVersion(localZipManifest.version, localZipManifest.sha256);
|
||||||
|
serviceManager.envConfig = getEnvConfig();
|
||||||
|
|
||||||
|
isFirstRun = false;
|
||||||
|
localVersion = localZipManifest.version;
|
||||||
|
localSha = localZipManifest.sha256;
|
||||||
|
debugLog("Local payload applied successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
sendToTauri('status', { message: 'Connecting to update servers...' });
|
sendToTauri('status', { message: 'Connecting to update servers...' });
|
||||||
let release = null;
|
let release = null;
|
||||||
@@ -261,7 +310,14 @@ async function start() {
|
|||||||
patcher.extractZipPayload(localZip);
|
patcher.extractZipPayload(localZip);
|
||||||
patcher.applyPatches(getReplacementsConfig());
|
patcher.applyPatches(getReplacementsConfig());
|
||||||
patcher.bridgeCredentials(serviceManager.credentialManager);
|
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();
|
serviceManager.envConfig = getEnvConfig();
|
||||||
debugLog("Local payload extracted successfully.");
|
debugLog("Local payload extracted successfully.");
|
||||||
} else {
|
} else {
|
||||||
@@ -367,17 +423,23 @@ process.on('exit', () => {
|
|||||||
if (serviceManager) {
|
if (serviceManager) {
|
||||||
for (const name of Object.keys(serviceManager.processes)) {
|
for (const name of Object.keys(serviceManager.processes)) {
|
||||||
const proc = serviceManager.processes[name];
|
const proc = serviceManager.processes[name];
|
||||||
if (proc && proc.child) {
|
if (proc && proc.child && proc.child.pid) {
|
||||||
try { proc.child.kill('SIGKILL'); } catch (e) { }
|
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 () => {
|
process.on('SIGINT', () => {
|
||||||
if (expressServer) expressServer.close();
|
process.exit(0);
|
||||||
if (playerExpressServer) playerExpressServer.close();
|
});
|
||||||
if (serviceManager) await serviceManager.stopAll();
|
process.on('SIGTERM', () => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vyla-home",
|
"name": "vyla-home",
|
||||||
"version": "1.1.9",
|
"version": "1.1.25",
|
||||||
"description": "Vyla Home Desktop App Launcher",
|
"description": "Vyla Home Desktop App Launcher",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
19
patcher.js
19
patcher.js
@@ -63,17 +63,24 @@ class Patcher {
|
|||||||
const fullPath = path.join(this.appDataPath, dir);
|
const fullPath = path.join(this.appDataPath, dir);
|
||||||
if (fs.existsSync(fullPath)) {
|
if (fs.existsSync(fullPath)) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
const trashPath = fullPath + '.trash.' + Date.now();
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||||
fs.renameSync(fullPath, trashPath);
|
} catch (err2) { }
|
||||||
} catch (renameErr) {
|
|
||||||
console.warn(`Could not remove or rename ${fullPath}. It will be overwritten.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
extractZipPayload(zipPath) {
|
||||||
|
|||||||
169
release.js
169
release.js
@@ -78,12 +78,12 @@ function uploadFileToGitLabPackage(filePath, packageName, version, fileName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createGitLabRelease(tag, releaseLinks) {
|
function createGitLabRelease(tag, releaseLinks) {
|
||||||
const sendRequest = (method, path, data = null) => {
|
const sendRequest = (method, reqPath, data = null) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
hostname: 'gitlab.com',
|
hostname: 'gitlab.com',
|
||||||
port: 443,
|
port: 443,
|
||||||
path: path,
|
path: reqPath,
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'PRIVATE-TOKEN': TOKEN,
|
'PRIVATE-TOKEN': TOKEN,
|
||||||
@@ -101,19 +101,9 @@ function createGitLabRelease(tag, releaseLinks) {
|
|||||||
res.on('data', chunk => body += chunk);
|
res.on('data', chunk => body += chunk);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
try {
|
try { resolve(body ? JSON.parse(body) : {}); } catch (e) { resolve({}); }
|
||||||
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);
|
|
||||||
} else {
|
} 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}`,
|
name: `Vyla Home Release ${tag}`,
|
||||||
tag_name: tag,
|
tag_name: tag,
|
||||||
ref: currentRef,
|
ref: currentRef,
|
||||||
description: `Automated local release ${tag}`,
|
description: `Automated Multi-Platform Release ${tag}`,
|
||||||
assets: {
|
assets: {
|
||||||
links: releaseLinks
|
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() {
|
async function main() {
|
||||||
@@ -166,7 +180,6 @@ async function main() {
|
|||||||
if (isFullBuild) {
|
if (isFullBuild) {
|
||||||
console.log('Ensuring bundled Node.js runtime...');
|
console.log('Ensuring bundled Node.js runtime...');
|
||||||
await ensureBundledNode();
|
await ensureBundledNode();
|
||||||
|
|
||||||
await ensureWebView2Loader();
|
await ensureWebView2Loader();
|
||||||
|
|
||||||
const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle');
|
const bundleDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle');
|
||||||
@@ -174,20 +187,32 @@ async function main() {
|
|||||||
fs.rmSync(bundleDir, { recursive: true, force: true });
|
fs.rmSync(bundleDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Compiling Windows Tauri installer locally...');
|
console.log('Compiling native Tauri application...');
|
||||||
execSync('npm run build', {
|
execSync('npm run build', { stdio: 'inherit' });
|
||||||
stdio: 'inherit'
|
|
||||||
});
|
|
||||||
|
|
||||||
const nsisDir = path.join(__dirname, 'src-tauri/target/release/bundle/nsis');
|
const targetVersion = version.startsWith('v') ? version.substring(1) : version;
|
||||||
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));
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (process.platform === 'win32') {
|
||||||
const originalName = files[0];
|
const nsisDir = path.join(__dirname, 'src-tauri', 'target', 'release', 'bundle', 'nsis');
|
||||||
installerPath = path.join(nsisDir, originalName);
|
if (fs.existsSync(nsisDir)) {
|
||||||
installerUploadName = originalName.replace(/\s+/g, '-');
|
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) {
|
if (installerPath && installerUploadName) {
|
||||||
console.log(`Uploading installer ${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);
|
const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUnique);
|
||||||
releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' });
|
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...');
|
console.log('[3/4] Publishing GitLab Release...');
|
||||||
const release = await createGitLabRelease(version, releaseLinks);
|
await createGitLabRelease(version, releaseLinks);
|
||||||
console.log(`[4/4] Release successfully created: ${release.name} (${release.tag_name})`);
|
console.log(`[4/4] Release process successfully completed for ${version}!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBundledNode() {
|
function ensureBundledNode() {
|
||||||
|
const os = require('os');
|
||||||
const binDir = path.join(__dirname, 'bin');
|
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 nodeVersion = 'v22.14.0';
|
||||||
const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`);
|
const versionMarker = path.join(binDir, `node-${nodeVersion}.marker`);
|
||||||
@@ -232,35 +263,65 @@ function ensureBundledNode() {
|
|||||||
fs.mkdirSync(binDir, { recursive: true });
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
console.log(`Downloading standalone Node.js ${nodeVersion} runtime for bundling...`);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fileStream = fs.createWriteStream(nodeExePath);
|
const download = (url, dest, cb) => {
|
||||||
https.get(nodeUrl, (res) => {
|
https.get(url, (res) => {
|
||||||
if (res.statusCode === 302 || res.statusCode === 301) {
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||||
https.get(res.headers.location, (redirectRes) => {
|
return download(res.headers.location, dest, cb);
|
||||||
redirectRes.pipe(fileStream);
|
}
|
||||||
fileStream.on('finish', () => {
|
const fileStream = fs.createWriteStream(dest);
|
||||||
fileStream.close();
|
res.pipe(fileStream);
|
||||||
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
|
fileStream.on('finish', () => {
|
||||||
resolve();
|
fileStream.close();
|
||||||
});
|
cb();
|
||||||
}).on('error', reject);
|
});
|
||||||
return;
|
}).on('error', reject);
|
||||||
}
|
};
|
||||||
res.pipe(fileStream);
|
|
||||||
fileStream.on('finish', () => {
|
if (isWin) {
|
||||||
fileStream.close();
|
const nodeUrl = `https://nodejs.org/dist/${nodeVersion}/win-x64/node.exe`;
|
||||||
|
download(nodeUrl, nodeExePath, () => {
|
||||||
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
|
fs.writeFileSync(versionMarker, nodeVersion, 'utf8');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}).on('error', (err) => {
|
} else {
|
||||||
try { fs.unlinkSync(nodeExePath); } catch (e) { }
|
const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
|
||||||
reject(err);
|
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() {
|
function ensureWebView2Loader() {
|
||||||
|
const os = require('os');
|
||||||
|
if (os.platform() !== 'win32') return Promise.resolve();
|
||||||
|
|
||||||
const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release');
|
const releaseDir = path.join(__dirname, 'src-tauri', 'target', 'release');
|
||||||
const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll');
|
const rootDllPath = path.join(__dirname, 'src-tauri', 'WebView2Loader.dll');
|
||||||
fs.mkdirSync(releaseDir, { recursive: true });
|
fs.mkdirSync(releaseDir, { recursive: true });
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -4222,7 +4222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vyla-home"
|
name = "vyla-home"
|
||||||
version = "1.1.9"
|
version = "1.1.25"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vyla-home"
|
name = "vyla-home"
|
||||||
version = "1.1.9"
|
version = "1.1.25"
|
||||||
description = "Vyla Home Desktop App Launcher"
|
description = "Vyla Home Desktop App Launcher"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
21
src-tauri/Info.plist
Normal file
21
src-tauri/Info.plist
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=src/main.rs");
|
println!("cargo:rerun-if-changed=src/main.rs");
|
||||||
println!("cargo:rerun-if-changed=tauri.conf.json");
|
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||||
|
|
||||||
|
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
|
||||||
|
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {
|
||||||
|
let mut target_dir = std::path::PathBuf::from(out_dir);
|
||||||
|
target_dir.pop();
|
||||||
|
target_dir.pop();
|
||||||
|
target_dir.pop();
|
||||||
|
let dll_src = std::path::Path::new(&manifest_dir).join("WebView2Loader.dll");
|
||||||
|
let dll_dest = target_dir.join("WebView2Loader.dll");
|
||||||
|
if dll_src.exists() {
|
||||||
|
let _ = std::fs::copy(dll_src, dll_dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
@@ -7,26 +7,19 @@ use tauri::{Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
|
|||||||
|
|
||||||
fn debug_log(msg: &str) {
|
fn debug_log(msg: &str) {
|
||||||
let log_dir = if let Ok(app_data) = std::env::var("APPDATA") {
|
let log_dir = if let Ok(app_data) = std::env::var("APPDATA") {
|
||||||
std::path::PathBuf::from(app_data)
|
std::path::PathBuf::from(app_data).join("vyla-home").join("vyla-home-files").join("logs")
|
||||||
.join("vyla-home")
|
} else if let Ok(home) = std::env::var("HOME") {
|
||||||
.join("vyla-home-files")
|
std::path::PathBuf::from(home).join("Library").join("Logs").join("VylaHome")
|
||||||
.join("logs")
|
|
||||||
} else if let Ok(user_profile) = std::env::var("USERPROFILE") {
|
} else if let Ok(user_profile) = std::env::var("USERPROFILE") {
|
||||||
std::path::PathBuf::from(user_profile)
|
std::path::PathBuf::from(user_profile).join(".vyla-home").join("logs")
|
||||||
.join(".vyla-home")
|
|
||||||
.join("logs")
|
|
||||||
} else {
|
} else {
|
||||||
std::path::PathBuf::from("logs")
|
std::env::temp_dir().join("vyla-home-logs")
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = std::fs::create_dir_all(&log_dir);
|
let _ = std::fs::create_dir_all(&log_dir);
|
||||||
let log_path = log_dir.join("rust-debug.log");
|
let log_path = log_dir.join("rust-debug.log");
|
||||||
|
|
||||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&log_path) {
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(&log_path)
|
|
||||||
{
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let _ = writeln!(file, "[RUST] {}", msg);
|
let _ = writeln!(file, "[RUST] {}", msg);
|
||||||
}
|
}
|
||||||
@@ -50,17 +43,38 @@ fn clean_path(path: &std::path::Path) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn kill_child_process(child: &mut Child) {
|
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.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_node_binary(app: &tauri::App) -> Option<String> {
|
fn find_node_binary(app: &tauri::App) -> Option<String> {
|
||||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||||
|
#[cfg(windows)]
|
||||||
let candidates = [
|
let candidates = [
|
||||||
resource_dir.join("bin").join("node.exe"),
|
resource_dir.join("bin").join("node.exe"),
|
||||||
resource_dir.join("node.exe"),
|
resource_dir.join("node.exe"),
|
||||||
resource_dir.join("_up_").join("bin").join("node.exe"),
|
resource_dir.join("_up_").join("bin").join("node.exe"),
|
||||||
resource_dir.join("_up_").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 {
|
for candidate in candidates {
|
||||||
if candidate.exists() {
|
if candidate.exists() {
|
||||||
@@ -73,9 +87,13 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
|
|||||||
if let Some(parent) = exe_path.parent() {
|
if let Some(parent) = exe_path.parent() {
|
||||||
let candidates = [
|
let candidates = [
|
||||||
parent.join("bin").join("node.exe"),
|
parent.join("bin").join("node.exe"),
|
||||||
|
parent.join("bin").join("node"),
|
||||||
parent.join("node.exe"),
|
parent.join("node.exe"),
|
||||||
|
parent.join("node"),
|
||||||
parent.join("resources").join("bin").join("node.exe"),
|
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.exe"),
|
||||||
|
parent.join("resources").join("node"),
|
||||||
];
|
];
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
if candidate.exists() {
|
if candidate.exists() {
|
||||||
@@ -87,9 +105,13 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
|
|||||||
|
|
||||||
if let Ok(path_var) = std::env::var("PATH") {
|
if let Ok(path_var) = std::env::var("PATH") {
|
||||||
for p in std::env::split_paths(&path_var) {
|
for p in std::env::split_paths(&path_var) {
|
||||||
let candidate = p.join("node.exe");
|
let node_exe = p.join("node.exe");
|
||||||
if candidate.exists() {
|
if node_exe.exists() {
|
||||||
return Some(clean_path(&candidate));
|
return Some(clean_path(&node_exe));
|
||||||
|
}
|
||||||
|
let node = p.join("node");
|
||||||
|
if node.exists() {
|
||||||
|
return Some(clean_path(&node));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +140,22 @@ fn find_node_binary(app: &tauri::App) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let default_paths = [
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
"/usr/bin/node",
|
||||||
|
"/opt/homebrew/bin/node",
|
||||||
|
"/opt/local/bin/node",
|
||||||
|
];
|
||||||
|
for path in default_paths {
|
||||||
|
let candidate = std::path::Path::new(path);
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(clean_path(candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +164,7 @@ fn resolve_script_and_root(app: &tauri::App) -> (String, String) {
|
|||||||
let candidates = vec![
|
let candidates = vec![
|
||||||
resource_dir.join("main.js"),
|
resource_dir.join("main.js"),
|
||||||
resource_dir.join("_up_").join("main.js"),
|
resource_dir.join("_up_").join("main.js"),
|
||||||
|
resource_dir.join("up").join("main.js"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
@@ -482,6 +521,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]
|
#[tauri::command]
|
||||||
fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> {
|
fn get_logo_base64(app: tauri::AppHandle) -> Result<String, String> {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@@ -528,44 +591,99 @@ fn main() {
|
|||||||
win_close,
|
win_close,
|
||||||
win_drag,
|
win_drag,
|
||||||
open_new_window,
|
open_new_window,
|
||||||
get_logo_base64
|
get_logo_base64,
|
||||||
|
kill_orphans
|
||||||
])
|
])
|
||||||
.manage(NodeServices(Mutex::new(None)))
|
.manage(NodeServices(Mutex::new(None)))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
debug_log("Tauri setup started");
|
debug_log("Tauri setup started");
|
||||||
let app_data = app.path().app_data_dir().unwrap_or_default();
|
let app_data = app.path().app_data_dir().unwrap_or_default();
|
||||||
let app_data_str = clean_path(&app_data);
|
|
||||||
|
let vyla_files = if let Ok(appdata_env) = std::env::var("APPDATA") {
|
||||||
|
std::path::PathBuf::from(appdata_env)
|
||||||
|
.join("vyla-home")
|
||||||
|
.join("vyla-home-files")
|
||||||
|
} else if let Ok(home) = std::env::var("HOME") {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let base = std::path::PathBuf::from(&home)
|
||||||
|
.join("Library")
|
||||||
|
.join("Application Support");
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let base = std::path::PathBuf::from(&home)
|
||||||
|
.join(".local")
|
||||||
|
.join("share");
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||||
|
let base = app_data.clone();
|
||||||
|
base.join("vyla-home").join("vyla-home-files")
|
||||||
|
} else {
|
||||||
|
app_data.join("vyla-home-files")
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_file = vyla_files.join("version.json");
|
||||||
|
let current_app_version = app.package_info().version.to_string();
|
||||||
|
let mut version_mismatch = false;
|
||||||
|
|
||||||
|
if version_file.exists() {
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&version_file) {
|
||||||
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||||
|
if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
|
||||||
|
if version != current_app_version {
|
||||||
|
version_mismatch = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
version_mismatch = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
version_mismatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if version_mismatch {
|
||||||
|
debug_log(&format!("App version updated to {}. Cleaning old files.", current_app_version));
|
||||||
|
let dirs_to_remove = ["frontend", "player", "stream-api", "auth-proxy", "config"];
|
||||||
|
for d in dirs_to_remove {
|
||||||
|
let _ = std::fs::remove_dir_all(vyla_files.join(d));
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_file(&version_file);
|
||||||
|
}
|
||||||
|
|
||||||
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
|
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
|
||||||
.title("Vyla Home")
|
.title("Vyla Home")
|
||||||
.inner_size(1280.0, 800.0)
|
.inner_size(1280.0, 800.0)
|
||||||
.decorations(false)
|
.decorations(false)
|
||||||
.transparent(false)
|
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
.devtools(cfg!(debug_assertions))
|
.devtools(true)
|
||||||
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
|
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
|
||||||
.initialization_script(LINK_HANDLER_SCRIPT)
|
.initialization_script(LINK_HANDLER_SCRIPT)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let (main_js, root_dir) = resolve_script_and_root(app);
|
let (main_js, root_dir) = resolve_script_and_root(app);
|
||||||
|
debug_log("Searching for node binary...");
|
||||||
let node_bin = match find_node_binary(app) {
|
let node_bin = match find_node_binary(app) {
|
||||||
Some(bin) => bin,
|
Some(bin) => {
|
||||||
|
debug_log(&format!("Found node binary: {}", bin));
|
||||||
|
bin
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let _ = window.emit(
|
debug_log("FATAL: No node binary found in bundle or PATH.");
|
||||||
"error",
|
std::thread::sleep(std::time::Duration::from_millis(600));
|
||||||
serde_json::json!({
|
let _ = window.eval("document.getElementById('status-message').textContent='Error: Node runtime missing. Please reinstall.';");
|
||||||
"message": "Node.js runtime not found. Please ensure Node.js is installed or bundled correctly."
|
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(());
|
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);
|
let mut cmd = Command::new(&node_bin);
|
||||||
cmd.arg(&main_js)
|
cmd.arg(&main_js)
|
||||||
.current_dir(&root_dir)
|
.current_dir(&root_dir)
|
||||||
.env("APPDATA_PATH", &app_data_str)
|
.env("APPDATA_PATH", &vyla_files_str)
|
||||||
.env("NODE_EXE", &node_bin)
|
.env("NODE_EXE", &node_bin)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
@@ -583,11 +701,15 @@ fn main() {
|
|||||||
child.id()
|
child.id()
|
||||||
));
|
));
|
||||||
if let Some(stderr) = child.stderr.take() {
|
if let Some(stderr) = child.stderr.take() {
|
||||||
|
let w = window.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
let reader = BufReader::new(stderr);
|
let reader = BufReader::new(stderr);
|
||||||
for line in reader.lines().flatten() {
|
for line in reader.lines().flatten() {
|
||||||
debug_log(&format!("[NODE STDERR] {}", line));
|
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) }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -751,12 +873,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug_log(&format!("Failed to spawn node: {}", e));
|
debug_log(&format!("Failed to spawn node: {}", e));
|
||||||
let _ = window.emit(
|
let emsg = format!("Failed to spawn node: {}", e);
|
||||||
"error",
|
let js = format!(
|
||||||
serde_json::json!({
|
"document.getElementById('status-message').textContent='Spawn Error';var b=document.getElementById('debug-log-box');b.style.display='block';b.textContent={};",
|
||||||
"message": format!("Failed to launch backend runtime: {}", e)
|
serde_json::to_string(&emsg).unwrap_or_default()
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
let _ = window.eval(&js);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Vyla Home",
|
"productName": "Vyla Home",
|
||||||
"version": "1.1.9",
|
"version": "1.1.25",
|
||||||
"identifier": "com.vyla.home",
|
"identifier": "com.vyla.home",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"security": {
|
"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": {
|
"bundle": {
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
|
"macOS": {
|
||||||
|
"exceptionDomain": "localhost",
|
||||||
|
"infoPlist": "../Info.plist"
|
||||||
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"webviewInstallMode": {
|
"webviewInstallMode": {
|
||||||
"type": "downloadBootstrapper"
|
"type": "downloadBootstrapper"
|
||||||
@@ -29,8 +33,9 @@
|
|||||||
},
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
"WebView2Loader.dll",
|
"WebView2Loader.dll",
|
||||||
"../bin/*",
|
"../bin/**/*",
|
||||||
"../main.js",
|
"../main.js",
|
||||||
|
"../preload.js",
|
||||||
"../service-manager.js",
|
"../service-manager.js",
|
||||||
"../patcher.js",
|
"../patcher.js",
|
||||||
"../crypto-utils.js",
|
"../crypto-utils.js",
|
||||||
|
|||||||
311
ui/index.html
311
ui/index.html
@@ -195,6 +195,9 @@
|
|||||||
<div class="loader-container">
|
<div class="loader-container">
|
||||||
<div class="loader-card">
|
<div class="loader-card">
|
||||||
<div class="status-text" id="status-message">Starting runtime engine...</div>
|
<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-track" id="progress-track" style="display: none;">
|
||||||
<div class="progress-fill" id="progress-fill"></div>
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,23 +259,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -284,61 +271,66 @@
|
|||||||
console.warn("OGL Shader background failed to load (offline):", err);
|
console.warn("OGL Shader background failed to load (offline):", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!window.__OGL_LOADED__) {
|
window.dispatchEvent(new Event('ogl-ready'));
|
||||||
document.body.style.background = "radial-gradient(circle at 80% 20%, #1e1b4b 0%, #050505 70%)";
|
</script>
|
||||||
} 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 hexToRgb(hex) {
|
<script>
|
||||||
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
window.addEventListener('ogl-ready', () => {
|
||||||
if (!match) return [1, 1, 1];
|
if (!window.__OGL_LOADED__) {
|
||||||
return [
|
document.body.style.background = "radial-gradient(circle at 80% 20%, #1e1b4b 0%, #050505 70%)";
|
||||||
parseInt(match[1], 16) / 255,
|
} else {
|
||||||
parseInt(match[2], 16) / 255,
|
const { Renderer, Program, Triangle, Mesh } = window.__OGL_LOADED__;
|
||||||
parseInt(match[3], 16) / 255
|
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) {
|
function hexToRgb(hex) {
|
||||||
switch (origin) {
|
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
case "top-left": return [1, 0];
|
if (!match) return [1, 1, 1];
|
||||||
case "bottom-right": return [0, 1];
|
return [
|
||||||
case "bottom-left": return [1, 1];
|
parseInt(match[1], 16) / 255,
|
||||||
default: return [0, 0];
|
parseInt(match[2], 16) / 255,
|
||||||
|
parseInt(match[3], 16) / 255
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = new Renderer({
|
function originToFlip(origin) {
|
||||||
dpr: Math.min(window.devicePixelRatio, 2),
|
switch (origin) {
|
||||||
alpha: true
|
case "top-left": return [1, 0];
|
||||||
});
|
case "bottom-right": return [0, 1];
|
||||||
const gl = renderer.gl;
|
case "bottom-left": return [1, 1];
|
||||||
gl.canvas.style.width = "100%";
|
default: return [0, 0];
|
||||||
gl.canvas.style.height = "100%";
|
}
|
||||||
rayContainer.appendChild(gl.canvas);
|
}
|
||||||
|
|
||||||
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;
|
attribute vec2 position;
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = vec4(position, 0.0, 1.0);
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const fragmentShader = `
|
const fragmentShader = `
|
||||||
precision highp float;
|
precision highp float;
|
||||||
uniform float iTime;
|
uniform float iTime;
|
||||||
uniform vec2 iResolution;
|
uniform vec2 iResolution;
|
||||||
@@ -399,77 +391,77 @@ void main() {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const [flipX, flipY] = originToFlip(settings.origin);
|
const [flipX, flipY] = originToFlip(settings.origin);
|
||||||
const uniforms = {
|
const uniforms = {
|
||||||
iTime: { value: 0 },
|
iTime: { value: 0 },
|
||||||
iResolution: { value: [1, 1] },
|
iResolution: { value: [1, 1] },
|
||||||
iSpeed: { value: settings.speed },
|
iSpeed: { value: settings.speed },
|
||||||
iRayColor1: { value: hexToRgb(settings.rayColor1) },
|
iRayColor1: { value: hexToRgb(settings.rayColor1) },
|
||||||
iRayColor2: { value: hexToRgb(settings.rayColor2) },
|
iRayColor2: { value: hexToRgb(settings.rayColor2) },
|
||||||
iIntensity: { value: settings.intensity },
|
iIntensity: { value: settings.intensity },
|
||||||
iSpread: { value: settings.spread },
|
iSpread: { value: settings.spread },
|
||||||
iFlipX: { value: flipX },
|
iFlipX: { value: flipX },
|
||||||
iFlipY: { value: flipY },
|
iFlipY: { value: flipY },
|
||||||
iTilt: { value: settings.tilt },
|
iTilt: { value: settings.tilt },
|
||||||
iSaturation: { value: settings.saturation },
|
iSaturation: { value: settings.saturation },
|
||||||
iBlend: { value: settings.blend },
|
iBlend: { value: settings.blend },
|
||||||
iFalloff: { value: settings.falloff },
|
iFalloff: { value: settings.falloff },
|
||||||
iOpacity: { value: settings.opacity }
|
iOpacity: { value: settings.opacity }
|
||||||
};
|
};
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
const geometry = new Triangle(gl);
|
||||||
const program = new Program(gl, { vertex: vertexShader, fragment: fragmentShader, uniforms });
|
const program = new Program(gl, { vertex: vertexShader, fragment: fragmentShader, uniforms });
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
const width = rayContainer.clientWidth;
|
const width = rayContainer.clientWidth;
|
||||||
const height = rayContainer.clientHeight;
|
const height = rayContainer.clientHeight;
|
||||||
renderer.dpr = Math.min(window.devicePixelRatio, 2);
|
renderer.dpr = Math.min(window.devicePixelRatio, 2);
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height);
|
||||||
uniforms.iResolution.value = [width * renderer.dpr, height * renderer.dpr];
|
uniforms.iResolution.value = [width * renderer.dpr, height * renderer.dpr];
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", resize);
|
window.addEventListener("resize", resize);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
function animate(time) {
|
function animate(time) {
|
||||||
uniforms.iTime.value = time * 0.001;
|
uniforms.iTime.value = time * 0.001;
|
||||||
renderer.render({ scene: mesh });
|
renderer.render({ scene: mesh });
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
|
|
||||||
function initLiquidButtons() {
|
function initLiquidButtons() {
|
||||||
const borderWidth = 0.16;
|
const borderWidth = 0.16;
|
||||||
const brightness = 35;
|
const brightness = 35;
|
||||||
const opacity = 0.55;
|
const opacity = 0.55;
|
||||||
const blur = 4;
|
const blur = 4;
|
||||||
const displace = 1.2;
|
const displace = 1.2;
|
||||||
const borderRadius = 9999;
|
const borderRadius = 9999;
|
||||||
const distortionScale = -380;
|
const distortionScale = -380;
|
||||||
const redOffset = -25;
|
const redOffset = -25;
|
||||||
const greenOffset = 0;
|
const greenOffset = 0;
|
||||||
const blueOffset = 25;
|
const blueOffset = 25;
|
||||||
const xChannel = "R";
|
const xChannel = "R";
|
||||||
const yChannel = "G";
|
const yChannel = "G";
|
||||||
const mixBlendMode = "screen";
|
const mixBlendMode = "screen";
|
||||||
|
|
||||||
document.querySelectorAll(".liquid-glass-btn").forEach((btn, index) => {
|
document.querySelectorAll(".liquid-glass-btn").forEach((btn, index) => {
|
||||||
const filterId = `glass-filter-${index + 1}`;
|
const filterId = `glass-filter-${index + 1}`;
|
||||||
btn.style.setProperty("--filter-id", `url(#${filterId})`);
|
btn.style.setProperty("--filter-id", `url(#${filterId})`);
|
||||||
|
|
||||||
const feImage = btn.querySelector(".fe-image");
|
const feImage = btn.querySelector(".fe-image");
|
||||||
const redChannel = btn.querySelector(".redchannel");
|
const redChannel = btn.querySelector(".redchannel");
|
||||||
const greenChannel = btn.querySelector(".greenchannel");
|
const greenChannel = btn.querySelector(".greenchannel");
|
||||||
const blueChannel = btn.querySelector(".bluechannel");
|
const blueChannel = btn.querySelector(".bluechannel");
|
||||||
const gaussianBlur = btn.querySelector(".gaussianBlur");
|
const gaussianBlur = btn.querySelector(".gaussianBlur");
|
||||||
|
|
||||||
function generateDisplacementMap() {
|
function generateDisplacementMap() {
|
||||||
const rect = btn.getBoundingClientRect();
|
const rect = btn.getBoundingClientRect();
|
||||||
const actualWidth = rect.width || 140;
|
const actualWidth = rect.width || 140;
|
||||||
const actualHeight = rect.height || 48;
|
const actualHeight = rect.height || 48;
|
||||||
|
|
||||||
const svgContent = `
|
const svgContent = `
|
||||||
<svg viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 ${actualWidth} ${actualHeight}" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="red-grad-${index}" x1="100%" y1="0%" x2="0%" y2="0%">
|
<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}"/>
|
<rect width="${actualWidth}" height="${actualHeight}" rx="${borderRadius}" fill="url(#blue-grad-${index})" style="mix-blend-mode:${mixBlendMode}"/>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
return "data:image/svg+xml," + encodeURIComponent(svgContent);
|
return "data:image/svg+xml," + encodeURIComponent(svgContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
feImage.setAttribute("href", generateDisplacementMap());
|
feImage.setAttribute("href", generateDisplacementMap());
|
||||||
redChannel.setAttribute("scale", distortionScale + redOffset);
|
redChannel.setAttribute("scale", distortionScale + redOffset);
|
||||||
greenChannel.setAttribute("scale", distortionScale + greenOffset);
|
greenChannel.setAttribute("scale", distortionScale + greenOffset);
|
||||||
blueChannel.setAttribute("scale", distortionScale + blueOffset);
|
blueChannel.setAttribute("scale", distortionScale + blueOffset);
|
||||||
[redChannel, greenChannel, blueChannel].forEach((ch) => {
|
[redChannel, greenChannel, blueChannel].forEach((ch) => {
|
||||||
ch.setAttribute("xChannelSelector", xChannel);
|
ch.setAttribute("xChannelSelector", xChannel);
|
||||||
ch.setAttribute("yChannelSelector", yChannel);
|
ch.setAttribute("yChannelSelector", yChannel);
|
||||||
});
|
});
|
||||||
gaussianBlur.setAttribute("stdDeviation", displace);
|
gaussianBlur.setAttribute("stdDeviation", displace);
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
btn.classList.remove("glass-surface--fallback");
|
btn.classList.remove("glass-surface--fallback");
|
||||||
btn.classList.add("glass-surface--svg");
|
btn.classList.add("glass-surface--svg");
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => setTimeout(update, 0));
|
const observer = new ResizeObserver(() => setTimeout(update, 0));
|
||||||
observer.observe(btn);
|
observer.observe(btn);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initLiquidButtons();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
initLiquidButtons();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const statusEl = document.getElementById("status-message");
|
const statusEl = document.getElementById("status-message");
|
||||||
|
const debugBox = document.getElementById("debug-log-box");
|
||||||
const progressTrack = document.getElementById("progress-track");
|
const progressTrack = document.getElementById("progress-track");
|
||||||
const progressFill = document.getElementById("progress-fill");
|
const progressFill = document.getElementById("progress-fill");
|
||||||
const actionsEl = document.getElementById("update-actions");
|
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() {
|
async function initTauriEvents() {
|
||||||
const eventApi = window.__TAURI__?.event || window.__TAURI_INTERNALS__;
|
let eventApi = null;
|
||||||
if (!eventApi || typeof eventApi.listen !== 'function') {
|
|
||||||
|
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);
|
setTimeout(initTauriEvents, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -560,7 +569,9 @@ void main() {
|
|||||||
if (progressTrack) progressTrack.style.display = "block";
|
if (progressTrack) progressTrack.style.display = "block";
|
||||||
const percent = data.percent || 0;
|
const percent = data.percent || 0;
|
||||||
if (progressFill) progressFill.style.width = percent + "%";
|
if (progressFill) progressFill.style.width = percent + "%";
|
||||||
setStatus(`Downloading update... ${percent}%`);
|
if (!currentStatusText.startsWith("Downloading update...")) {
|
||||||
|
setStatus("Downloading update...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -576,15 +587,15 @@ void main() {
|
|||||||
const data = e.payload || e;
|
const data = e.payload || e;
|
||||||
if (data && data.url) {
|
if (data && data.url) {
|
||||||
setStatus("Launching...");
|
setStatus("Launching...");
|
||||||
window.location.href = data.url;
|
window.location.replace(data.url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventApi.listen("error", (e) => {
|
eventApi.listen("error", (e) => {
|
||||||
const data = e.payload || e;
|
const data = e.payload || e;
|
||||||
if (data && data.message) {
|
const msg = data && data.message ? data.message : JSON.stringify(data);
|
||||||
setStatus(`Error: ${data.message}`, "#ef4444");
|
setStatus("Runtime Error", "#fca5a5");
|
||||||
}
|
showDebug(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,8 @@ class UpdateClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkForUpdates() {
|
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) {
|
if (!releases || releases.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user