From cee165ddfa586809d39ee4db4c437b16ebcf2f31 Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Fri, 21 Aug 2026 14:32:59 -0700 Subject: [PATCH] Completely rewrote the code, home builds locally rather on Gitlab, releases are done manually aswell --- .gitlab-ci.yml | 328 +--------------- DOCUEMENTATION/CLEAR.md | 13 + build-pipeline.js | 14 +- main.js | 127 ++++--- package-lock.json | 8 +- package.json | 6 +- patcher.js | 11 +- release.js | 164 ++++++++ service-manager.js | 23 +- src-tauri/src/main.rs | 117 ++++-- src-tauri/tauri.conf.json | 10 + ui/index.html | 760 ++++++++++++++++++++++++++++++-------- 12 files changed, 993 insertions(+), 588 deletions(-) create mode 100644 DOCUEMENTATION/CLEAR.md create mode 100644 release.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bebbe9..d8a4525 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 - } \ No newline at end of file + - Write-Host "Dependencies installed successfully." + - Write-Host "Releases and payload builds are now handled locally via 'npm run release' to save runner minutes." \ No newline at end of file diff --git a/DOCUEMENTATION/CLEAR.md b/DOCUEMENTATION/CLEAR.md new file mode 100644 index 0000000..62603ff --- /dev/null +++ b/DOCUEMENTATION/CLEAR.md @@ -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 \ No newline at end of file diff --git a/build-pipeline.js b/build-pipeline.js index b19ad9b..4cd5750 100644 --- a/build-pipeline.js +++ b/build-pipeline.js @@ -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) { diff --git a/main.js b/main.js index c832b13..c0fdab7 100644 --- a/main.js +++ b/main.js @@ -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(); diff --git a/package-lock.json b/package-lock.json index 9a3792f..96dae2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 93a7648..27172df 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/patcher.js b/patcher.js index e060432..1623cb0 100644 --- a/patcher.js +++ b/patcher.js @@ -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.`); + } + } } } } diff --git a/release.js b/release.js new file mode 100644 index 0000000..a1f0897 --- /dev/null +++ b/release.js @@ -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); +}); \ No newline at end of file diff --git a/service-manager.js b/service-manager.js index 208918e..b66f715 100644 --- a/service-manager.js +++ b/service-manager.js @@ -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'); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8e18ed5..7a74839 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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>); -#[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::(&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::(&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 = app.state(); - if let Ok(mut lock) = state.0.lock() { - *lock = Some(child); - }; + let state: State = app.state(); + if let Ok(mut lock) = state.0.lock() { + *lock = Some(child); + }; + } + Err(e) => { + debug_log(&format!("FAILED to spawn Node: {}", e)); + } } Ok(()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 736bb52..dd8f3e1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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/**/*" ] } } \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index 038a91b..f5af64a 100644 --- a/ui/index.html +++ b/ui/index.html @@ -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; } - +
+
+ + + + + + + + + + + + + -
- -
Starting Vyla Home...
+

Starting Vyla Home

+

Initializing services...

+ +
+
+
+
-
+
- + +

Couldn't start Vyla Home

- + +
+ +