Completely rewrote the code, home builds locally rather on Gitlab, releases are done manually aswell

This commit is contained in:
2026-08-21 14:32:59 -07:00
parent 103e080664
commit cee165ddfa
12 changed files with 993 additions and 588 deletions

View File

@@ -1,338 +1,22 @@
stages:
- build
- release
- check
build:
stage: build
check:
stage: check
tags:
- saas-windows-medium-amd64
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- build-pipeline.js
- package.json
- config/**/*
- src-tauri/**/*
- service-manager.js
- patcher.js
- credential-manager.js
- crypto-utils.js
- update-client.js
- .gitlab-ci.yml
variables:
GITLAB_TOKEN: $VYLA_GITLAB_TOKEN
PLAYER_TMDB_KEY: $TMDB_KEY
PLAYER_API_KEY: $API_KEY
STREAM_API_TMDB_API_KEY: $TMDB_API_KEY
STREAM_API_GA_API_SECRET: $GA_API_SECRET
STREAM_API_GA_MEASUREMENT_ID: $GA_MEASUREMENT_ID
STREAM_API_TOKEN_SECRET: $TOKEN_SECRET
PLAYER_BASE_URL: $BASE_URL
STREAM_API_PROXY_URL: $PROXY_URL
STREAM_API_PROXY_STREAMS: $PROXY_STREAMS
STREAM_API_ENABLE_DEBUG_ROUTE: $ENABLE_DEBUG_ROUTE
PLAYER_BYPASS_AUTH: $BYPASS_AUTH
STREAM_API_BYPASS_AUTH: $BYPASS_AUTH
AUTH_PROXY_VITE_BASE_URL: $AUTH_PROXY_BASE_URL
AUTH_PROXY_VITE_API_KEY: $AUTH_PROXY_API_KEY
AUTH_PROXY_DEV_BYPASS_AUTH: $DEV_BYPASS_AUTH
AUTH_PROXY_PROXY_VITE_API_KEY: $PROXY_VITE_API_KEY
GIT_TERMINAL_PROMPT: "0"
CARGO_HOME: "$CI_PROJECT_DIR/.cargo"
cache:
key:
files:
- package-lock.json
- src-tauri/Cargo.lock
paths:
- .cargo/
- src-tauri/target/
- node_modules/
policy: pull-push
script:
- npm ci
- |
$repos = @("frontend", "player", "stream-api", "auth-proxy")
$last = @{}
if (Test-Path "last-shas.json") {
$rawJson = Get-Content "last-shas.json" -Raw | ConvertFrom-Json
if ($rawJson) {
$rawJson.psobject.properties | ForEach-Object {
$last[$_.Name] = $_.Value
}
}
}
$current = @{}
$changed = $false
foreach ($repo in $repos) {
$headers = @{
"PRIVATE-TOKEN" = "$env:GITLAB_TOKEN"
}
$uri = "https://gitlab.com/api/v4/projects/vyla-entertainment%2F$repo/repository/commits/main"
try {
$resp = Invoke-RestMethod -Uri $uri -Headers $headers
$sha = $resp.id
$current[$repo] = $sha
if (-not $last.ContainsKey($repo) -or $last[$repo] -ne $sha) {
Write-Host "Change detected in $repo (main): $sha"
$changed = $true
}
else {
Write-Host "No change in $repo (main)"
}
}
catch {
Write-Warning "Failed to fetch commit info for $repo."
}
}
$current | ConvertTo-Json | Set-Content last-shas.json
if ($env:CI_PIPELINE_SOURCE -ne "schedule") {
$changed = $true
}
if (-not $changed) {
Write-Host "No changes detected. Skipping build."
exit 0
}
- Write-Host "Starting secure build process..."
- node build-pipeline.js
- |
if (Test-Path "update-payload.zip") {
Write-Host "Build artifact verified: update-payload.zip exists"
$size = (Get-Item update-payload.zip).Length
Write-Host " Size: $size bytes"
}
else {
Write-Error "Build artifact missing: update-payload.zip"
exit 1
}
if (Test-Path "manifest.json") {
Write-Host "Manifest file verified"
Get-Content manifest.json |
ConvertFrom-Json |
ConvertTo-Json -Depth 10
}
else {
Write-Error "Manifest file missing"
exit 1
}
if (Test-Path "validation-report.json") {
Write-Host "Validation report verified"
}
- Write-Host "Setting up Rust toolchain..."
- |
if (-not (Get-Command rustup -ErrorAction SilentlyContinue)) {
Invoke-WebRequest `
-Uri "https://win.rustup.rs/x86_64" `
-OutFile "rustup-init.exe"
.\rustup-init.exe -y --default-toolchain stable
Remove-Item "rustup-init.exe"
}
$env:Path = "$env:USERPROFILE\.cargo\bin;" + $env:Path
rustup default stable
rustc --version
cargo --version
- Write-Host "Packaging Tauri application..."
- npm run build
- |
Write-Host "Verifying Windows installers..."
$nsisDir = "src-tauri/target/release/bundle/nsis"
$msiDir = "src-tauri/target/release/bundle/msi"
$nsisFiles = @(Get-ChildItem "$nsisDir/*.exe" -ErrorAction SilentlyContinue)
$msiFiles = @(Get-ChildItem "$msiDir/*.msi" -ErrorAction SilentlyContinue)
if ($nsisFiles.Count -eq 0) {
Write-Error "No NSIS installer was found."
exit 1
}
if ($msiFiles.Count -eq 0) {
Write-Error "No MSI installer was found."
exit 1
}
Write-Host "NSIS installer:"
$nsisFiles | ForEach-Object {
Write-Host " $($_.FullName)"
Write-Host " Size: $($_.Length) bytes"
}
Write-Host "MSI installer:"
$msiFiles | ForEach-Object {
Write-Host " $($_.FullName)"
Write-Host " Size: $($_.Length) bytes"
}
- |
Write-Host "Saving build job information..."
@"
BUILD_JOB_ID=$env:CI_JOB_ID
BUILD_PIPELINE_ID=$env:CI_PIPELINE_ID
BUILD_PIPELINE_IID=$env:CI_PIPELINE_IID
BUILD_COMMIT_SHA=$env:CI_COMMIT_SHA
"@ | Set-Content build-info.env
Write-Host "Build job ID: $env:CI_JOB_ID"
Write-Host "Pipeline IID: $env:CI_PIPELINE_IID"
artifacts:
paths:
- update-payload.zip
- manifest.json
- validation-report.json
- build-info.env
- src-tauri/target/release/bundle/nsis/*.exe
- src-tauri/target/release/bundle/msi/*.msi
reports:
dotenv: build-info.env
expire_in: 30 days
release:
stage: release
tags:
- saas-windows-medium-amd64
needs:
- job: build
artifacts: true
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- |
Write-Host "Publishing Release to GitLab..."
if (-not $env:BUILD_JOB_ID) {
Write-Error "BUILD_JOB_ID is missing."
exit 1
}
Write-Host "Build job ID: $env:BUILD_JOB_ID"
$versionTag = "v$env:CI_PIPELINE_IID"
$exeItem = Get-ChildItem `
"src-tauri/target/release/bundle/nsis/*.exe" `
-ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $exeItem) {
Write-Error "No Windows NSIS installer was found in downloaded build artifacts."
exit 1
}
$exeName = $exeItem.Name
$exeEncoded = [System.Uri]::EscapeDataString($exeName)
Write-Host "Installer: $exeName"
$artifactBaseUrl = "$env:CI_PROJECT_URL/-/jobs/$env:BUILD_JOB_ID/artifacts/raw"
$exeUrl = "$artifactBaseUrl/src-tauri/target/release/bundle/nsis/$exeEncoded"
$payloadUrl = "$artifactBaseUrl/update-payload.zip"
$manifestUrl = "$artifactBaseUrl/manifest.json"
Write-Host "Installer URL: $exeUrl"
Write-Host "Payload URL: $payloadUrl"
Write-Host "Manifest URL: $manifestUrl"
$releaseBody = @{
name = "Vyla Home Release $versionTag"
tag_name = "$versionTag"
description = @"
Automated release for build $versionTag.
Windows installer:
$exeName
Build job:
$env:BUILD_JOB_ID
"@
ref = "$env:CI_COMMIT_SHA"
assets = @{
links = @(
@{
name = "$exeName"
url = "$exeUrl"
link_type = "package"
},
@{
name = "update-payload.zip"
url = "$payloadUrl"
link_type = "other"
},
@{
name = "manifest.json"
url = "$manifestUrl"
link_type = "other"
}
)
}
} | ConvertTo-Json -Depth 10
$releaseHeaders = @{
"JOB-TOKEN" = "$env:CI_JOB_TOKEN"
"Content-Type" = "application/json"
}
try {
$createdRelease = Invoke-RestMethod `
-Uri "https://gitlab.com/api/v4/projects/$env:CI_PROJECT_ID/releases" `
-Method Post `
-Headers $releaseHeaders `
-Body $releaseBody
Write-Host "Release successfully published: $($createdRelease.tag_name)"
Write-Host "Release URL: $($createdRelease._links.self)"
}
catch {
Write-Error "Release creation failed: $($_.Exception.Message)"
if ($_.ErrorDetails.Message) {
Write-Error $_.ErrorDetails.Message
}
exit 1
}
- Write-Host "Dependencies installed successfully."
- Write-Host "Releases and payload builds are now handled locally via 'npm run release' to save runner minutes."

13
DOCUEMENTATION/CLEAR.md Normal file
View File

@@ -0,0 +1,13 @@
taskkill /F /IM node.exe -ErrorAction SilentlyContinue
taskkill /F /IM vyla-home.exe -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:APPDATA\com.vyla.home" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:APPDATA\vyla-home" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\Downloads\vyla-*-debug*.log" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "src-tauri\target" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "dist-build" -ErrorAction SilentlyContinue
Remove-Item -Force "update-payload.zip" -ErrorAction SilentlyContinue
Remove-Item -Force "manifest.json" -ErrorAction SilentlyContinue
Remove-Item -Force "validation-report.json" -ErrorAction SilentlyContinue

View File

@@ -19,17 +19,8 @@ const REPO_CONFIG = {
'stream-api': { name: 'stream-api', branch: 'main' },
};
function getGitLabToken() {
const token = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
if (!token) {
throw new Error('GitLab token not found. Set GITLAB_TOKEN or VYLA_GITLAB_TOKEN environment variable.');
}
return token;
}
function clonePrivateRepo(repoConfig, targetDir) {
const token = getGitLabToken();
const repoUrl = `https://oauth2:${token}@gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
const repoUrl = `https://gitlab.com/${GITLAB_GROUP}/${repoConfig.name}.git`;
try {
if (fs.existsSync(targetDir)) {
@@ -37,8 +28,7 @@ function clonePrivateRepo(repoConfig, targetDir) {
}
execSync(`git clone --depth 1 --branch ${repoConfig.branch} ${repoUrl} ${targetDir}`, {
stdio: 'inherit',
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
stdio: 'inherit'
});
} catch (error) {

127
main.js
View File

@@ -6,6 +6,13 @@ const ServiceManager = require('./service-manager');
const Patcher = require('./patcher');
const UpdateClient = require('./update-client');
const debugLogPath = path.join(os.homedir(), 'Downloads', 'vyla-node-debug.log');
function debugLog(msg) {
try { fs.appendFileSync(debugLogPath, `[NODE] ${msg}\n`, 'utf8'); } catch (e) { }
}
debugLog("--- Node.js Process Started ---");
let serviceManager;
let expressApp;
let expressServer;
@@ -14,30 +21,32 @@ let playerExpressServer;
const APPDATA_DIR = process.env.APPDATA_PATH || path.join(os.homedir(), 'AppData', 'Roaming', 'vyla-home', 'vyla-home-files');
fs.mkdirSync(APPDATA_DIR, { recursive: true });
console.log(`[DEBUG] AppData directory: ${APPDATA_DIR}`);
debugLog(`AppData Dir: ${APPDATA_DIR}`);
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'));
const updateClient = new UpdateClient(APPDATA_DIR);
process.on('unhandledRejection', (reason) => {
const message = reason && reason.message ? reason.message : String(reason);
console.error(`[ERROR] Unhandled Rejection: ${message}`);
fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNHANDLED REJECTION: ${message}\n`, 'utf8');
});
process.on('uncaughtException', (err) => {
console.error(`[ERROR] Uncaught Exception: ${err.message}`);
fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNCAUGHT EXCEPTION: ${err.message}\n`, 'utf8');
});
function sendToTauri(event, data) {
debugLog(`Emitting to Tauri: [${event}] -> ${JSON.stringify(data)}`);
process.stdout.write(JSON.stringify({ event, data }) + '\n');
}
process.on('unhandledRejection', (reason) => {
const message = reason && reason.message ? reason.message : String(reason);
debugLog(`UNHANDLED REJECTION: ${message}`);
sendToTauri('error', { message });
});
process.on('uncaughtException', (err) => {
debugLog(`UNCAUGHT EXCEPTION: ${err.message}`);
sendToTauri('error', { message: err.message });
});
async function start() {
console.log('[DEBUG] Initializing patcher and service manager...');
debugLog("Inside start() function");
sendToTauri('status', { message: 'Initializing patcher...' });
const patcher = new Patcher(APPDATA_DIR);
serviceManager = new ServiceManager(APPDATA_DIR, envConfig);
@@ -49,17 +58,22 @@ async function start() {
while (attempts < 3 && !downloadSuccess) {
attempts++;
try {
console.log(`[DEBUG] Downloading update, attempt ${attempts}...`);
debugLog(`Attempting to download update (Attempt ${attempts})...`);
sendToTauri('status', { message: `Downloading update (Attempt ${attempts}/3)...` });
await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => {
const percent = Math.round((downloaded / total) * 100);
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
sendToTauri('download-progress', { downloaded, total, percent });
});
debugLog('Download complete. Verifying checksum...');
sendToTauri('status', { message: 'Verifying payload checksum...' });
if (!patcher.verifyChecksum(tempZipPath, release.sha256)) {
throw new Error('SHA-256 checksum verification failed.');
}
console.log('[DEBUG] Extracting payload and applying patches...');
debugLog('Checksum verified. Extracting...');
sendToTauri('status', { message: 'Extracting update files...' });
patcher.purgeOldCode();
patcher.extractZipPayload(tempZipPath);
@@ -67,14 +81,22 @@ async function start() {
throw new Error('Extraction verification failed: critical startup files missing.');
}
debugLog('Extraction complete. Applying patches...');
sendToTauri('status', { message: 'Applying patches & configuring...' });
patcher.applyPatches(replacementsConfig);
patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion(release.version, release.sha256);
fs.unlinkSync(tempZipPath);
if (fs.existsSync(tempZipPath)) {
fs.unlinkSync(tempZipPath);
}
downloadSuccess = true;
debugLog('Update installed successfully!');
} catch (err) {
if (fs.existsSync(tempZipPath)) fs.unlinkSync(tempZipPath);
debugLog(`Download/Install error on attempt ${attempts}: ${err.message}`);
if (fs.existsSync(tempZipPath)) {
try { fs.unlinkSync(tempZipPath); } catch (e) { }
}
if (attempts >= 3) {
throw new Error(`Failed to download and verify build after 3 attempts: ${err.message}`);
}
@@ -87,88 +109,95 @@ async function start() {
const isFirstRun = !patcher.verifyExtraction();
const localVersion = patcher.getCurrentVersion();
const localSha = patcher.getCurrentSha();
console.log(`[DEBUG] Local version: ${localVersion}, isFirstRun: ${isFirstRun}`);
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`);
sendToTauri('status', { message: 'Checking for updates...' });
let release = null;
try {
debugLog("Fetching updates from updateClient...");
release = await updateClient.checkForUpdates();
debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`);
} catch (e) {
console.warn('[DEBUG] Could not check for remote updates:', e.message);
debugLog(`Update check failed (offline?): ${e.message}`);
}
if (release) {
const hasUpdate = release.version !== localVersion || (release.sha256 && release.sha256 !== localSha);
if (isFirstRun || hasUpdate) {
console.log('[DEBUG] Performing update install from release...');
debugLog("Update required or first run. Installing remote build...");
await installBuild(release);
} else {
debugLog("Already on the latest version.");
}
} else if (isFirstRun) {
debugLog("No remote release found, but it's the first run. Looking for local update-payload.zip...");
const localZip = path.join(__dirname, 'update-payload.zip');
console.log(`[DEBUG] Checking local zip at: ${localZip} (exists: ${fs.existsSync(localZip)})`);
if (fs.existsSync(path.join(__dirname, 'update-payload.zip'))) {
const localZip = path.join(__dirname, 'update-payload.zip');
console.log('[DEBUG] Syncing local update-payload.zip to AppData...');
if (fs.existsSync(localZip)) {
sendToTauri('status', { message: 'Extracting initial local payload...' });
patcher.purgeOldCode();
patcher.extractZipPayload(localZip);
patcher.applyPatches(replacementsConfig);
patcher.bridgeCredentials(serviceManager.credentialManager);
patcher.saveCurrentVersion("1.0.0");
debugLog("Local payload extracted successfully.");
} else {
debugLog("No local payload found!");
}
}
const verified = patcher.verifyExtraction();
console.log(`[DEBUG] Payload verification status: ${verified}`);
if (!verified) {
throw new Error('No application files found. Build the payload first using "npm run build:payload" or check your internet connection.');
if (!patcher.verifyExtraction()) {
throw new Error('No application files found. Check your internet connection and try again.');
}
console.log('[DEBUG] Loading credentials...');
sendToTauri('status', { message: 'Loading credentials...' });
serviceManager.credentialManager.loadCredentials();
console.log('[DEBUG] Starting auth-proxy and stream-api...');
sendToTauri('status', { message: 'Starting backend services...' });
await serviceManager.startServices([
{ name: 'auth-proxy', relativeDir: 'auth-proxy' },
{ name: 'stream-api', relativeDir: 'stream-api' },
]);
debugLog("Backend services spawn requested.");
const playerDir = path.join(APPDATA_DIR, 'player');
console.log(`[DEBUG] Starting Player Express server on port 3001 from: ${playerDir}`);
playerExpressApp = express();
playerExpressApp.use(express.static(playerDir, { maxAge: '1d', etag: true }));
playerExpressApp.get('*', (req, res) => {
res.sendFile(path.join(playerDir, 'index.html'));
});
playerExpressServer = playerExpressApp.listen(3001, '0.0.0.0', () => {
console.log('[DEBUG] Player Express server listening on 3001');
});
playerExpressApp.get('*', (req, res) => { res.sendFile(path.join(playerDir, 'index.html')); });
playerExpressServer = playerExpressApp.listen(3001, '0.0.0.0');
debugLog("Player Express running on 3001");
console.log('[DEBUG] Waiting for ports [3000, 3001, 7860]...');
sendToTauri('status', { message: 'Waiting for ports 3000, 3001, 7860...' });
await serviceManager.waitForPorts([3000, 3001, 7860]);
console.log('[DEBUG] All ports ready!');
debugLog("Ports 3000, 3001, 7860 are ready.");
const frontendDir = path.join(APPDATA_DIR, 'frontend');
console.log(`[DEBUG] Starting Frontend Express server on port 4890 from: ${frontendDir}`);
expressApp = express();
expressApp.use(express.static(frontendDir, { maxAge: '1d', etag: true }));
expressApp.get('*', (req, res) => {
res.sendFile(path.join(frontendDir, 'index.html'));
});
expressApp.get('*', (req, res) => { res.sendFile(path.join(frontendDir, 'index.html')); });
const FRONTEND_PORT = 4890;
expressServer = expressApp.listen(FRONTEND_PORT, '0.0.0.0', () => {
console.log(`[DEBUG] Frontend Express server active at http://localhost:${FRONTEND_PORT}`);
debugLog("Frontend Express running on 4890. Sending READY to Tauri.");
sendToTauri('ready', { url: `http://localhost:${FRONTEND_PORT}` });
});
} catch (error) {
if (serviceManager) {
await serviceManager.stopAll().catch(() => { });
}
console.error('[DEBUG] Fatal error caught:', error.message);
debugLog(`Fatal Start Error: ${error.message}`);
sendToTauri('error', { message: error.message });
}
}
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) { }
}
}
}
});
process.on('SIGINT', async () => {
if (expressServer) expressServer.close();
if (playerExpressServer) playerExpressServer.close();

8
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@vyla-entertainment/sdk": "^1.2.0",
"@vyla-entertainment/sdk": "^1.10.0",
"adm-zip": "^0.6.0",
"express": "^4.19.2"
},
@@ -337,9 +337,9 @@
"license": "MIT"
},
"node_modules/@vyla-entertainment/sdk": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@vyla-entertainment/sdk/-/sdk-1.2.0.tgz",
"integrity": "sha512-s7jkF+cAm75W/9EZOfPo4LH2Fh3H9jrpAT1u+qsHi3L1yhjkRm2SnKeQBPMOt3hz1OttOxd62LP22iLBvUy7EQ==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@vyla-entertainment/sdk/-/sdk-1.10.0.tgz",
"integrity": "sha512-9UIBK850YEK+LkpNChYU/YsMatVuoqoMUOsE6HeKQCSys20/oe/0z/r1B2Vq7z1WFV4BNqE9GSeeT1nkjnc/zQ==",
"license": "CC-BY-NC-4.0"
},
"node_modules/accepts": {

View File

@@ -6,13 +6,15 @@
"scripts": {
"dev": "tauri dev",
"build:payload": "node build-pipeline.js",
"build": "tauri build"
"build": "tauri build",
"release": "node release.js",
"release:full": "node release.js --installer"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@vyla-entertainment/sdk": "^1.2.0",
"@vyla-entertainment/sdk": "^1.10.0",
"adm-zip": "^0.6.0",
"express": "^4.19.2"
},

View File

@@ -62,7 +62,16 @@ class Patcher {
for (const dir of dirs) {
const fullPath = path.join(this.appDataPath, dir);
if (fs.existsSync(fullPath)) {
fs.rmSync(fullPath, { recursive: true, force: true });
try {
fs.rmSync(fullPath, { recursive: true, force: true });
} catch (e) {
try {
const trashPath = fullPath + '.trash.' + Date.now();
fs.renameSync(fullPath, trashPath);
} catch (renameErr) {
console.warn(`[PATCHER] Could not remove or rename ${fullPath}. It will be overwritten.`);
}
}
}
}
}

164
release.js Normal file
View File

@@ -0,0 +1,164 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const { execSync } = require('child_process');
const GITLAB_PROJECT_PATH = 'vyla-entertainment%2Fhome';
const PROJECT_ID = process.env.CI_PROJECT_ID || GITLAB_PROJECT_PATH;
const TOKEN = process.env.GITLAB_TOKEN || process.env.VYLA_GITLAB_TOKEN;
if (!TOKEN) {
console.error('Error: GITLAB_TOKEN or VYLA_GITLAB_TOKEN environment variable is required.');
process.exit(1);
}
function getPackageVersion() {
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
return pkg.version || '1.0.0';
}
function uploadFileToGitLabPackage(filePath, packageName, version, fileName) {
return new Promise((resolve, reject) => {
const stat = fs.statSync(filePath);
const url = `https://gitlab.com/api/v4/projects/${PROJECT_ID}/packages/generic/${packageName}/${version}/${encodeURIComponent(fileName)}`;
const urlObj = new URL(url);
const req = https.request({
hostname: urlObj.hostname,
port: 443,
path: urlObj.pathname,
method: 'PUT',
headers: {
'PRIVATE-TOKEN': TOKEN,
'Content-Type': 'application/octet-stream',
'Content-Length': stat.size
}
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(url);
} else {
reject(new Error(`Failed to upload ${fileName} (HTTP ${res.statusCode}): ${body}`));
}
});
});
req.on('error', reject);
const fileStream = fs.createReadStream(filePath);
fileStream.on('error', reject);
fileStream.pipe(req);
});
}
function createGitLabRelease(tag, releaseLinks) {
return new Promise((resolve, reject) => {
let currentRef = 'main';
try {
currentRef = execSync('git rev-parse HEAD').toString().trim();
} catch (e) { }
const postData = JSON.stringify({
name: `Vyla Home Release ${tag}`,
tag_name: tag,
ref: currentRef,
description: `Automated local release ${tag}`,
assets: {
links: releaseLinks
}
});
const req = https.request({
hostname: 'gitlab.com',
port: 443,
path: `/api/v4/projects/${PROJECT_ID}/releases`,
method: 'POST',
headers: {
'PRIVATE-TOKEN': TOKEN,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode === 201 || res.statusCode === 200) {
resolve(JSON.parse(body));
} else {
reject(new Error(`Failed to create release (HTTP ${res.statusCode}): ${body}`));
}
});
});
req.on('error', reject);
req.write(postData);
req.end();
});
}
async function main() {
const isFullBuild = process.argv.includes('--installer');
const customTag = process.argv.find(arg => arg.startsWith('--tag='))?.split('=')[1];
const version = customTag || `v${getPackageVersion()}`;
console.log(`=== Building Vyla Home Release (${version}) ===`);
console.log('[1/4] Building update payload and manifest...');
execSync('node build-pipeline.js', { stdio: 'inherit' });
const payloadPath = path.join(__dirname, 'update-payload.zip');
const manifestPath = path.join(__dirname, 'manifest.json');
if (!fs.existsSync(payloadPath) || !fs.existsSync(manifestPath)) {
throw new Error('update-payload.zip or manifest.json not found.');
}
let installerPath = null;
let installerUploadName = null;
if (isFullBuild) {
console.log('[Optional] Compiling Windows Tauri installer locally...');
execSync('npm run build', { stdio: 'inherit' });
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'));
if (files.length > 0) {
const originalName = files[0];
installerPath = path.join(nsisDir, originalName);
installerUploadName = originalName.replace(/\s+/g, '-');
}
}
}
console.log('[2/4] Uploading artifacts to GitLab Package Registry...');
console.log('Uploading update-payload.zip...');
const payloadUrl = await uploadFileToGitLabPackage(payloadPath, 'vyla-home', version, 'update-payload.zip');
console.log('Uploading manifest.json...');
const manifestUrl = await uploadFileToGitLabPackage(manifestPath, 'vyla-home', version, 'manifest.json');
const releaseLinks = [
{ name: 'update-payload.zip', url: payloadUrl, link_type: 'other' },
{ name: 'manifest.json', url: manifestUrl, link_type: 'other' }
];
if (installerPath && installerUploadName) {
console.log(`Uploading installer ${installerUploadName}...`);
const installerUrl = await uploadFileToGitLabPackage(installerPath, 'vyla-home', version, installerUploadName);
releaseLinks.push({ name: installerUploadName, url: installerUrl, link_type: 'package' });
}
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})`);
}
main().catch(err => {
console.error('Release failed:', err.message);
process.exit(1);
});

View File

@@ -1,7 +1,6 @@
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const http = require('http');
const CredentialManager = require('./credential-manager');
class ServiceManager {
@@ -23,6 +22,8 @@ class ServiceManager {
const logPath = path.join(this.logsDir, `${name}.log`);
const timestamp = new Date().toISOString();
fs.appendFileSync(logPath, `[${timestamp}] ${line}\n`, 'utf8');
process.stdout.write(JSON.stringify({ event: "service-log", data: { service: name, message: line } }) + '\n');
} catch (e) {
}
}
@@ -39,10 +40,14 @@ class ServiceManager {
throw new Error(`Service script not found: ${scriptPath}`);
}
const tokenSecret = process.env.TOKEN_SECRET ||
let tokenSecret = process.env.TOKEN_SECRET ||
process.env.STREAM_API_TOKEN_SECRET ||
this.credentialManager.getCredential('stream-api', 'TOKEN_SECRET') ||
require('crypto').randomBytes(32).toString('hex');
this.credentialManager.getCredential('stream-api', 'TOKEN_SECRET');
if (!tokenSecret || tokenSecret.length < 32) {
tokenSecret = require('crypto').randomBytes(32).toString('hex');
this.credentialManager.setCredential('stream-api', 'TOKEN_SECRET', tokenSecret);
}
const env = {
...process.env,
@@ -77,13 +82,11 @@ class ServiceManager {
child.stdout.on('data', (data) => {
const text = data.toString().trim();
console.log(`[${name}] STDOUT: ${text}`);
this.logToFile(name, `STDOUT: ${text}`);
});
child.stderr.on('data', (data) => {
const text = data.toString().trim();
console.error(`[${name}] STDERR: ${text}`);
this.logToFile(name, `STDERR: ${text}`);
});
@@ -117,10 +120,6 @@ class ServiceManager {
));
}
async waitForPorts(ports, timeoutMs = 30000) {
await Promise.all(ports.map((port) => this.waitForPort(port, timeoutMs)));
}
async stopAll() {
this.isStopping = true;
for (const name of Object.keys(this.stableTimers)) {
@@ -146,6 +145,10 @@ class ServiceManager {
this.processes = {};
}
async waitForPorts(ports, timeoutMs = 30000) {
await Promise.all(ports.map((port) => this.waitForPort(port, timeoutMs)));
}
async waitForPort(port, timeoutMs = 30000) {
const start = Date.now();
const net = require('net');

View File

@@ -3,22 +3,61 @@
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::{Manager, State, WebviewUrl, WebviewWindowBuilder};
use tauri::{Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
fn debug_log(msg: &str) {
if let Ok(user_profile) = std::env::var("USERPROFILE") {
let log_path = std::path::Path::new(&user_profile).join("Downloads").join("vyla-rust-debug.log");
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&log_path) {
use std::io::Write;
let _ = writeln!(file, "[RUST] {}", msg);
}
}
}
struct NodeServices(Mutex<Option<Child>>);
#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, serde::Serialize, Clone)]
struct BackendMessage {
event: String,
data: serde_json::Value,
}
fn get_project_root() -> PathBuf {
// Helper to remove the \\?\ prefix that Node.js chokes on
fn clean_path(path: &std::path::Path) -> String {
let s = path.to_string_lossy().to_string();
if s.starts_with("\\\\?\\") {
s[4..].to_string()
} else {
s
}
}
fn resolve_script_and_root(app: &tauri::App) -> (String, String) {
if let Ok(resource_dir) = app.path().resource_dir() {
debug_log(&format!("Resource dir found: {:?}", resource_dir));
let candidates = vec![
resource_dir.join("main.js"),
resource_dir.join("_up_").join("main.js"),
];
for candidate in candidates {
if candidate.exists() {
debug_log(&format!("main.js found at: {:?}", candidate));
let root_dir = candidate.parent().unwrap();
return (clean_path(&candidate), clean_path(root_dir));
}
}
}
let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if dir.ends_with("src-tauri") {
dir.pop();
}
dir
let candidate = dir.join("main.js");
debug_log(&format!("Fallback script path: {:?}", candidate));
(clean_path(&candidate), clean_path(&dir))
}
const LINK_HANDLER_SCRIPT: &str = r#"
@@ -54,33 +93,39 @@ const LINK_HANDLER_SCRIPT: &str = r#"
"#;
fn main() {
debug_log("=== Vyla Home Executable Started ===");
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.manage(NodeServices(Mutex::new(None)))
.setup(|app| {
debug_log("Tauri setup started");
let app_data = app.path().app_data_dir().unwrap_or_default();
let app_data_str = app_data.to_string_lossy().to_string();
let app_data_str = clean_path(&app_data);
debug_log(&format!("AppData path: {}", app_data_str));
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
.title("Vyla Home")
.inner_size(1280.0, 800.0)
.decorations(true)
.resizable(true)
.fullscreen(false)
.initialization_script(LINK_HANDLER_SCRIPT)
.build()?;
debug_log("Webview created");
let root_dir = get_project_root();
let main_js = root_dir.join("main.js");
let (main_js, root_dir) = resolve_script_and_root(app);
debug_log(&format!("Spawning node with script: {}", main_js));
let mut cmd = Command::new("node");
cmd.arg(&main_js)
.current_dir(&root_dir)
.env("APPDATA_PATH", &app_data_str)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
.stderr(Stdio::piped());
#[cfg(windows)]
{
@@ -89,29 +134,45 @@ fn main() {
cmd.creation_flags(CREATE_NO_WINDOW);
}
if let Ok(mut child) = cmd.spawn() {
if let Some(stdout) = child.stdout.take() {
let w = window.clone();
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
let reader = BufReader::new(stdout);
for line in reader.lines().flatten() {
println!("{}", line);
if let Ok(msg) = serde_json::from_str::<BackendMessage>(&line) {
if msg.event == "ready" {
if let Some(url) = msg.data.get("url").and_then(|u| u.as_str()) {
let _ = w.eval(&format!("window.location.href = '{}';", url));
match cmd.spawn() {
Ok(mut child) => {
debug_log("Node spawned successfully");
if let Some(stdout) = child.stdout.take() {
let w = window.clone();
std::thread::spawn(move || {
use std::io::{BufRead, BufReader};
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) {
let _ = w.emit(&msg.event, msg.data.clone());
if msg.event == "ready" {
if let Some(url) = msg.data.get("url").and_then(|u| u.as_str()) {
let _ = w.eval(&format!("window.location.href = '{}';", url));
}
}
}
}
}
});
}
});
}
if let Some(stderr) = child.stderr.take() {
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));
}
});
}
let state: State<NodeServices> = app.state();
if let Ok(mut lock) = state.0.lock() {
*lock = Some(child);
};
let state: State<NodeServices> = app.state();
if let Ok(mut lock) = state.0.lock() {
*lock = Some(child);
};
}
Err(e) => {
debug_log(&format!("FAILED to spawn Node: {}", e));
}
}
Ok(())

View File

@@ -21,6 +21,16 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"../main.js",
"../service-manager.js",
"../patcher.js",
"../crypto-utils.js",
"../credential-manager.js",
"../update-client.js",
"../config/**/*",
"../node_modules/**/*"
]
}
}

View File

@@ -14,97 +14,44 @@
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif;
overflow: hidden;
}
canvas {
position: fixed;
inset: 0;
display: block;
width: 100vw;
height: 100vh;
}
.overlay {
position: fixed;
inset: 0;
body {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
.view-state {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
pointer-events: none;
width: 100%;
animation: fadeIn 0.4s ease forwards;
}
#ascii-logo {
font-family: "Cascadia Mono", "Consolas", monospace;
font-size: 11px;
line-height: 1.15;
color: rgba(255, 255, 255, .75);
white-space: pre;
margin: 0;
user-select: none;
text-shadow: 0 4px 12px rgba(0, 0, 0, .6);
transform: scale(0.4);
opacity: 0;
animation: logoIn 1.1s cubic-bezier(.16, 1, .3, 1) forwards;
.view-state.active {
display: flex;
}
#status {
font-size: 10px;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
color: rgba(255, 255, 255, .4);
opacity: 0;
animation: statusIn .6s ease forwards .7s;
}
@keyframes logoIn {
0% {
transform: scale(0.4);
opacity: 0;
filter: blur(6px);
}
60% {
opacity: 1;
filter: blur(0);
}
100% {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}
@keyframes statusIn {
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(6px);
transform: scale(0.98);
}
to {
opacity: 1;
transform: translateY(0);
transform: scale(1);
}
}
.error-view {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: #000;
padding: 32px;
z-index: 100;
}
.container {
max-width: 480px;
text-align: center;
@@ -114,9 +61,18 @@
width: 38px;
height: 38px;
margin: 0 auto 22px;
}
.icon-error {
color: #ff453a;
}
.icon-loader {
color: #ffffff;
width: 45px;
height: 45px;
}
h1 {
margin: 0;
font-size: 22px;
@@ -133,136 +89,620 @@
word-break: break-word;
}
.buttons {
display: flex;
gap: 10px;
justify-content: center;
}
button {
appearance: none;
border: 0;
border-radius: 999px;
background: #fff;
color: #000;
padding: 10px 18px;
border-radius: 8px;
padding: 11px 20px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: opacity .15s ease, transform .15s ease;
min-width: 120px;
}
button:hover:not(:disabled) {
opacity: .85;
}
button:active:not(:disabled) {
transform: scale(.97);
}
button:disabled {
cursor: default;
opacity: .35;
}
.primary {
background: #fff;
color: #000;
}
.secondary {
background: rgba(255, 255, 255, .1);
color: #fff;
}
.progress-wrap {
width: 100%;
max-width: 240px;
height: 4px;
background: rgba(255, 255, 255, 0.15);
border-radius: 999px;
margin: 0 auto 28px;
overflow: hidden;
display: none;
}
.progress-fill {
width: 0%;
height: 100%;
background: #0a84ff;
transition: width 0.2s ease;
}
.box5631 {
transform-origin: 50% 50%;
fill: currentColor;
}
@keyframes moveBox5631-1 {
9.09% {
transform: translate(-12px, 0);
}
18.18% {
transform: translate(0px, 0);
}
27.27% {
transform: translate(0px, 0);
}
36.36% {
transform: translate(12px, 0);
}
45.45% {
transform: translate(12px, 12px);
}
54.54% {
transform: translate(12px, 12px);
}
63.63% {
transform: translate(12px, 12px);
}
72.73% {
transform: translate(12px, 0px);
}
81.81% {
transform: translate(0px, 0px);
}
90.90% {
transform: translate(-12px, 0px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(1) {
animation: moveBox5631-1 4s infinite;
}
@keyframes moveBox5631-2 {
9.09% {
transform: translate(0, 0);
}
18.18% {
transform: translate(12px, 0);
}
27.27% {
transform: translate(0px, 0);
}
36.36% {
transform: translate(12px, 0);
}
45.45% {
transform: translate(12px, 12px);
}
54.54% {
transform: translate(12px, 12px);
}
63.63% {
transform: translate(12px, 12px);
}
72.73% {
transform: translate(12px, 12px);
}
81.81% {
transform: translate(0px, 12px);
}
90.90% {
transform: translate(0px, 12px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(2) {
animation: moveBox5631-2 4s infinite;
}
@keyframes moveBox5631-3 {
9.09% {
transform: translate(-12px, 0);
}
18.18% {
transform: translate(-12px, 0);
}
27.27% {
transform: translate(0px, 0);
}
36.36% {
transform: translate(-12px, 0);
}
45.45% {
transform: translate(-12px, 0);
}
54.54% {
transform: translate(-12px, 0);
}
63.63% {
transform: translate(-12px, 0);
}
72.73% {
transform: translate(-12px, 0);
}
81.81% {
transform: translate(-12px, -12px);
}
90.90% {
transform: translate(0px, -12px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(3) {
animation: moveBox5631-3 4s infinite;
}
@keyframes moveBox5631-4 {
9.09% {
transform: translate(-12px, 0);
}
18.18% {
transform: translate(-12px, 0);
}
27.27% {
transform: translate(-12px, -12px);
}
36.36% {
transform: translate(0px, -12px);
}
45.45% {
transform: translate(0px, 0px);
}
54.54% {
transform: translate(0px, -12px);
}
63.63% {
transform: translate(0px, -12px);
}
72.73% {
transform: translate(0px, -12px);
}
81.81% {
transform: translate(-12px, -12px);
}
90.90% {
transform: translate(-12px, 0px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(4) {
animation: moveBox5631-4 4s infinite;
}
@keyframes moveBox5631-5 {
9.09% {
transform: translate(0, 0);
}
18.18% {
transform: translate(0, 0);
}
27.27% {
transform: translate(0, 0);
}
36.36% {
transform: translate(12px, 0);
}
45.45% {
transform: translate(12px, 0);
}
54.54% {
transform: translate(12px, 0);
}
63.63% {
transform: translate(12px, 0);
}
72.73% {
transform: translate(12px, 0);
}
81.81% {
transform: translate(12px, -12px);
}
90.90% {
transform: translate(0px, -12px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(5) {
animation: moveBox5631-5 4s infinite;
}
@keyframes moveBox5631-6 {
9.09% {
transform: translate(0, 0);
}
18.18% {
transform: translate(-12px, 0);
}
27.27% {
transform: translate(-12px, 0);
}
36.36% {
transform: translate(0px, 0);
}
45.45% {
transform: translate(0px, 0);
}
54.54% {
transform: translate(0px, 0);
}
63.63% {
transform: translate(0px, 0);
}
72.73% {
transform: translate(0px, 12px);
}
81.81% {
transform: translate(-12px, 12px);
}
90.90% {
transform: translate(-12px, 0px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(6) {
animation: moveBox5631-6 4s infinite;
}
@keyframes moveBox5631-7 {
9.09% {
transform: translate(12px, 0);
}
18.18% {
transform: translate(12px, 0);
}
27.27% {
transform: translate(12px, 0);
}
36.36% {
transform: translate(0px, 0);
}
45.45% {
transform: translate(0px, -12px);
}
54.54% {
transform: translate(12px, -12px);
}
63.63% {
transform: translate(0px, -12px);
}
72.73% {
transform: translate(0px, -12px);
}
81.81% {
transform: translate(0px, 0px);
}
90.90% {
transform: translate(12px, 0px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(7) {
animation: moveBox5631-7 4s infinite;
}
@keyframes moveBox5631-8 {
9.09% {
transform: translate(0, 0);
}
18.18% {
transform: translate(-12px, 0);
}
27.27% {
transform: translate(-12px, -12px);
}
36.36% {
transform: translate(0px, -12px);
}
45.45% {
transform: translate(0px, -12px);
}
54.54% {
transform: translate(0px, -12px);
}
63.63% {
transform: translate(0px, -12px);
}
72.73% {
transform: translate(0px, -12px);
}
81.81% {
transform: translate(12px, -12px);
}
90.90% {
transform: translate(12px, 0px);
}
100% {
transform: translate(0px, 0px);
}
}
.box5631:nth-child(8) {
animation: moveBox5631-8 4s infinite;
}
@keyframes moveBox5631-9 {
9.09% {
transform: translate(-12px, 0);
}
18.18% {
transform: translate(-12px, 0);
}
27.27% {
transform: translate(0px, 0);
}
36.36% {
transform: translate(-12px, 0);
}
45.45% {
transform: translate(0px, 0);
}
54.54% {
transform: translate(0px, 0);
}
63.63% {
transform: translate(-12px, 0);
}
72.73% {
transform: translate(-12px, 0);
}
81.81% {
transform: translate(-24px, 0);
}
90.90% {
transform: translate(-12px, 0);
}
100% {
transform: translate(0px, 0);
}
}
.box5631:nth-child(9) {
animation: moveBox5631-9 4s infinite;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="view-loader" class="view-state active">
<div class="container">
<svg class="icon icon-loader" viewBox="-13 -13 45 45" xmlns="http://www.w3.org/2000/svg">
<g>
<circle class="box5631" cx="13" cy="1" r="5" />
<circle class="box5631" cx="13" cy="1" r="5" />
<circle class="box5631" cx="25" cy="25" r="5" />
<circle class="box5631" cx="13" cy="13" r="5" />
<circle class="box5631" cx="13" cy="13" r="5" />
<circle class="box5631" cx="25" cy="13" r="5" />
<circle class="box5631" cx="1" cy="25" r="5" />
<circle class="box5631" cx="13" cy="25" r="5" />
<circle class="box5631" cx="25" cy="25" r="5" />
</g>
</svg>
<div class="overlay">
<pre id="ascii-logo"></pre>
<div id="status">Starting Vyla Home...</div>
<h1>Starting Vyla Home</h1>
<p id="status-msg">Initializing services...</p>
<div id="progress-wrap" class="progress-wrap">
<div id="progress-fill" class="progress-fill"></div>
</div>
</div>
</div>
<div id="error-view" class="error-view">
<div id="view-error" class="view-state">
<div class="container">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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>
<button onclick="location.reload()">Try Again</button>
<div class="buttons">
<button class="primary" onclick="location.reload()">Try Again</button>
</div>
</div>
</div>
<script>
fetch('logo.txt').then(r => r.text()).then(t => {
document.getElementById('ascii-logo').textContent = t;
}).catch(() => { });
const viewLoader = document.getElementById('view-loader');
const viewError = document.getElementById('view-error');
const CHARS = ' .:-=+*#%@';
const FONT_SIZE = 50;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const statusMsg = document.getElementById('status-msg');
const errorMsg = document.getElementById('error-msg');
let CW, CH, cols, rows, t = 0;
let mouseX = -9999, mouseY = -9999, lastMouseMove = 0, mouseStrength = 0;
const FADE_TIME = 250, SMOOTHING = 0.08;
const progressWrap = document.getElementById('progress-wrap');
const progressFill = document.getElementById('progress-fill');
function initFont() {
ctx.font = FONT_SIZE + 'px "Courier New", monospace';
CW = Math.ceil(ctx.measureText('M').width);
CH = FONT_SIZE + 2;
ctx.textBaseline = 'top';
function showView(viewElement) {
document.querySelectorAll('.view-state').forEach(el => el.classList.remove('active'));
viewElement.classList.add('active');
}
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initFont();
cols = Math.ceil(canvas.width / CW) + 1;
rows = Math.ceil(canvas.height / CH) + 1;
}
window.addEventListener('resize', resize);
window.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
lastMouseMove = performance.now();
});
function plasma(cx, cy, tt) {
const x = cx + Math.sin(cy * 3 + tt * 0.7) * 0.07;
const y = cy + Math.cos(cx * 3 + tt * 0.5) * 0.07;
let v =
Math.sin(x * 10 + tt) +
Math.sin(y * 10 + tt) +
Math.sin((x + y) * 7 + tt) +
Math.sin(Math.sqrt(x * x + y * y) * 12 - tt * 1.2) +
Math.sin(x * 6 - tt * 0.8) +
Math.sin(y * 8 + tt * 0.9);
return (v / 6 + 1) / 2;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
t += 0.03;
const elapsed = performance.now() - lastMouseMove;
const targetStrength = Math.max(0, 1 - elapsed / FADE_TIME);
mouseStrength += (targetStrength - mouseStrength) * SMOOTHING;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * CW;
const y = row * CH;
const dx = x - mouseX;
const dy = y - mouseY;
const dist = Math.sqrt(dx * dx + dy * dy);
const force = (7000 / (dist * dist + 1500)) * mouseStrength;
const v = plasma(col / cols, row / rows, t);
const brightness = Math.round(v * 60);
const charIdx = Math.floor(v * (CHARS.length - 0.001));
ctx.fillStyle = 'rgb(' + brightness + ',' + brightness + ',' + brightness + ')';
ctx.fillText(CHARS[charIdx], x + dx * force, y + dy * force);
if (window.__TAURI__ && window.__TAURI__.event) {
window.__TAURI__.event.listen('status', (e) => {
if (e.payload && e.payload.message) {
statusMsg.textContent = e.payload.message;
}
}
requestAnimationFrame(render);
});
window.__TAURI__.event.listen('download-progress', (e) => {
if (e.payload) {
statusMsg.textContent = `Downloading update: ${e.payload.percent}%`;
progressWrap.style.display = 'block';
progressFill.style.width = `${e.payload.percent}%`;
}
});
window.__TAURI__.event.listen('error', (e) => {
if (e.payload && e.payload.message) {
errorMsg.textContent = e.payload.message;
showView(viewError);
}
});
window.__TAURI__.event.listen('ready', (e) => {
if (e.payload && e.payload.url) {
window.location.href = e.payload.url;
}
});
}
resize();
render();
function checkBackend() {
function pollBackend() {
fetch('http://localhost:4890')
.then(res => {
if (res.ok) {
window.location.href = 'http://localhost:4890';
} else {
setTimeout(checkBackend, 1000);
}
if (res.ok) window.location.href = 'http://localhost:4890';
else setTimeout(pollBackend, 1000);
})
.catch(() => {
setTimeout(checkBackend, 1000);
});
.catch(() => setTimeout(pollBackend, 1000));
}
setTimeout(checkBackend, 1000);
setTimeout(pollBackend, 1500);
</script>
</body>