mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-14 14:17:25 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c6a9d1e5b | |||
| 609891f478 | |||
| b201510b5a | |||
| acd46d665b | |||
| f07c0ea0fd | |||
| aaa0df5f17 | |||
| 08950e101f | |||
| f8aa86eecf | |||
| 587d70c46a |
@@ -24,14 +24,14 @@ Fixing os error 32
|
|||||||
Stop-Process -Name "vyla-home", "node", "cargo" -Force -ErrorAction SilentlyContinue
|
Stop-Process -Name "vyla-home", "node", "cargo" -Force -ErrorAction SilentlyContinue
|
||||||
```
|
```
|
||||||
|
|
||||||
Fake a test?
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:VYLA_TEST_UI="1"; npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Fake an old local version:
|
Fake an old local version:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
'{"version":"0.0.1","sha256":null}' | Set-Content "$env:APPDATA\vyla-home\vyla-home-files\version.json"
|
'{"version":"0.0.1","sha256":null}' | Set-Content "$env:APPDATA\vyla-home\vyla-home-files\version.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Fake a test?
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:VYLA_TEST_UI="1"; npm run dev
|
||||||
|
```
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ function validatePayload(zipPath) {
|
|||||||
const requiredFiles = [
|
const requiredFiles = [
|
||||||
'frontend/index.html'
|
'frontend/index.html'
|
||||||
];
|
];
|
||||||
|
|
||||||
const validationResults = {
|
const validationResults = {
|
||||||
valid: true,
|
valid: true,
|
||||||
errors: [],
|
errors: [],
|
||||||
|
|||||||
253
file-updater.js
253
file-updater.js
@@ -9,6 +9,9 @@ const SDK_NAME = '@vyla-entertainment/sdk';
|
|||||||
const SDK_LATEST_URL = 'https://registry.npmjs.org/@vyla-entertainment%2Fsdk/latest';
|
const SDK_LATEST_URL = 'https://registry.npmjs.org/@vyla-entertainment%2Fsdk/latest';
|
||||||
const ALLOWED_ROOTS = ['frontend', 'player'];
|
const ALLOWED_ROOTS = ['frontend', 'player'];
|
||||||
const MAX_TARBALL = 100 * 1024 * 1024;
|
const MAX_TARBALL = 100 * 1024 * 1024;
|
||||||
|
const MAX_FILE = 50 * 1024 * 1024;
|
||||||
|
const LIVE_HOSTS = ['gitlab.com'];
|
||||||
|
const REPO_API = 'https://gitlab.com/api/v4/projects/vyla-entertainment%2F';
|
||||||
|
|
||||||
const verParts = (v) => String(v || '').replace(/^v/, '').split(/[.\-+]/).map((n) => parseInt(n, 10) || 0);
|
const verParts = (v) => String(v || '').replace(/^v/, '').split(/[.\-+]/).map((n) => parseInt(n, 10) || 0);
|
||||||
const cmpVer = (a, b) => {
|
const cmpVer = (a, b) => {
|
||||||
@@ -22,8 +25,11 @@ const cmpVer = (a, b) => {
|
|||||||
|
|
||||||
const sha256Buf = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
const sha256Buf = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
||||||
const sha256File = (p) => sha256Buf(fs.readFileSync(p));
|
const sha256File = (p) => sha256Buf(fs.readFileSync(p));
|
||||||
|
const gitBlobSha = (buf) => crypto.createHash('sha1').update(Buffer.concat([Buffer.from(`blob ${buf.length}\0`), buf])).digest('hex');
|
||||||
|
|
||||||
function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0 }) {
|
const hooks = { notice: null };
|
||||||
|
|
||||||
|
function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0, retries = 0 }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (redirects > 5) return reject(new Error('Too many redirects'));
|
if (redirects > 5) return reject(new Error('Too many redirects'));
|
||||||
let u;
|
let u;
|
||||||
@@ -44,6 +50,15 @@ function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0 }) {
|
|||||||
fetchBuffer(next, { headers: keep, maxBytes, hosts, redirects: redirects + 1 }).then(resolve, reject);
|
fetchBuffer(next, { headers: keep, maxBytes, hosts, redirects: redirects + 1 }).then(resolve, reject);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (res.statusCode === 429 && retries < 4) {
|
||||||
|
res.resume();
|
||||||
|
const wait = Math.min(parseInt(res.headers['retry-after'], 10) || 5, 30) * 1000;
|
||||||
|
if (hooks.notice) hooks.notice(`GitLab rate limit (HTTP 429). Retrying in ${Math.round(wait / 1000)}s (${retries + 1}/4)...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchBuffer(url, { headers, maxBytes, hosts, redirects, retries: retries + 1 }).then(resolve, reject);
|
||||||
|
}, wait);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
res.resume();
|
res.resume();
|
||||||
reject(new Error(`HTTP ${res.statusCode}`));
|
reject(new Error(`HTTP ${res.statusCode}`));
|
||||||
@@ -114,16 +129,24 @@ class FileUpdater {
|
|||||||
this.appDataPath = appDataPath;
|
this.appDataPath = appDataPath;
|
||||||
this.updateClient = updateClient;
|
this.updateClient = updateClient;
|
||||||
this.onProgress = null;
|
this.onProgress = null;
|
||||||
this.pending = null;
|
|
||||||
this.statePath = path.join(appDataPath, 'file-state.json');
|
this.statePath = path.join(appDataPath, 'file-state.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set onNotice(fn) {
|
||||||
|
hooks.notice = fn;
|
||||||
|
}
|
||||||
|
|
||||||
sdkHeaders() {
|
sdkHeaders() {
|
||||||
const h = { Accept: 'application/json' };
|
const h = { Accept: 'application/json' };
|
||||||
if (process.env.NPM_TOKEN) h.Authorization = `Bearer ${process.env.NPM_TOKEN}`;
|
if (process.env.NPM_TOKEN) h.Authorization = `Bearer ${process.env.NPM_TOKEN}`;
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveHeaders(extra = {}) {
|
||||||
|
const t = this.updateClient && this.updateClient.token;
|
||||||
|
return t ? { ...extra, 'PRIVATE-TOKEN': t } : extra;
|
||||||
|
}
|
||||||
|
|
||||||
target(rel) {
|
target(rel) {
|
||||||
const root = path.resolve(this.appDataPath);
|
const root = path.resolve(this.appDataPath);
|
||||||
const full = path.resolve(root, ...rel.split('/'));
|
const full = path.resolve(root, ...rel.split('/'));
|
||||||
@@ -136,17 +159,28 @@ class FileUpdater {
|
|||||||
const s = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
|
const s = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
|
||||||
return {
|
return {
|
||||||
seq: typeof s.seq === 'number' ? s.seq : null,
|
seq: typeof s.seq === 'number' ? s.seq : null,
|
||||||
base: s.base && typeof s.base === 'object' ? s.base : {}
|
base: s.base && typeof s.base === 'object' ? s.base : {},
|
||||||
|
remote: s.remote && typeof s.remote === 'object' ? s.remote : {},
|
||||||
|
local: s.local && typeof s.local === 'object' ? s.local : {},
|
||||||
|
commits: s.commits && typeof s.commits === 'object' ? s.commits : {}
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { seq: null, base: {} };
|
return { seq: null, base: {}, remote: {}, local: {}, commits: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeState(state) {
|
writeState(state) {
|
||||||
|
const cur = this.readState();
|
||||||
|
const next = {
|
||||||
|
seq: state.seq !== undefined ? state.seq : cur.seq,
|
||||||
|
base: state.base || cur.base,
|
||||||
|
remote: state.remote || cur.remote,
|
||||||
|
local: state.local || cur.local,
|
||||||
|
commits: state.commits || cur.commits
|
||||||
|
};
|
||||||
fs.mkdirSync(this.appDataPath, { recursive: true });
|
fs.mkdirSync(this.appDataPath, { recursive: true });
|
||||||
const tmp = `${this.statePath}.tmp`;
|
const tmp = `${this.statePath}.tmp`;
|
||||||
fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
|
fs.writeFileSync(tmp, JSON.stringify(next), 'utf8');
|
||||||
fs.renameSync(tmp, this.statePath);
|
fs.renameSync(tmp, this.statePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +200,7 @@ class FileUpdater {
|
|||||||
const dir = path.join(this.appDataPath, root);
|
const dir = path.join(this.appDataPath, root);
|
||||||
if (fs.existsSync(dir)) walk(dir, root);
|
if (fs.existsSync(dir)) walk(dir, root);
|
||||||
}
|
}
|
||||||
this.writeState({ seq: this.readState().seq, base });
|
this.writeState({ seq: this.readState().seq, base, remote: {}, local: {}, commits: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
zipMatches(zipPath, sha256) {
|
zipMatches(zipPath, sha256) {
|
||||||
@@ -296,6 +330,213 @@ class FileUpdater {
|
|||||||
return changed ? Buffer.from(text, 'utf8') : buf;
|
return changed ? Buffer.from(text, 'utf8') : buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async repoTree(root) {
|
||||||
|
const out = [];
|
||||||
|
for (let page = 1; page <= 50; page++) {
|
||||||
|
const url = `${REPO_API}${root}/repository/tree?recursive=true&per_page=100&ref=main&page=${page}`;
|
||||||
|
return fetchBuffer(`${REPO_API}${root}/repository/files/${file}/raw?ref=main`, { headers: this.liveHeaders(), maxBytes: MAX_FILE, hosts: LIVE_HOSTS });
|
||||||
|
const items = JSON.parse(raw.toString('utf8'));
|
||||||
|
if (!Array.isArray(items) || items.length === 0) break;
|
||||||
|
out.push(...items);
|
||||||
|
if (items.length < 100) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadArchive(root) {
|
||||||
|
const buf = await fetchBuffer(`${REPO_API}${root}/repository/archive.zip?sha=main`, { headers: this.liveHeaders(), maxBytes: 600 * 1024 * 1024, hosts: LIVE_HOSTS });
|
||||||
|
const zip = new AdmZip(buf);
|
||||||
|
const map = new Map();
|
||||||
|
for (const entry of zip.getEntries()) {
|
||||||
|
if (entry.isDirectory) continue;
|
||||||
|
const name = entry.entryName;
|
||||||
|
const i = name.indexOf('/');
|
||||||
|
if (i < 0) continue;
|
||||||
|
map.set(`${root}/${name.slice(i + 1)}`, entry);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRepoFile(rel) {
|
||||||
|
const hit = this.archive && this.archive.get(rel);
|
||||||
|
if (hit) return Promise.resolve(hit.getData());
|
||||||
|
const parts = rel.split('/');
|
||||||
|
const root = parts.shift();
|
||||||
|
const file = encodeURIComponent(parts.join('/'));
|
||||||
|
return fetchBuffer(`${REPO_API}${root}/repository/files/${file}/raw?ref=main`, { maxBytes: MAX_FILE, hosts: LIVE_HOSTS });
|
||||||
|
}
|
||||||
|
|
||||||
|
async headCommit(root) {
|
||||||
|
try {
|
||||||
|
const raw = await fetchBuffer(`${REPO_API}${root}/repository/commits/main`, {
|
||||||
|
headers: this.liveHeaders({ Accept: 'application/json' }),
|
||||||
|
maxBytes: 1024 * 1024,
|
||||||
|
hosts: LIVE_HOSTS,
|
||||||
|
retries: 3
|
||||||
|
});
|
||||||
|
const id = JSON.parse(raw.toString('utf8')).id;
|
||||||
|
return /^[0-9a-f]{40}$/.test(id) ? id : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localIntact(root, state) {
|
||||||
|
const prefix = `${root}/`;
|
||||||
|
const keys = Object.keys(state.remote).filter((k) => k.startsWith(prefix));
|
||||||
|
if (keys.length === 0) return false;
|
||||||
|
for (const rel of keys) {
|
||||||
|
try {
|
||||||
|
if (sha256File(this.target(rel)) !== state.local[rel]) return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkRepos() {
|
||||||
|
const state = this.readState();
|
||||||
|
const changes = [];
|
||||||
|
const remoteIds = {};
|
||||||
|
const checkedRoots = [];
|
||||||
|
const errors = [];
|
||||||
|
const verified = { remote: {}, local: {} };
|
||||||
|
const commits = {};
|
||||||
|
this.archive = new Map();
|
||||||
|
for (const root of ALLOWED_ROOTS) {
|
||||||
|
const head = await this.headCommit(root);
|
||||||
|
if (this.localIntact(root, state) && (!head || head === state.commits[root])) continue;
|
||||||
|
let map;
|
||||||
|
try {
|
||||||
|
map = await this.loadArchive(root);
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(`${root}: ${e.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (map.size === 0) {
|
||||||
|
errors.push(`${root}: empty archive`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkedRoots.push(root);
|
||||||
|
if (head) commits[root] = head;
|
||||||
|
for (const [key, entry] of map) {
|
||||||
|
const rel = safeRel(key);
|
||||||
|
if (!rel) continue;
|
||||||
|
const data = entry.getData();
|
||||||
|
const id = gitBlobSha(data);
|
||||||
|
remoteIds[rel] = id;
|
||||||
|
const full = this.target(rel);
|
||||||
|
let ok = false;
|
||||||
|
let localHash = null;
|
||||||
|
if (fs.existsSync(full)) {
|
||||||
|
const buf = fs.readFileSync(full);
|
||||||
|
localHash = sha256Buf(buf);
|
||||||
|
ok = gitBlobSha(buf) === id || (state.remote[rel] === id && state.local[rel] === localHash);
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
verified.remote[rel] = id;
|
||||||
|
verified.local[rel] = localHash;
|
||||||
|
} else {
|
||||||
|
changes.push({ path: rel, sha256: id, size: data.length });
|
||||||
|
this.archive.set(rel, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (checkedRoots.length === 0) {
|
||||||
|
if (errors.length) throw new Error(errors.join(' | '));
|
||||||
|
return { seq: state.seq === null ? 0 : state.seq, changes: [], removed: [], verified, commits };
|
||||||
|
}
|
||||||
|
const tracked = new Set([...Object.keys(state.base), ...Object.keys(state.remote)]);
|
||||||
|
const removed = [...tracked].filter((rel) =>
|
||||||
|
checkedRoots.includes(rel.split('/')[0]) &&
|
||||||
|
!remoteIds[rel] &&
|
||||||
|
safeRel(rel) &&
|
||||||
|
fs.existsSync(this.target(rel))
|
||||||
|
);
|
||||||
|
if (changes.length === 0 && removed.length === 0) {
|
||||||
|
for (const k of Object.keys(verified.remote)) {
|
||||||
|
state.remote[k] = verified.remote[k];
|
||||||
|
state.local[k] = verified.local[k];
|
||||||
|
}
|
||||||
|
this.writeState({ remote: state.remote, local: state.local, commits: { ...state.commits, ...commits } });
|
||||||
|
this.archive = null;
|
||||||
|
}
|
||||||
|
return { seq: state.seq === null ? 0 : state.seq, changes, removed, verified, commits };
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyLive(info, replacements, onFile) {
|
||||||
|
const staged = [];
|
||||||
|
const total = info.changes.length;
|
||||||
|
let next = 0;
|
||||||
|
if (!this.archive) this.archive = new Map();
|
||||||
|
let finished = 0;
|
||||||
|
let failure = null;
|
||||||
|
const worker = async () => {
|
||||||
|
while (next < total && !failure) {
|
||||||
|
const c = info.changes[next++];
|
||||||
|
try {
|
||||||
|
const data = await this.fetchRepoFile(c.path);
|
||||||
|
if (gitBlobSha(data) !== c.sha256) throw new Error(`Checksum mismatch: ${c.path}`);
|
||||||
|
staged.push({ c, data: this.applyReplacements(c.path, data, replacements) });
|
||||||
|
finished++;
|
||||||
|
if (onFile) onFile(c.path, finished, total);
|
||||||
|
} catch (e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await Promise.all(Array.from({ length: Math.min(4, total) }, worker));
|
||||||
|
this.archive = null;
|
||||||
|
if (failure) throw failure;
|
||||||
|
|
||||||
|
const done = [];
|
||||||
|
try {
|
||||||
|
for (const s of staged) {
|
||||||
|
const full = this.target(s.c.path);
|
||||||
|
const prev = fs.existsSync(full) ? fs.readFileSync(full) : null;
|
||||||
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||||
|
fs.writeFileSync(`${full}.vyla-tmp`, s.data);
|
||||||
|
fs.renameSync(`${full}.vyla-tmp`, full);
|
||||||
|
done.push({ full, prev });
|
||||||
|
}
|
||||||
|
for (const rel of info.removed) {
|
||||||
|
const full = this.target(rel);
|
||||||
|
if (!fs.existsSync(full)) continue;
|
||||||
|
const prev = fs.readFileSync(full);
|
||||||
|
fs.rmSync(full, { force: true });
|
||||||
|
done.push({ full, prev });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
for (const d of done.reverse()) {
|
||||||
|
try {
|
||||||
|
if (d.prev === null) fs.rmSync(d.full, { force: true });
|
||||||
|
else fs.writeFileSync(d.full, d.prev);
|
||||||
|
} catch (x) { }
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = this.readState();
|
||||||
|
for (const s of staged) {
|
||||||
|
state.base[s.c.path] = s.c.sha256;
|
||||||
|
state.remote[s.c.path] = s.c.sha256;
|
||||||
|
state.local[s.c.path] = sha256Buf(s.data);
|
||||||
|
}
|
||||||
|
for (const rel of info.removed) {
|
||||||
|
delete state.base[rel];
|
||||||
|
delete state.remote[rel];
|
||||||
|
delete state.local[rel];
|
||||||
|
}
|
||||||
|
const v = info.verified || { remote: {}, local: {} };
|
||||||
|
for (const k of Object.keys(v.remote)) {
|
||||||
|
state.remote[k] = v.remote[k];
|
||||||
|
state.local[k] = v.local[k];
|
||||||
|
}
|
||||||
|
state.commits = { ...state.commits, ...(info.commits || {}) };
|
||||||
|
this.writeState({ seq: info.seq, base: state.base, remote: state.remote, local: state.local, commits: state.commits });
|
||||||
|
}
|
||||||
|
|
||||||
installedSdk() {
|
installedSdk() {
|
||||||
const roots = [this.appDataPath, path.join(this.appDataPath, 'frontend'), path.join(this.appDataPath, 'player')];
|
const roots = [this.appDataPath, path.join(this.appDataPath, 'frontend'), path.join(this.appDataPath, 'player')];
|
||||||
const found = [];
|
const found = [];
|
||||||
|
|||||||
216
main.js
216
main.js
@@ -84,6 +84,7 @@ function getReplacementsConfig() {
|
|||||||
|
|
||||||
const updateClient = new UpdateClient(APPDATA_DIR);
|
const updateClient = new UpdateClient(APPDATA_DIR);
|
||||||
const fileUpdater = new FileUpdater(APPDATA_DIR, updateClient);
|
const fileUpdater = new FileUpdater(APPDATA_DIR, updateClient);
|
||||||
|
fileUpdater.onNotice = (message) => sendToTauri('notice', { message });
|
||||||
let lastFilePercent = -1;
|
let lastFilePercent = -1;
|
||||||
fileUpdater.onProgress = (downloaded, total) => {
|
fileUpdater.onProgress = (downloaded, total) => {
|
||||||
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
|
||||||
@@ -113,55 +114,6 @@ function portInUse(port) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptUser(event, payload, timeoutMs = 120000) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const token = require('crypto').randomBytes(24).toString('hex');
|
|
||||||
const app = express();
|
|
||||||
let server = null;
|
|
||||||
let finished = false;
|
|
||||||
let timer = null;
|
|
||||||
|
|
||||||
const finish = (result) => {
|
|
||||||
if (finished) return;
|
|
||||||
finished = true;
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (server) server.close();
|
|
||||||
resolve(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
timer = setTimeout(() => finish(null), timeoutMs);
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
const ip = req.socket.remoteAddress;
|
|
||||||
if (ip !== '127.0.0.1' && ip !== '::1' && ip !== '::ffff:127.0.0.1') {
|
|
||||||
return res.status(403).end();
|
|
||||||
}
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
|
||||||
res.header('Access-Control-Allow-Methods', 'POST,OPTIONS');
|
|
||||||
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
||||||
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/prompt/answer', express.json({ limit: '64kb' }), (req, res) => {
|
|
||||||
const body = req.body || {};
|
|
||||||
if (body.token !== token) return res.status(403).end();
|
|
||||||
res.sendStatus(200);
|
|
||||||
finish({
|
|
||||||
accept: body.accept === true,
|
|
||||||
files: Array.isArray(body.files) ? body.files.map(String) : []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server = app.listen(4891, '127.0.0.1', () => {
|
|
||||||
sendToTauri(event, { ...payload, token });
|
|
||||||
});
|
|
||||||
server.on('error', (err) => {
|
|
||||||
debugLog(`Prompt server bind error: ${err.message}`);
|
|
||||||
finish(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
debugLog("Inside start() function");
|
debugLog("Inside start() function");
|
||||||
@@ -271,7 +223,7 @@ async function start() {
|
|||||||
debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`);
|
debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog(`Update check failed (offline?): ${e.message}`);
|
debugLog(`Update check failed (offline?): ${e.message}`);
|
||||||
sendToTauri('status', { message: `Update server check skipped (${e.message})` });
|
sendToTauri('notice', { message: `Update check skipped: ${e.message}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeVer = (v) => (v || '').toString().replace(/^v/, '').trim();
|
const normalizeVer = (v) => (v || '').toString().replace(/^v/, '').trim();
|
||||||
@@ -291,86 +243,16 @@ async function start() {
|
|||||||
const versionNewer = isNewer(release.version, localVersion);
|
const versionNewer = isNewer(release.version, localVersion);
|
||||||
const sameVerNewBuild = normalizeVer(release.version) === normalizeVer(localVersion) &&
|
const sameVerNewBuild = normalizeVer(release.version) === normalizeVer(localVersion) &&
|
||||||
!!release.sha256 && !!localSha && release.sha256 !== localSha;
|
!!release.sha256 && !!localSha && release.sha256 !== localSha;
|
||||||
const hasUpdate = versionNewer;
|
const hasUpdate = versionNewer || sameVerNewBuild;
|
||||||
|
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
debugLog("First run with no local payload. Installing remote build...");
|
debugLog("First run with no local payload. Installing remote build...");
|
||||||
sendToTauri('status', { message: 'First run detected. Preparing initial installation...' });
|
sendToTauri('status', { message: 'First run detected. Preparing initial installation...' });
|
||||||
await installBuild(release);
|
await installBuild(release);
|
||||||
} else if (hasUpdate) {
|
} else if (hasUpdate) {
|
||||||
debugLog("Update available. Prompting user...");
|
debugLog("Update available. Installing update...");
|
||||||
sendToTauri('status', { message: `New update available (${release.version}).` });
|
sendToTauri('status', { message: `Updating to ${release.version}...` });
|
||||||
|
await installBuild(release);
|
||||||
const tempApp = express();
|
|
||||||
let userDecision = null;
|
|
||||||
updateDecisionHandler = (val) => { userDecision = val; };
|
|
||||||
|
|
||||||
tempApp.use((req, res, next) => {
|
|
||||||
const clientIp = req.socket.remoteAddress;
|
|
||||||
if (clientIp !== '127.0.0.1' && clientIp !== '::1' && clientIp !== '::ffff:127.0.0.1') {
|
|
||||||
return res.status(403).end();
|
|
||||||
}
|
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
|
||||||
res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
||||||
res.header('Access-Control-Allow-Headers', '*');
|
|
||||||
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
tempApp.get('/update/info', (req, res) => {
|
|
||||||
res.json({ available: true, current: localVersion, latest: release.version });
|
|
||||||
});
|
|
||||||
const handleYes = (req, res) => { debugLog("tempApp received YES"); userDecision = true; res.sendStatus(200); };
|
|
||||||
const handleNo = (req, res) => { debugLog("tempApp received NO"); userDecision = false; res.sendStatus(200); };
|
|
||||||
|
|
||||||
tempApp.all('/update/yes', handleYes);
|
|
||||||
tempApp.all('/update/install', handleYes);
|
|
||||||
tempApp.all('/update/no', handleNo);
|
|
||||||
tempApp.all('/update/skip', handleNo);
|
|
||||||
tempApp.all('/update/:choice', (req, res) => {
|
|
||||||
const c = String(req.params.choice || '').toLowerCase();
|
|
||||||
if (c === 'yes' || c === 'install' || c === 'true') userDecision = true;
|
|
||||||
if (c === 'no' || c === 'skip' || c === 'false') userDecision = false;
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tempServer = await new Promise((resolve) => {
|
|
||||||
const s = tempApp.listen(4891, '127.0.0.1', () => resolve(s));
|
|
||||||
s.on('error', (err) => {
|
|
||||||
debugLog(`Update server bind error: ${err.message}`);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tempServer) {
|
|
||||||
userDecision = false;
|
|
||||||
} else {
|
|
||||||
sendToTauri('update-available', { current: localVersion, latest: release.version });
|
|
||||||
}
|
|
||||||
|
|
||||||
let waitLoops = 0;
|
|
||||||
while (userDecision === null) {
|
|
||||||
if (waitLoops > 24000) {
|
|
||||||
debugLog("Update decision timed out after 10 minutes, defaulting to skip.");
|
|
||||||
userDecision = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
waitLoops++;
|
|
||||||
await new Promise(r => setTimeout(r, 25));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDecisionHandler = null;
|
|
||||||
if (tempServer) {
|
|
||||||
tempServer.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userDecision) {
|
|
||||||
debugLog("User accepted update.");
|
|
||||||
await installBuild(release);
|
|
||||||
} else {
|
|
||||||
debugLog("User skipped update.");
|
|
||||||
sendToTauri('status', { message: 'Update skipped. Continuing startup...' });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
debugLog("Already on the latest version.");
|
debugLog("Already on the latest version.");
|
||||||
sendToTauri('status', { message: 'Up to date.' });
|
sendToTauri('status', { message: 'Up to date.' });
|
||||||
@@ -388,12 +270,14 @@ async function start() {
|
|||||||
patcher.bridgeCredentials(serviceManager.credentialManager);
|
patcher.bridgeCredentials(serviceManager.credentialManager);
|
||||||
|
|
||||||
let ver = "1.0.0";
|
let ver = "1.0.0";
|
||||||
|
let manSha = null;
|
||||||
try {
|
try {
|
||||||
const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8'));
|
const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8'));
|
||||||
ver = man.version || ver;
|
ver = man.version || ver;
|
||||||
|
manSha = man.sha256 || null;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
|
|
||||||
patcher.saveCurrentVersion(ver);
|
patcher.saveCurrentVersion(ver, manSha);
|
||||||
serviceManager.envConfig = getEnvConfig();
|
serviceManager.envConfig = getEnvConfig();
|
||||||
debugLog("Local payload extracted successfully.");
|
debugLog("Local payload extracted successfully.");
|
||||||
} else {
|
} else {
|
||||||
@@ -406,78 +290,42 @@ async function start() {
|
|||||||
throw new Error('Could not download the app files. Check your internet connection and relaunch.');
|
throw new Error('Could not download the app files. Check your internet connection and relaunch.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.VYLA_TEST_UI === '1') {
|
|
||||||
const fakeFiles = [
|
|
||||||
{ path: 'frontend/server.js', status: 'modified', size: 1024 },
|
|
||||||
{ path: 'frontend/public/app.js', status: 'modified', size: 2048 },
|
|
||||||
{ path: 'player/server.js', status: 'modified', size: 512 },
|
|
||||||
{ path: 'player/lib/new-file.js', status: 'added', size: 256 }
|
|
||||||
];
|
|
||||||
const filesAnswer = await promptUser('files-available', { files: fakeFiles });
|
|
||||||
debugLog(`TEST files answer: ${JSON.stringify(filesAnswer)}`);
|
|
||||||
if (filesAnswer && filesAnswer.accept) {
|
|
||||||
for (let i = 1; i <= filesAnswer.files.length; i++) {
|
|
||||||
sendToTauri('extract-progress', {
|
|
||||||
file: filesAnswer.files[i - 1],
|
|
||||||
done: i,
|
|
||||||
total: filesAnswer.files.length,
|
|
||||||
percent: Math.round((i / filesAnswer.files.length) * 100)
|
|
||||||
});
|
|
||||||
await new Promise((r) => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const sdkAnswer = await promptUser('sdk-available', { current: '1.19.0', latest: '1.20.0' });
|
|
||||||
debugLog(`TEST sdk answer: ${JSON.stringify(sdkAnswer)}`);
|
|
||||||
sendToTauri('status', { message: sdkAnswer && sdkAnswer.accept ? 'TEST: SDK accepted' : 'TEST: SDK skipped' });
|
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
|
||||||
}
|
|
||||||
|
|
||||||
sendToTauri('status', { message: 'Checking UI files and SDK...' });
|
sendToTauri('status', { message: 'Checking UI files and SDK...' });
|
||||||
const [filesCheck, sdkCheck] = await Promise.all([
|
const [filesCheck, sdkCheck] = await Promise.all([
|
||||||
withTimeout(fileUpdater.checkFiles().catch((e) => {
|
withTimeout(fileUpdater.checkRepos().catch((e) => {
|
||||||
debugLog(`File check failed: ${e.message}`);
|
debugLog(`Live file check failed: ${e.message}`);
|
||||||
|
sendToTauri('notice', { message: `File check failed: ${e.message}` });
|
||||||
return null;
|
return null;
|
||||||
}), 180000),
|
}), 240000),
|
||||||
withTimeout(fileUpdater.checkSdk().catch((e) => {
|
withTimeout(fileUpdater.checkSdk().catch((e) => {
|
||||||
debugLog(`SDK check failed: ${e.message}`);
|
debugLog(`SDK check failed: ${e.message}`);
|
||||||
|
sendToTauri('notice', { message: `SDK check failed: ${e.message}` });
|
||||||
return null;
|
return null;
|
||||||
}), 12000)
|
}), 12000)
|
||||||
]);
|
]);
|
||||||
|
debugLog(`Repo check result: ${filesCheck ? `${filesCheck.changes.length} change(s)` : 'failed or timed out'}`);
|
||||||
|
|
||||||
if (filesCheck && filesCheck.changes.length > 0) {
|
if (filesCheck && (filesCheck.changes.length > 0 || filesCheck.removed.length > 0)) {
|
||||||
sendToTauri('status', { message: `${filesCheck.changes.length} file update(s) available.` });
|
sendToTauri('status', { message: `Updating ${filesCheck.changes.length} file(s)...` });
|
||||||
const answer = await promptUser('files-available', {
|
try {
|
||||||
files: filesCheck.changes.map((c) => ({ path: c.path, status: c.status, size: c.size }))
|
await fileUpdater.applyLive(filesCheck, getReplacementsConfig(), (file, done, total) => {
|
||||||
});
|
sendToTauri('extract-progress', { file, done, total, percent: Math.round((done / total) * 100) });
|
||||||
if (answer && answer.accept) {
|
});
|
||||||
const allowed = new Set(answer.files);
|
sendToTauri('status', { message: `Updated ${filesCheck.changes.length} file(s).` });
|
||||||
const selected = filesCheck.changes.filter((c) => allowed.has(c.path));
|
} catch (e) {
|
||||||
if (selected.length > 0) {
|
debugLog(`File update failed: ${e.message}`);
|
||||||
try {
|
sendToTauri('notice', { message: `File update failed: ${e.message}` });
|
||||||
await fileUpdater.applyFiles(selected, filesCheck.seq, getReplacementsConfig(), (file, done, total) => {
|
|
||||||
sendToTauri('extract-progress', { file, done, total, percent: Math.round((done / total) * 100) });
|
|
||||||
});
|
|
||||||
sendToTauri('status', { message: `Updated ${selected.length} file(s).` });
|
|
||||||
} catch (e) {
|
|
||||||
debugLog(`File update failed: ${e.message}`);
|
|
||||||
sendToTauri('status', { message: `File update failed: ${e.message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sdkCheck && sdkCheck.outdated) {
|
if (sdkCheck && sdkCheck.outdated) {
|
||||||
sendToTauri('status', { message: `SDK update available (${sdkCheck.latest}).` });
|
sendToTauri('status', { message: `Updating SDK (${sdkCheck.latest})...` });
|
||||||
const answer = await promptUser('sdk-available', { current: sdkCheck.current, latest: sdkCheck.latest });
|
try {
|
||||||
if (answer && answer.accept) {
|
await fileUpdater.installSdk(sdkCheck);
|
||||||
try {
|
sendToTauri('status', { message: `SDK updated to ${sdkCheck.latest}.` });
|
||||||
sendToTauri('status', { message: `Installing SDK ${sdkCheck.latest}...` });
|
} catch (e) {
|
||||||
await fileUpdater.installSdk(sdkCheck);
|
debugLog(`SDK install failed: ${e.message}`);
|
||||||
sendToTauri('status', { message: `SDK updated to ${sdkCheck.latest}.` });
|
sendToTauri('status', { message: `SDK update failed: ${e.message}` });
|
||||||
} catch (e) {
|
|
||||||
debugLog(`SDK install failed: ${e.message}`);
|
|
||||||
sendToTauri('status', { message: `SDK update failed: ${e.message}` });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@tauri-apps/api": "2.12.1",
|
"@tauri-apps/api": "2.12.1",
|
||||||
"@tauri-apps/plugin-opener": "2.7.0",
|
"@tauri-apps/plugin-opener": "2.7.0",
|
||||||
"@tauri-apps/plugin-process": "2.4.0",
|
"@tauri-apps/plugin-process": "2.4.0",
|
||||||
|
"@tauri-apps/plugin-updater": "^2",
|
||||||
"@tauri-apps/plugin-shell": "2.5.0",
|
"@tauri-apps/plugin-shell": "2.5.0",
|
||||||
"@vyla-entertainment/sdk": "^1.20.0",
|
"@vyla-entertainment/sdk": "^1.20.0",
|
||||||
"adm-zip": "^0.6.1",
|
"adm-zip": "^0.6.1",
|
||||||
@@ -1535,4 +1536,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vyla-home",
|
"name": "vyla-home",
|
||||||
"version": "1.1.49",
|
"version": "1.1.54",
|
||||||
"description": "Vyla Home Desktop App Launcher",
|
"description": "Vyla Home Desktop App Launcher",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.12.1"
|
"@tauri-apps/cli": "2.12.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
sign-ui.js
45
sign-ui.js
@@ -1,45 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const roots = { frontend: process.argv[2], player: process.argv[3] };
|
|
||||||
const outDir = path.join(__dirname, 'ui-updates');
|
|
||||||
const filesDir = path.join(outDir, 'files');
|
|
||||||
const keyPath = process.env.VYLA_UI_KEY || path.join(__dirname, 'ui-private.pem');
|
|
||||||
|
|
||||||
fs.rmSync(outDir, { recursive: true, force: true });
|
|
||||||
fs.mkdirSync(filesDir, { recursive: true });
|
|
||||||
fs.writeFileSync(path.join(outDir, '.gitattributes'), '* -text\n');
|
|
||||||
|
|
||||||
const files = [];
|
|
||||||
const walk = (src, rel) => {
|
|
||||||
for (const name of fs.readdirSync(src)) {
|
|
||||||
if (name === 'node_modules' || name === '.git') continue;
|
|
||||||
const full = path.join(src, name);
|
|
||||||
const r = `${rel}/${name}`;
|
|
||||||
const st = fs.lstatSync(full);
|
|
||||||
if (st.isDirectory()) {
|
|
||||||
walk(full, r);
|
|
||||||
} else if (st.isFile()) {
|
|
||||||
const data = fs.readFileSync(full);
|
|
||||||
const dest = path.join(filesDir, ...r.split('/'));
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.writeFileSync(dest, data);
|
|
||||||
files.push({ path: r, sha256: crypto.createHash('sha256').update(data).digest('hex'), size: data.length });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [root, src] of Object.entries(roots)) {
|
|
||||||
if (!src || !fs.existsSync(src)) {
|
|
||||||
console.error(`Missing source dir for ${root}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
walk(path.resolve(src), root);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = Buffer.from(JSON.stringify({ seq: Math.floor(Date.now() / 1000), files }));
|
|
||||||
const sig = crypto.sign(null, raw, crypto.createPrivateKey(fs.readFileSync(keyPath)));
|
|
||||||
fs.writeFileSync(path.join(outDir, 'files-manifest.json'), raw);
|
|
||||||
fs.writeFileSync(path.join(outDir, 'files-manifest.sig'), sig.toString('base64'));
|
|
||||||
console.log(`${files.length} files signed`);
|
|
||||||
BIN
source.png
BIN
source.png
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -4073,7 +4073,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vyla-home"
|
name = "vyla-home"
|
||||||
version = "1.1.49"
|
version = "1.1.54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc2 0.5.2",
|
"objc2 0.5.2",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vyla-home"
|
name = "vyla-home"
|
||||||
version = "1.1.49"
|
version = "1.1.54"
|
||||||
description = "Vyla Home Desktop App Launcher"
|
description = "Vyla Home Desktop App Launcher"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=src/main.rs");
|
println!("cargo:rerun-if-changed=src/main.rs");
|
||||||
println!("cargo:rerun-if-changed=tauri.conf.json");
|
println!("cargo:rerun-if-changed=tauri.conf.json");
|
||||||
println!("cargo:rerun-if-changed=capabilities");
|
println!("cargo:rerun-if-changed=capabilities");
|
||||||
|
for f in [
|
||||||
|
"main.js",
|
||||||
|
"service-manager.js",
|
||||||
|
"file-updater.js",
|
||||||
|
"patcher.js",
|
||||||
|
"crypto-utils.js",
|
||||||
|
"credential-manager.js",
|
||||||
|
"update-client.js",
|
||||||
|
] {
|
||||||
|
println!("cargo:rerun-if-changed=../{}", f);
|
||||||
|
}
|
||||||
|
println!("cargo:rerun-if-changed=../config");
|
||||||
|
|
||||||
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
|
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
|
||||||
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {
|
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Vyla Home",
|
"productName": "Vyla Home",
|
||||||
"version": "1.1.49",
|
"version": "1.1.54",
|
||||||
"identifier": "com.vyla.home",
|
"identifier": "com.vyla.home",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
"../node_modules/**/*"
|
"../node_modules/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
405
ui/index.html
405
ui/index.html
@@ -85,7 +85,6 @@
|
|||||||
height: 70px;
|
height: 70px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
@@ -101,7 +100,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-char {
|
.status-char {
|
||||||
@@ -162,160 +160,16 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
|
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
transition: width 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
|
transition: width 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-apple {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 44px;
|
|
||||||
min-width: 124px;
|
|
||||||
padding: 0 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: none;
|
|
||||||
color: #ffffff;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.25);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: translate 0.2s cubic-bezier(0.16, 1, 0.3, 1), scale 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
outline: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-apple:active {
|
|
||||||
scale: 0.97;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-filter {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-apple.glass-svg {
|
|
||||||
background: hsl(0 0% 100% / var(--glass-frost, 0));
|
|
||||||
backdrop-filter: var(--filter-id) saturate(var(--glass-saturation, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-apple.glass-frost {
|
|
||||||
background: transparent;
|
|
||||||
backdrop-filter: blur(20px) saturate(1.8);
|
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.glass-svg {
|
|
||||||
background: hsl(0 0% 100% / calc(var(--glass-frost, 0) + 0.14));
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.glass-frost {
|
|
||||||
background: rgba(255, 255, 255, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-panel {
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-list {
|
|
||||||
max-height: 170px;
|
|
||||||
overflow-y: auto;
|
|
||||||
text-align: left;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
backdrop-filter: blur(20px) saturate(1.5);
|
|
||||||
-webkit-backdrop-filter: blur(20px) saturate(1.5);
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 4px 0;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-row input[type="checkbox"] {
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
|
||||||
transition: background-color 0.18s ease, border-color 0.18s ease, background-size 0.18s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-row:hover input[type="checkbox"] {
|
|
||||||
background-color: rgba(255, 255, 255, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-row input[type="checkbox"]:checked {
|
|
||||||
background-color: rgba(255, 255, 255, 0.95);
|
|
||||||
border-color: transparent;
|
|
||||||
background-size: 11px 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-row input[type="checkbox"]:active {
|
|
||||||
transform: scale(0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-tag {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 1px 8px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#debug-log-box {
|
#debug-log-box {
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(255, 59, 48, 0.18);
|
background: rgba(255, 59, 48, 0.18);
|
||||||
border: 1px solid rgba(255, 59, 48, 0.35);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||||
@@ -327,6 +181,20 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice-line {
|
||||||
|
display: none;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 204, 102, 0.14);
|
||||||
|
color: #ffd98a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -337,30 +205,15 @@
|
|||||||
|
|
||||||
<div class="loader-container">
|
<div class="loader-container">
|
||||||
<div class="loader-card">
|
<div class="loader-card">
|
||||||
<img class="app-icon" src="https://gitlab.com/vyla-entertainment/home/-/raw/main/build/white_favicon.png"
|
<img class="app-icon" src="/white_favicon.png" alt="Vyla" draggable="false">
|
||||||
alt="Vyla" draggable="false">
|
|
||||||
<div class="status-text" id="status-message">Starting runtime engine...</div>
|
<div class="status-text" id="status-message">Starting runtime engine...</div>
|
||||||
|
<div class="notice-line" id="notice-line"></div>
|
||||||
<div id="debug-log-box"></div>
|
<div id="debug-log-box"></div>
|
||||||
<div class="file-line" id="file-line"></div>
|
<div class="file-line" id="file-line"></div>
|
||||||
<div class="progress-track" id="progress-track" style="display: none;">
|
<div class="progress-track" id="progress-track" style="display: none;">
|
||||||
<div class="progress-fill" id="progress-fill"></div>
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="files-panel" id="files-panel" style="display: none;">
|
|
||||||
<div class="files-list" id="files-list"></div>
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn-apple btn-primary" id="btn-files-apply">Apply Selected</button>
|
|
||||||
<button class="btn-apple btn-secondary" id="btn-files-skip">Skip</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="button-group" id="sdk-actions" style="display: none;">
|
|
||||||
<button class="btn-apple btn-primary" id="btn-sdk-yes">Update SDK</button>
|
|
||||||
<button class="btn-apple btn-secondary" id="btn-sdk-no">Skip</button>
|
|
||||||
</div>
|
|
||||||
<div class="button-group" id="update-actions" style="display: none;">
|
|
||||||
<button class="btn-apple btn-primary" id="btn-update-yes">Update Now</button>
|
|
||||||
<button class="btn-apple btn-secondary" id="btn-update-no">Skip</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1008,18 +861,7 @@ void main() {
|
|||||||
const progressTrack = document.getElementById("progress-track");
|
const progressTrack = document.getElementById("progress-track");
|
||||||
const progressFill = document.getElementById("progress-fill");
|
const progressFill = document.getElementById("progress-fill");
|
||||||
const fileLine = document.getElementById("file-line");
|
const fileLine = document.getElementById("file-line");
|
||||||
const actionsEl = document.getElementById("update-actions");
|
|
||||||
const btnYes = document.getElementById("btn-update-yes");
|
|
||||||
const btnNo = document.getElementById("btn-update-no");
|
|
||||||
const debugBox = document.getElementById("debug-log-box");
|
const debugBox = document.getElementById("debug-log-box");
|
||||||
const filesPanel = document.getElementById("files-panel");
|
|
||||||
const filesList = document.getElementById("files-list");
|
|
||||||
const sdkActions = document.getElementById("sdk-actions");
|
|
||||||
const btnFilesApply = document.getElementById("btn-files-apply");
|
|
||||||
const btnFilesSkip = document.getElementById("btn-files-skip");
|
|
||||||
const btnSdkYes = document.getElementById("btn-sdk-yes");
|
|
||||||
const btnSdkNo = document.getElementById("btn-sdk-no");
|
|
||||||
let pendingToken = null;
|
|
||||||
|
|
||||||
function setStatus(text, color = "rgba(255, 255, 255, 0.95)") {
|
function setStatus(text, color = "rgba(255, 255, 255, 0.95)") {
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
@@ -1036,8 +878,14 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setProgress(pct) {
|
function setProgress(pct) {
|
||||||
if (progressTrack) progressTrack.style.display = "block";
|
if (!progressTrack) return;
|
||||||
if (progressFill) progressFill.style.width = Math.min(100, Math.max(0, pct)) + "%";
|
if (pct > 0) {
|
||||||
|
progressTrack.style.display = "block";
|
||||||
|
if (progressFill) progressFill.style.width = Math.min(100, Math.max(0, pct)) + "%";
|
||||||
|
} else {
|
||||||
|
progressTrack.style.display = "none";
|
||||||
|
if (progressFill) progressFill.style.width = "0%";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDebug(msg) {
|
function showDebug(msg) {
|
||||||
@@ -1060,6 +908,19 @@ void main() {
|
|||||||
if (data && data.message) setStatus(data.message);
|
if (data && data.message) setStatus(data.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventApi.listen("notice", (e) => {
|
||||||
|
const data = e.payload || e;
|
||||||
|
const el = document.getElementById("notice-line");
|
||||||
|
if (!el) return;
|
||||||
|
if (data && data.message) {
|
||||||
|
el.textContent = data.message;
|
||||||
|
el.style.display = "block";
|
||||||
|
} else {
|
||||||
|
el.textContent = "";
|
||||||
|
el.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
eventApi.listen("download-progress", (e) => {
|
eventApi.listen("download-progress", (e) => {
|
||||||
const data = e.payload || e;
|
const data = e.payload || e;
|
||||||
if (data && typeof data.percent === "number") {
|
if (data && typeof data.percent === "number") {
|
||||||
@@ -1078,56 +939,10 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventApi.listen("update-available", (e) => {
|
|
||||||
const data = e.payload || e;
|
|
||||||
if (data && data.latest) {
|
|
||||||
if (progressTrack) progressTrack.style.display = "none";
|
|
||||||
setStatus(`Update Available: ${data.latest}`);
|
|
||||||
if (actionsEl) actionsEl.style.display = "flex";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventApi.listen("files-available", (e) => {
|
|
||||||
const data = e.payload || e;
|
|
||||||
if (!data || !Array.isArray(data.files)) return;
|
|
||||||
pendingToken = data.token;
|
|
||||||
if (progressTrack) progressTrack.style.display = "none";
|
|
||||||
filesList.innerHTML = "";
|
|
||||||
data.files.forEach((f) => {
|
|
||||||
const row = document.createElement("label");
|
|
||||||
row.className = "file-row";
|
|
||||||
const cb = document.createElement("input");
|
|
||||||
cb.type = "checkbox";
|
|
||||||
cb.checked = true;
|
|
||||||
cb.value = f.path;
|
|
||||||
const name = document.createElement("span");
|
|
||||||
name.className = "file-name";
|
|
||||||
name.textContent = f.path;
|
|
||||||
name.title = f.path;
|
|
||||||
const tag = document.createElement("span");
|
|
||||||
tag.className = "file-tag";
|
|
||||||
tag.textContent = f.status;
|
|
||||||
row.append(cb, name, tag);
|
|
||||||
filesList.appendChild(row);
|
|
||||||
});
|
|
||||||
setStatus(`${data.files.length} file update${data.files.length === 1 ? "" : "s"} found`);
|
|
||||||
filesPanel.style.display = "flex";
|
|
||||||
});
|
|
||||||
|
|
||||||
eventApi.listen("sdk-available", (e) => {
|
|
||||||
const data = e.payload || e;
|
|
||||||
if (!data || !data.latest) return;
|
|
||||||
pendingToken = data.token;
|
|
||||||
if (progressTrack) progressTrack.style.display = "none";
|
|
||||||
setStatus(`SDK update: ${data.current} → ${data.latest}`);
|
|
||||||
sdkActions.style.display = "flex";
|
|
||||||
});
|
|
||||||
|
|
||||||
eventApi.listen("ready", (e) => {
|
eventApi.listen("ready", (e) => {
|
||||||
const data = e.payload || e;
|
const data = e.payload || e;
|
||||||
if (data && data.url) {
|
if (data && data.url) {
|
||||||
if (progressTrack) progressTrack.style.display = "none";
|
if (progressTrack) progressTrack.style.display = "none";
|
||||||
if (actionsEl) actionsEl.style.display = "none";
|
|
||||||
if (fileLine) fileLine.textContent = "";
|
if (fileLine) fileLine.textContent = "";
|
||||||
setStatus("Launching...");
|
setStatus("Launching...");
|
||||||
}
|
}
|
||||||
@@ -1142,154 +957,8 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function answerPrompt(accept, files) {
|
|
||||||
return fetch("http://127.0.0.1:4891/prompt/answer", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ token: pendingToken, accept, files })
|
|
||||||
}).catch((err) => {
|
|
||||||
setStatus("Failed to reach update service", "#ff6b6b");
|
|
||||||
showDebug(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
btnFilesApply.addEventListener("click", () => {
|
|
||||||
const picked = Array.from(filesList.querySelectorAll("input:checked")).map((i) => i.value);
|
|
||||||
filesPanel.style.display = "none";
|
|
||||||
setStatus("Applying file updates...");
|
|
||||||
answerPrompt(picked.length > 0, picked);
|
|
||||||
});
|
|
||||||
|
|
||||||
btnFilesSkip.addEventListener("click", () => {
|
|
||||||
filesPanel.style.display = "none";
|
|
||||||
setStatus("Skipping file updates...");
|
|
||||||
answerPrompt(false, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
btnSdkYes.addEventListener("click", () => {
|
|
||||||
sdkActions.style.display = "none";
|
|
||||||
setStatus("Updating SDK...");
|
|
||||||
answerPrompt(true, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
btnSdkNo.addEventListener("click", () => {
|
|
||||||
sdkActions.style.display = "none";
|
|
||||||
setStatus("Skipping SDK update...");
|
|
||||||
answerPrompt(false, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
btnYes.addEventListener("click", () => {
|
|
||||||
if (actionsEl) actionsEl.style.display = "none";
|
|
||||||
setStatus("Starting update installation...");
|
|
||||||
fetch("http://127.0.0.1:4891/update/yes", { method: "POST" }).catch((err) => {
|
|
||||||
setStatus("Failed to reach update service", "#ff6b6b");
|
|
||||||
showDebug(err.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
btnNo.addEventListener("click", () => {
|
|
||||||
if (actionsEl) actionsEl.style.display = "none";
|
|
||||||
setStatus("Skipping update. Starting services...");
|
|
||||||
fetch("http://127.0.0.1:4891/update/no", { method: "POST" }).catch(() => { });
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyGlassSurface(el, cfg) {
|
|
||||||
const c = Object.assign({
|
|
||||||
borderRadius: 38,
|
|
||||||
borderWidth: 0.07,
|
|
||||||
brightness: 50,
|
|
||||||
opacity: 0.93,
|
|
||||||
blur: 11,
|
|
||||||
displace: 0,
|
|
||||||
backgroundOpacity: 0,
|
|
||||||
saturation: 1,
|
|
||||||
distortionScale: -180,
|
|
||||||
redOffset: 0,
|
|
||||||
greenOffset: 10,
|
|
||||||
blueOffset: 20,
|
|
||||||
mixBlendMode: 'difference'
|
|
||||||
}, cfg);
|
|
||||||
const id = 'glass-' + Math.random().toString(36).slice(2, 9);
|
|
||||||
const ua = navigator.userAgent;
|
|
||||||
const blocked = (/Safari/.test(ua) && !/Chrome/.test(ua)) || /Firefox/.test(ua);
|
|
||||||
const probe = document.createElement('div');
|
|
||||||
probe.style.backdropFilter = 'url(#' + id + ')';
|
|
||||||
const supported = !blocked && probe.style.backdropFilter !== '';
|
|
||||||
|
|
||||||
el.style.setProperty('--glass-frost', c.backgroundOpacity);
|
|
||||||
el.style.setProperty('--glass-saturation', c.saturation);
|
|
||||||
|
|
||||||
if (!supported) {
|
|
||||||
el.classList.add('glass-frost');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = (name, offset, matrix) =>
|
|
||||||
'<feDisplacementMap in="SourceGraphic" in2="map" scale="' + (c.distortionScale + offset) +
|
|
||||||
'" xChannelSelector="R" yChannelSelector="G" result="d' + name + '"/>' +
|
|
||||||
'<feColorMatrix in="d' + name + '" type="matrix" values="' + matrix + '" result="' + name + '"/>';
|
|
||||||
|
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
||||||
svg.setAttribute('class', 'glass-filter');
|
|
||||||
svg.setAttribute('aria-hidden', 'true');
|
|
||||||
svg.innerHTML =
|
|
||||||
'<defs><filter id="' + id + '" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%">' +
|
|
||||||
'<feImage x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map"/>' +
|
|
||||||
channel('r', c.redOffset, '1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0') +
|
|
||||||
channel('g', c.greenOffset, '0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0') +
|
|
||||||
channel('b', c.blueOffset, '0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0') +
|
|
||||||
'<feBlend in="r" in2="g" mode="screen" result="rg"/>' +
|
|
||||||
'<feBlend in="rg" in2="b" mode="screen" result="out"/>' +
|
|
||||||
'<feGaussianBlur in="out" stdDeviation="' + c.displace + '"/>' +
|
|
||||||
'</filter></defs>';
|
|
||||||
|
|
||||||
const image = svg.querySelector('feImage');
|
|
||||||
const redId = id + '-r';
|
|
||||||
const blueId = id + '-b';
|
|
||||||
const softId = id + '-s';
|
|
||||||
|
|
||||||
const buildMap = () => {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const w = Math.round(rect.width) || 120;
|
|
||||||
const h = Math.round(rect.height) || 44;
|
|
||||||
const edge = Math.min(w, h) * c.borderWidth * 0.5;
|
|
||||||
const map =
|
|
||||||
'<svg width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" xmlns="http://www.w3.org/2000/svg">' +
|
|
||||||
'<defs>' +
|
|
||||||
'<linearGradient id="' + redId + '" x1="100%" y1="0%" x2="0%" y2="0%"><stop offset="0%" stop-color="#0000"/><stop offset="100%" stop-color="red"/></linearGradient>' +
|
|
||||||
'<linearGradient id="' + blueId + '" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#0000"/><stop offset="100%" stop-color="blue"/></linearGradient>' +
|
|
||||||
'<filter id="' + softId + '" filterUnits="userSpaceOnUse" x="0" y="0" width="' + w + '" height="' + h + '" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="' + c.blur + '"/></filter>' +
|
|
||||||
'</defs>' +
|
|
||||||
'<rect x="0" y="0" width="' + w + '" height="' + h + '" fill="black"/>' +
|
|
||||||
'<rect x="0" y="0" width="' + w + '" height="' + h + '" rx="' + c.borderRadius + '" fill="url(#' + redId + ')"/>' +
|
|
||||||
'<rect x="0" y="0" width="' + w + '" height="' + h + '" rx="' + c.borderRadius + '" fill="url(#' + blueId + ')" style="mix-blend-mode:' + c.mixBlendMode + '"/>' +
|
|
||||||
'<rect x="' + edge + '" y="' + edge + '" width="' + (w - edge * 2) + '" height="' + (h - edge * 2) + '" rx="' + c.borderRadius + '" fill="hsl(0 0% ' + c.brightness + '% / ' + c.opacity + ')" filter="url(#' + softId + ')"/>' +
|
|
||||||
'</svg>';
|
|
||||||
image.setAttribute('href', 'data:image/svg+xml,' + encodeURIComponent(map));
|
|
||||||
};
|
|
||||||
|
|
||||||
el.appendChild(svg);
|
|
||||||
el.style.setProperty('--filter-id', 'url(#' + id + ')');
|
|
||||||
el.classList.add('glass-svg');
|
|
||||||
buildMap();
|
|
||||||
new ResizeObserver(buildMap).observe(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
const glassConfig = {
|
|
||||||
borderRadius: 50,
|
|
||||||
distortionScale: -180,
|
|
||||||
displace: 0.5,
|
|
||||||
backgroundOpacity: 0.1
|
|
||||||
};
|
|
||||||
initTauriEvents();
|
initTauriEvents();
|
||||||
document.querySelectorAll('.btn-apple').forEach((el) => {
|
|
||||||
try {
|
|
||||||
applyGlassSurface(el, glassConfig);
|
|
||||||
} catch (e) {
|
|
||||||
el.classList.add('glass-frost');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try { initGradientAscii(); } catch (e) { }
|
try { initGradientAscii(); } catch (e) { }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
Reference in New Issue
Block a user