mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-12 05:07:21 -05:00
Completely rewrote the code, home builds locally rather on Gitlab, releases are done manually aswell
This commit is contained in:
328
.gitlab-ci.yml
328
.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
|
||||
}
|
||||
- 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
13
DOCUEMENTATION/CLEAR.md
Normal 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
|
||||
@@ -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
127
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();
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
11
patcher.js
11
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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
release.js
Normal file
164
release.js
Normal 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);
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
760
ui/index.html
760
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;
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user