mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-13 21:57:21 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5950b6ca | |||
| a506a2efb4 | |||
| b0096a2073 | |||
| 8a333d6770 | |||
| 9c4d250640 | |||
| 03ed2f0e53 | |||
| 47c97fdf21 | |||
| e7204589a4 |
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"player": {
|
||||
"PORT": "3000"
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"clientId": "1394887879154598038",
|
||||
"logoImage": "vyla_logo",
|
||||
"tmdbApiKey": "3f6703f3e2a0b2b40fe4fbb3e1546244"
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,14 @@ class CryptoUtils {
|
||||
|
||||
let key;
|
||||
try {
|
||||
const isMac = os.platform() === 'darwin';
|
||||
const machineId = [
|
||||
os.hostname(),
|
||||
isMac ? '' : os.hostname(),
|
||||
os.platform(),
|
||||
os.arch(),
|
||||
os.cpus()[0] ? os.cpus()[0].model : '',
|
||||
process.env.COMPUTERNAME || '',
|
||||
process.env.USERNAME || ''
|
||||
process.env.USERNAME || (isMac ? os.userInfo().username : '')
|
||||
].join('|');
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
||||
442
discord-rpc.js
Normal file
442
discord-rpc.js
Normal file
@@ -0,0 +1,442 @@
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DEFAULT_CLIENT_ID = '1394887879154598038';
|
||||
const DEFAULT_LOGO = 'vyla_logo';
|
||||
|
||||
const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/w500';
|
||||
const OP = { HANDSHAKE: 0, FRAME: 1, CLOSE: 2, PING: 3, PONG: 4 };
|
||||
|
||||
function ipcPaths() {
|
||||
const out = [];
|
||||
if (process.platform === 'win32') {
|
||||
for (let i = 0; i < 10; i++) out.push(`\\\\?\\pipe\\discord-ipc-${i}`);
|
||||
return out;
|
||||
}
|
||||
const bases = [...new Set([process.env.XDG_RUNTIME_DIR, process.env.TMPDIR, process.env.TMP, process.env.TEMP, '/tmp'].filter(Boolean))];
|
||||
for (const base of bases) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
out.push(path.join(base, `discord-ipc-${i}`));
|
||||
out.push(path.join(base, 'app', 'com.discordapp.Discord', `discord-ipc-${i}`));
|
||||
out.push(path.join(base, 'snap.discord', `discord-ipc-${i}`));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class DiscordIpc {
|
||||
constructor(clientId, log) {
|
||||
this.clientId = clientId;
|
||||
this.log = log || (() => { });
|
||||
this.socket = null;
|
||||
this.ready = false;
|
||||
this.connecting = false;
|
||||
this.stopped = true;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.retryTimer = null;
|
||||
this.desired = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stopped = false;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
clearTimeout(this.retryTimer);
|
||||
const s = this.socket;
|
||||
this.socket = null;
|
||||
this.ready = false;
|
||||
if (s) {
|
||||
try { s.destroy(); } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
scheduleRetry() {
|
||||
if (this.stopped) return;
|
||||
clearTimeout(this.retryTimer);
|
||||
this.retryTimer = setTimeout(() => this.connect(), 15000);
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.stopped || this.socket || this.connecting) return;
|
||||
this.connecting = true;
|
||||
const paths = ipcPaths();
|
||||
const attempt = (i) => {
|
||||
if (this.stopped) { this.connecting = false; return; }
|
||||
if (i >= paths.length) {
|
||||
this.connecting = false;
|
||||
this.scheduleRetry();
|
||||
return;
|
||||
}
|
||||
const sock = net.createConnection(paths[i]);
|
||||
let opened = false;
|
||||
sock.once('error', () => {
|
||||
if (!opened) {
|
||||
sock.destroy();
|
||||
attempt(i + 1);
|
||||
}
|
||||
});
|
||||
sock.once('connect', () => {
|
||||
opened = true;
|
||||
this.connecting = false;
|
||||
this.attach(sock);
|
||||
});
|
||||
};
|
||||
attempt(0);
|
||||
}
|
||||
|
||||
attach(sock) {
|
||||
this.socket = sock;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.ready = false;
|
||||
sock.on('data', (chunk) => this.onData(sock, chunk));
|
||||
sock.on('error', () => { });
|
||||
sock.on('close', () => {
|
||||
if (this.socket === sock) {
|
||||
this.socket = null;
|
||||
this.ready = false;
|
||||
this.log('Discord IPC closed');
|
||||
this.scheduleRetry();
|
||||
}
|
||||
});
|
||||
this.send(OP.HANDSHAKE, { v: 1, client_id: this.clientId });
|
||||
}
|
||||
|
||||
send(op, payload) {
|
||||
if (!this.socket) return;
|
||||
const body = Buffer.from(JSON.stringify(payload), 'utf8');
|
||||
const header = Buffer.alloc(8);
|
||||
header.writeInt32LE(op, 0);
|
||||
header.writeInt32LE(body.length, 4);
|
||||
try {
|
||||
this.socket.write(Buffer.concat([header, body]));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
onData(sock, chunk) {
|
||||
if (this.socket !== sock) return;
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
while (this.buffer.length >= 8) {
|
||||
const op = this.buffer.readInt32LE(0);
|
||||
const len = this.buffer.readInt32LE(4);
|
||||
if (len < 0 || len > 1024 * 1024) {
|
||||
sock.destroy();
|
||||
return;
|
||||
}
|
||||
if (this.buffer.length < 8 + len) return;
|
||||
const raw = this.buffer.slice(8, 8 + len).toString('utf8');
|
||||
this.buffer = this.buffer.slice(8 + len);
|
||||
let payload = {};
|
||||
try { payload = JSON.parse(raw); } catch (e) { }
|
||||
this.handle(sock, op, payload);
|
||||
}
|
||||
}
|
||||
|
||||
handle(sock, op, payload) {
|
||||
if (op === OP.PING) {
|
||||
this.send(OP.PONG, payload);
|
||||
} else if (op === OP.CLOSE) {
|
||||
this.log(`Discord closed the connection: ${JSON.stringify(payload)}`);
|
||||
sock.destroy();
|
||||
} else if (op === OP.FRAME) {
|
||||
if (payload.evt === 'READY') {
|
||||
this.ready = true;
|
||||
this.log('Discord RPC ready');
|
||||
this.flush();
|
||||
} else if (payload.evt === 'ERROR') {
|
||||
this.log(`Discord RPC error: ${JSON.stringify(payload.data)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (!this.ready) return;
|
||||
const args = { pid: process.pid };
|
||||
if (this.desired) args.activity = this.desired;
|
||||
this.send(OP.FRAME, { cmd: 'SET_ACTIVITY', args, nonce: crypto.randomUUID() });
|
||||
}
|
||||
|
||||
setActivity(activity) {
|
||||
this.desired = activity || null;
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
function parseRoute(href) {
|
||||
let u;
|
||||
try { u = new URL(href); } catch (e) { return { kind: 'idle' }; }
|
||||
if (u.pathname !== '/' && u.pathname !== '/index.html') return { kind: 'idle' };
|
||||
|
||||
const p = u.searchParams;
|
||||
const id = (p.get('id') || '').trim();
|
||||
if (!/^\d+$/.test(id)) return { kind: 'idle' };
|
||||
|
||||
const type = (p.get('type') || '').toLowerCase();
|
||||
if (type === 'movie' || type === 'tv') {
|
||||
return { kind: 'browse', type, id };
|
||||
}
|
||||
|
||||
const season = parseInt(p.get('season'), 10);
|
||||
const episode = parseInt(p.get('episode'), 10);
|
||||
if (Number.isFinite(season) && Number.isFinite(episode)) {
|
||||
return { kind: 'watch', type: 'tv', id, season, episode };
|
||||
}
|
||||
return { kind: 'watch', type: 'movie', id };
|
||||
}
|
||||
|
||||
function getJson(url, headers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, { headers: { 'User-Agent': 'VylaHome-Client', 'Accept': 'application/json', ...(headers || {}) } }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (c) => { data += c; });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
try { resolve(JSON.parse(data)); } catch (e) { reject(new Error('Invalid JSON')); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(8000, () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clip(s, max = 128) {
|
||||
const str = String(s || '').trim();
|
||||
return str.length > max ? str.slice(0, max - 1) + '…' : str;
|
||||
}
|
||||
|
||||
function cleanPageTitle(t) {
|
||||
const s = String(t || '').replace(/\s*[-|–—•]\s*Vyla.*$/i, '').trim();
|
||||
if (!s || /^vyla/i.test(s)) return null;
|
||||
return s;
|
||||
}
|
||||
|
||||
class VylaPresence {
|
||||
constructor({ getConfig, log }) {
|
||||
this.getConfig = getConfig || (() => ({}));
|
||||
this.log = log || (() => { });
|
||||
this.windows = new Map();
|
||||
this.ipc = null;
|
||||
this.started = false;
|
||||
this.metaCache = new Map();
|
||||
this.pageMemory = new Map();
|
||||
this.lastStart = 0;
|
||||
this.watchKey = null;
|
||||
this.watchStart = 0;
|
||||
this.lastActivityKey = null;
|
||||
this.timer = null;
|
||||
this.token = 0;
|
||||
}
|
||||
|
||||
settings() {
|
||||
const cfg = (this.getConfig() || {}).discord || {};
|
||||
const clientId = [process.env.VYLA_DISCORD_CLIENT_ID, cfg.clientId, DEFAULT_CLIENT_ID]
|
||||
.map((v) => String(v || '').trim())
|
||||
.find((v) => /^\d{17,20}$/.test(v)) || '';
|
||||
const tmdbKey = String(process.env.VYLA_TMDB_API_KEY || cfg.tmdbApiKey || '').trim();
|
||||
return {
|
||||
enabled: cfg.enabled !== false,
|
||||
clientId: /^\d{17,20}$/.test(clientId) ? clientId : '',
|
||||
logo: cfg.logoImage || DEFAULT_LOGO,
|
||||
tmdbKey: /^YOUR_/i.test(tmdbKey) ? '' : tmdbKey
|
||||
};
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.started) return;
|
||||
const s = this.settings();
|
||||
if (!s.enabled) {
|
||||
this.log('Discord presence disabled in config.');
|
||||
return;
|
||||
}
|
||||
if (!s.clientId) {
|
||||
this.log('Discord presence not started: no clientId configured.');
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
this.ipc = new DiscordIpc(s.clientId, (m) => this.log(m));
|
||||
this.ipc.start();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearTimeout(this.timer);
|
||||
this.started = false;
|
||||
if (this.ipc) {
|
||||
this.ipc.stop();
|
||||
this.ipc = null;
|
||||
}
|
||||
}
|
||||
|
||||
report(payload) {
|
||||
if (!payload || !payload.label) return;
|
||||
this.log(`report ${payload.label}: ${payload.closed ? 'closed' : payload.url}`);
|
||||
if (payload.closed) {
|
||||
this.windows.delete(payload.label);
|
||||
} else {
|
||||
this.windows.set(payload.label, {
|
||||
url: String(payload.url || ''),
|
||||
title: String(payload.title || ''),
|
||||
image: String(payload.image || ''),
|
||||
position: Number(payload.position),
|
||||
duration: Number(payload.duration),
|
||||
paused: payload.paused === true,
|
||||
at: Date.now()
|
||||
});
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
current() {
|
||||
let best = null;
|
||||
for (const w of this.windows.values()) {
|
||||
if (!best || w.at >= best.at) best = w;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (!this.started) return;
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => this.apply().catch((e) => this.log(`Presence update failed: ${e.message}`)), 400);
|
||||
}
|
||||
|
||||
async fetchMeta(route) {
|
||||
const s = this.settings();
|
||||
if (!s.tmdbKey) return null;
|
||||
const key = `${route.type}:${route.id}`;
|
||||
if (this.metaCache.has(key)) return this.metaCache.get(key);
|
||||
try {
|
||||
const bearer = s.tmdbKey.length > 40;
|
||||
const url = `https://api.themoviedb.org/3/${route.type}/${route.id}` + (bearer ? '' : `?api_key=${encodeURIComponent(s.tmdbKey)}`);
|
||||
const d = await getJson(url, bearer ? { Authorization: `Bearer ${s.tmdbKey}` } : null);
|
||||
const meta = {
|
||||
title: d.title || d.name || null,
|
||||
poster: d.poster_path ? `${TMDB_IMAGE_BASE}${d.poster_path}` : null,
|
||||
runtime: d.runtime || (Array.isArray(d.episode_run_time) && d.episode_run_time[0]) || 0
|
||||
};
|
||||
this.metaCache.set(key, meta);
|
||||
return meta;
|
||||
} catch (e) {
|
||||
this.log(`TMDB lookup failed for ${key}: ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchEpisodeName(route) {
|
||||
const s = this.settings();
|
||||
if (!s.tmdbKey) return null;
|
||||
const key = `ep:${route.id}:${route.season}:${route.episode}`;
|
||||
if (this.metaCache.has(key)) return this.metaCache.get(key);
|
||||
try {
|
||||
const bearer = s.tmdbKey.length > 40;
|
||||
const url = `https://api.themoviedb.org/3/tv/${route.id}/season/${route.season}/episode/${route.episode}` + (bearer ? '' : `?api_key=${encodeURIComponent(s.tmdbKey)}`);
|
||||
const d = await getJson(url, bearer ? { Authorization: `Bearer ${s.tmdbKey}` } : null);
|
||||
const info = { name: d.name || null, runtime: d.runtime || 0 };
|
||||
this.metaCache.set(key, info);
|
||||
return info;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async apply() {
|
||||
const token = ++this.token;
|
||||
const s = this.settings();
|
||||
const win = this.current();
|
||||
const route = win ? parseRoute(win.url) : { kind: 'idle' };
|
||||
|
||||
let activity;
|
||||
if (route.kind === 'idle') {
|
||||
this.watchKey = null;
|
||||
activity = {
|
||||
type: 0,
|
||||
details: 'On Vyla Home',
|
||||
assets: { large_image: s.logo, large_text: 'Vyla Home' }
|
||||
};
|
||||
} else {
|
||||
const meta = await this.fetchMeta(route);
|
||||
const episode = (route.kind === 'watch' && route.type === 'tv' ? await this.fetchEpisodeName(route) : null) || {};
|
||||
if (token !== this.token) return;
|
||||
|
||||
const pageTitle = win ? cleanPageTitle(win.title) : null;
|
||||
const pageImage = win && /^https:\/\//i.test(win.image) ? win.image : null;
|
||||
const memKey = `${route.type}:${route.id}`;
|
||||
const mem = this.pageMemory.get(memKey) || {};
|
||||
if (pageTitle) mem.title = pageTitle;
|
||||
if (pageImage) mem.poster = pageImage;
|
||||
this.pageMemory.set(memKey, mem);
|
||||
const title = (meta && meta.title) || pageTitle || mem.title || (route.type === 'tv' ? 'a TV show' : 'a movie');
|
||||
const poster = (meta && meta.poster) || pageImage || mem.poster;
|
||||
|
||||
const assets = {
|
||||
large_image: poster || s.logo,
|
||||
large_text: clip(title)
|
||||
};
|
||||
if (poster) {
|
||||
assets.small_image = s.logo;
|
||||
assets.small_text = 'Vyla Home';
|
||||
}
|
||||
|
||||
if (route.kind === 'browse') {
|
||||
this.watchKey = null;
|
||||
activity = {
|
||||
type: 0,
|
||||
details: 'Browsing Vyla',
|
||||
state: clip(`Browsing ${title}`),
|
||||
assets
|
||||
};
|
||||
} else {
|
||||
const key = route.type === 'tv' ? `tv:${route.id}:${route.season}:${route.episode}` : `movie:${route.id}`;
|
||||
if (this.watchKey !== key) {
|
||||
this.watchKey = key;
|
||||
this.watchStart = Date.now();
|
||||
this.lastStart = 0;
|
||||
}
|
||||
const runtimeMin = episode.runtime || (meta && meta.runtime) || 0;
|
||||
let timestamps;
|
||||
if (win && Number.isFinite(win.duration) && win.duration > 0) {
|
||||
if (win.paused) {
|
||||
this.lastStart = 0;
|
||||
} else {
|
||||
let start = Date.now() - Math.round(win.position * 1000);
|
||||
if (this.lastStart && Math.abs(start - this.lastStart) < 4000) start = this.lastStart;
|
||||
this.lastStart = start;
|
||||
timestamps = { start, end: start + Math.round(win.duration * 1000) };
|
||||
}
|
||||
} else if (runtimeMin) {
|
||||
timestamps = { start: this.watchStart, end: this.watchStart + runtimeMin * 60000 };
|
||||
} else {
|
||||
timestamps = { start: this.watchStart };
|
||||
}
|
||||
activity = {
|
||||
type: 3,
|
||||
details: clip(`Watching ${title}`),
|
||||
assets
|
||||
};
|
||||
if (timestamps) activity.timestamps = timestamps;
|
||||
if (route.type === 'tv') {
|
||||
activity.state = clip(`Season ${route.season}, Episode ${route.episode}` + (episode.name ? ` · ${episode.name}` : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sig = JSON.stringify(activity);
|
||||
if (sig === this.lastActivityKey) return;
|
||||
this.lastActivityKey = sig;
|
||||
this.log(`activity: ${sig}`);
|
||||
if (this.ipc) this.ipc.setActivity(activity);
|
||||
}
|
||||
}
|
||||
|
||||
VylaPresence.parseRoute = parseRoute;
|
||||
module.exports = VylaPresence;
|
||||
13
main.js
13
main.js
@@ -6,6 +6,7 @@ const ServiceManager = require('./service-manager');
|
||||
const Patcher = require('./patcher');
|
||||
const UpdateClient = require('./update-client');
|
||||
const FileUpdater = require('./file-updater');
|
||||
const VylaPresence = require('./discord-rpc');
|
||||
|
||||
function getDefaultAppData() {
|
||||
if (process.platform === 'darwin') {
|
||||
@@ -43,6 +44,12 @@ function sendToTauri(event, data) {
|
||||
}
|
||||
|
||||
process.stdin.resume();
|
||||
require('readline').createInterface({ input: process.stdin }).on('line', (line) => {
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
if (msg && msg.type === 'presence') presence.report(msg.payload);
|
||||
} catch (e) { }
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
debugLog('stdin closed - parent died. Self-terminating...');
|
||||
process.exit(0);
|
||||
@@ -88,6 +95,7 @@ function liveReposEnabled() {
|
||||
}
|
||||
|
||||
const updateClient = new UpdateClient(APPDATA_DIR);
|
||||
const presence = new VylaPresence({ getConfig: getEnvConfig, log: debugLog });
|
||||
const fileUpdater = new FileUpdater(APPDATA_DIR, updateClient);
|
||||
fileUpdater.onNotice = (message) => sendToTauri('notice', { message });
|
||||
let lastFilePercent = -1;
|
||||
@@ -436,6 +444,7 @@ async function start() {
|
||||
throw new Error(`${e.message}. Check logs in ${LOGS_DIR}`);
|
||||
}
|
||||
debugLog("Ports 3000 and 4890 are ready.");
|
||||
try { presence.start(); } catch (e) { debugLog(`Presence start failed: ${e.message}`); }
|
||||
|
||||
sendToTauri('status', { message: 'All backend services running and healthy.' });
|
||||
debugLog("Sending READY to Tauri.");
|
||||
@@ -448,6 +457,7 @@ async function start() {
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
try { presence.stop(); } catch (e) { }
|
||||
if (serviceManager) {
|
||||
serviceManager.stopAll();
|
||||
}
|
||||
@@ -459,5 +469,8 @@ process.on('SIGINT', () => {
|
||||
process.on('SIGTERM', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGHUP', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
start();
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "vyla-home",
|
||||
"version": "1.1.49",
|
||||
"version": "1.1.56",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vyla-home",
|
||||
"version": "1.1.49",
|
||||
"version": "1.1.56",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.12.1",
|
||||
"@tauri-apps/plugin-opener": "2.7.0",
|
||||
"@tauri-apps/plugin-process": "2.4.0",
|
||||
"@tauri-apps/plugin-updater": "^2",
|
||||
"@tauri-apps/plugin-shell": "2.5.0",
|
||||
"@vyla-entertainment/sdk": "^1.20.0",
|
||||
"@vyla-entertainment/sdk": "latest",
|
||||
"adm-zip": "^0.6.1",
|
||||
"express": "^4.22.3"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vyla-home",
|
||||
"version": "1.1.55",
|
||||
"version": "1.1.57",
|
||||
"description": "Vyla Home Desktop App Launcher",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
44
release.js
44
release.js
@@ -158,6 +158,24 @@ function createGitLabRelease(tag, releaseLinks) {
|
||||
});
|
||||
}
|
||||
|
||||
function injectDiscordConfig() {
|
||||
const envPath = path.join(__dirname, 'config', 'env.json');
|
||||
const cfg = fs.existsSync(envPath) ? JSON.parse(fs.readFileSync(envPath, 'utf8')) : {};
|
||||
const tmdb = process.env.TMDB_API_KEY || process.env.TMDB_KEY || '';
|
||||
const clientId = process.env.DISCORD_CLIENT_ID || '';
|
||||
|
||||
cfg.discord = cfg.discord || {};
|
||||
if (tmdb) {
|
||||
cfg.discord.tmdbApiKey = tmdb;
|
||||
} else {
|
||||
console.log('[WARNING] No TMDB_API_KEY or TMDB_KEY set. Discord posters will fall back to page data.');
|
||||
}
|
||||
if (clientId) cfg.discord.clientId = clientId;
|
||||
|
||||
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
||||
fs.writeFileSync(envPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isFullBuild = process.argv.includes('--installer');
|
||||
const customTag = process.argv.find(arg => arg.startsWith('--tag='))?.split('=')[1];
|
||||
@@ -166,6 +184,8 @@ async function main() {
|
||||
|
||||
console.log(`Building Vyla Home Release (${version})`);
|
||||
|
||||
injectDiscordConfig();
|
||||
|
||||
console.log('[1/4] Building update payload and manifest...');
|
||||
execSync('node build-pipeline.js', { stdio: 'inherit', env: { ...process.env, RELEASE_VERSION: version } });
|
||||
|
||||
@@ -243,6 +263,23 @@ async function main() {
|
||||
console.log('Uploading update-payload.zip...');
|
||||
const payloadUrl = await uploadFileToGitLabPackage(payloadPath, 'vyla-home', version, `update-payload-${timestamp}.zip`);
|
||||
|
||||
const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
for (const [name, c] of Object.entries(manifestData.components || {})) {
|
||||
console.log(`Uploading ${c.file}...`);
|
||||
c.url = await uploadFileToGitLabPackage(path.join(__dirname, 'release-assets', c.file), 'vyla-home', version, `${name}-${timestamp}.zip`);
|
||||
if (!c.url || !c.url.includes('/packages/generic/vyla-home/')) {
|
||||
throw new Error(`Upload for ${name} did not return a package URL.`);
|
||||
}
|
||||
}
|
||||
|
||||
const signingKeyPem = process.env.VYLA_UPDATE_SIGNING_KEY;
|
||||
if (!signingKeyPem) {
|
||||
throw new Error('VYLA_UPDATE_SIGNING_KEY is required to sign the update manifest.');
|
||||
}
|
||||
delete manifestData.signature;
|
||||
manifestData.signature = crypto.sign(null, Buffer.from(UpdateClient.canonicalManifest(manifestData)), signingKeyPem.replace(/\\n/g, '\n')).toString('base64');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2), 'utf8');
|
||||
|
||||
console.log('Uploading manifest.json...');
|
||||
const manifestUrl = await uploadFileToGitLabPackage(manifestPath, 'vyla-home', version, `manifest-${timestamp}.json`);
|
||||
|
||||
@@ -251,13 +288,6 @@ async function main() {
|
||||
{ name: 'manifest.json', url: manifestUrl, link_type: 'other' }
|
||||
];
|
||||
|
||||
const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
for (const [name, c] of Object.entries(manifestData.components || {})) {
|
||||
console.log(`Uploading ${c.file}...`);
|
||||
const componentUrl = await uploadFileToGitLabPackage(path.join(__dirname, 'release-assets', c.file), 'vyla-home', version, `${name}-${timestamp}.zip`);
|
||||
releaseLinks.push({ name: c.file, url: componentUrl, link_type: 'other' });
|
||||
}
|
||||
|
||||
if (installerPath && installerUploadName) {
|
||||
console.log(`Uploading installer ${installerUploadName}...`);
|
||||
const ext = path.extname(installerUploadName);
|
||||
|
||||
@@ -54,6 +54,9 @@ class ServiceManager {
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
...(process.platform === 'darwin'
|
||||
? { PATH: [process.env.PATH, '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'].filter(Boolean).join(path.delimiter) }
|
||||
: {}),
|
||||
PORT: (this.envConfig[name] && this.envConfig[name].PORT) || defaultPorts[name] || process.env.PORT || 3000,
|
||||
TOKEN_SECRET: tokenSecret,
|
||||
...(this.envConfig[name] || {}),
|
||||
|
||||
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
@@ -4073,8 +4073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vyla-home"
|
||||
version = "1.1.55"
|
||||
version = "1.1.57"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vyla-home"
|
||||
version = "1.1.55"
|
||||
version = "1.1.57"
|
||||
description = "Vyla Home Desktop App Launcher"
|
||||
edition = "2021"
|
||||
|
||||
@@ -17,6 +17,9 @@ window-vibrancy = "0.8"
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.5"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -10,6 +10,7 @@ fn main() {
|
||||
"crypto-utils.js",
|
||||
"credential-manager.js",
|
||||
"update-client.js",
|
||||
"discord-rpc.js",
|
||||
] {
|
||||
println!("cargo:rerun-if-changed=../{}", f);
|
||||
}
|
||||
@@ -49,6 +50,7 @@ fn main() {
|
||||
"win_close",
|
||||
"win_drag",
|
||||
"open_new_window",
|
||||
"report_location",
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"allow-win-close",
|
||||
"allow-win-drag",
|
||||
"allow-open-new-window",
|
||||
"allow-report-location",
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-minimize",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Window and opener permissions for local and localhost-served pages","remote":{"urls":["http://localhost:*","http://localhost:*/*","http://127.0.0.1:*","http://127.0.0.1:*/*"]},"local":true,"windows":["*"],"webviews":["*"],"permissions":["allow-win-minimize","allow-win-maximize","allow-win-close","allow-win-drag","allow-open-new-window","core:default","core:window:allow-start-dragging","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-destroy","core:window:allow-is-maximized","core:window:allow-is-fullscreen","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-show","core:window:allow-hide","opener:default"]}}
|
||||
{"default":{"identifier":"default","description":"Window and opener permissions for local and localhost-served pages","remote":{"urls":["http://localhost:*","http://localhost:*/*","http://127.0.0.1:*","http://127.0.0.1:*/*"]},"local":true,"windows":["*"],"webviews":["*"],"permissions":["allow-win-minimize","allow-win-maximize","allow-win-close","allow-win-drag","allow-open-new-window","allow-report-location","core:default","core:window:allow-start-dragging","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-destroy","core:window:allow-is-maximized","core:window:allow-is-fullscreen","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-show","core:window:allow-hide","opener:default"]}}
|
||||
@@ -345,10 +345,10 @@
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`",
|
||||
"description": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`\n- `allow-report-location`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`"
|
||||
"markdownDescription": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`\n- `allow-report-location`"
|
||||
},
|
||||
{
|
||||
"description": "Allows opening a new webview window",
|
||||
@@ -356,6 +356,12 @@
|
||||
"const": "allow-open-new-window",
|
||||
"markdownDescription": "Allows opening a new webview window"
|
||||
},
|
||||
{
|
||||
"description": "Allows reporting the current page to the backend",
|
||||
"type": "string",
|
||||
"const": "allow-report-location",
|
||||
"markdownDescription": "Allows reporting the current page to the backend"
|
||||
},
|
||||
{
|
||||
"description": "Allows closing the window",
|
||||
"type": "string",
|
||||
@@ -386,6 +392,12 @@
|
||||
"const": "deny-open-new-window",
|
||||
"markdownDescription": "Denies the open_new_window command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the report_location command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-report-location",
|
||||
"markdownDescription": "Denies the report_location command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the win_close command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
@@ -345,10 +345,10 @@
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`",
|
||||
"description": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`\n- `allow-report-location`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`"
|
||||
"markdownDescription": "Default permissions for Vyla Home app commands\n#### This permission set includes:\n\n- `allow-win-minimize`\n- `allow-win-maximize`\n- `allow-win-close`\n- `allow-win-drag`\n- `allow-open-new-window`\n- `allow-report-location`"
|
||||
},
|
||||
{
|
||||
"description": "Allows opening a new webview window",
|
||||
@@ -356,6 +356,12 @@
|
||||
"const": "allow-open-new-window",
|
||||
"markdownDescription": "Allows opening a new webview window"
|
||||
},
|
||||
{
|
||||
"description": "Allows reporting the current page to the backend",
|
||||
"type": "string",
|
||||
"const": "allow-report-location",
|
||||
"markdownDescription": "Allows reporting the current page to the backend"
|
||||
},
|
||||
{
|
||||
"description": "Allows closing the window",
|
||||
"type": "string",
|
||||
@@ -386,6 +392,12 @@
|
||||
"const": "deny-open-new-window",
|
||||
"markdownDescription": "Denies the open_new_window command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the report_location command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-report-location",
|
||||
"markdownDescription": "Denies the report_location command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the win_close command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
11
src-tauri/permissions/autogenerated/report_location.toml
Normal file
11
src-tauri/permissions/autogenerated/report_location.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-report-location"
|
||||
description = "Enables the report_location command without any pre-configured scope."
|
||||
commands.allow = ["report_location"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-report-location"
|
||||
description = "Denies the report_location command without any pre-configured scope."
|
||||
commands.deny = ["report_location"]
|
||||
@@ -30,6 +30,12 @@ description = "Allows opening a new webview window"
|
||||
[[permission.allow]]
|
||||
cmd = "open_new_window"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-report-location"
|
||||
description = "Allows reporting the current page to the backend"
|
||||
[[permission.allow]]
|
||||
cmd = "report_location"
|
||||
|
||||
[[set]]
|
||||
identifier = "default"
|
||||
description = "Default permissions for Vyla Home app commands"
|
||||
@@ -38,5 +44,6 @@ permissions = [
|
||||
"allow-win-maximize",
|
||||
"allow-win-close",
|
||||
"allow-win-drag",
|
||||
"allow-open-new-window"
|
||||
]
|
||||
"allow-open-new-window",
|
||||
"allow-report-location"
|
||||
]
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::webview::NewWindowResponse;
|
||||
use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager, State, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
fn debug_log(msg: &str) {
|
||||
let log_dir = if let Ok(app_data) = std::env::var("APPDATA") {
|
||||
@@ -40,10 +40,25 @@ fn debug_log(msg: &str) {
|
||||
|
||||
struct NodeServices(Mutex<Option<Child>>);
|
||||
|
||||
struct NodeStdin(Mutex<Option<std::process::ChildStdin>>);
|
||||
|
||||
fn write_node_line(state: &NodeStdin, line: &str) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
let mut lock = state.0.lock().map_err(|e| e.to_string())?;
|
||||
if let Some(stdin) = lock.as_mut() {
|
||||
stdin.write_all(line.as_bytes()).map_err(|e| e.to_string())?;
|
||||
stdin.write_all(b"\n").map_err(|e| e.to_string())?;
|
||||
stdin.flush().map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static WINDOW_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
|
||||
|
||||
static READY_URL: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
static APP_LOADED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
fn js_log(app: &AppHandle, msg: &str) {
|
||||
debug_log(msg);
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
@@ -94,6 +109,17 @@ fn kill_child_process(child: &mut Child) {
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let pgid = child.id() as i32;
|
||||
unsafe {
|
||||
libc::killpg(pgid, libc::SIGTERM);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||
unsafe {
|
||||
libc::killpg(pgid, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
@@ -472,6 +498,70 @@ const LINK_HANDLER_SCRIPT: &str = r##"
|
||||
injectLiquidNavBar();
|
||||
}
|
||||
startNavObserver();
|
||||
|
||||
(function () {
|
||||
let last = '';
|
||||
let timer = null;
|
||||
|
||||
function metaImage() {
|
||||
const m = document.querySelector("meta[property='og:image']");
|
||||
return m && m.content ? m.content : '';
|
||||
}
|
||||
|
||||
function send(attempt) {
|
||||
const n = typeof attempt === 'number' ? attempt : 0;
|
||||
const tauri = window.__TAURI__;
|
||||
if (!tauri || !tauri.core) {
|
||||
if (n < 40) setTimeout(() => send(n + 1), 250);
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
label: window.__VYLA_WINDOW_LABEL || 'main',
|
||||
url: location.href,
|
||||
title: document.title || '',
|
||||
image: metaImage(),
|
||||
closed: false
|
||||
};
|
||||
const v = document.querySelector('video');
|
||||
if (v && Number.isFinite(v.duration) && v.duration > 0) {
|
||||
payload.position = v.currentTime;
|
||||
payload.duration = v.duration;
|
||||
payload.paused = v.paused;
|
||||
}
|
||||
if (!tauri.event) {
|
||||
if (n < 40) setTimeout(() => send(n + 1), 250);
|
||||
return;
|
||||
}
|
||||
tauri.event.emit('vyla-presence', payload).catch((e) => console.error('[vyla] presence emit failed', e));
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (location.href === last) return;
|
||||
last = location.href;
|
||||
send();
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(send, 1500);
|
||||
}
|
||||
|
||||
['pushState', 'replaceState'].forEach((fn) => {
|
||||
const orig = history[fn];
|
||||
history[fn] = function () {
|
||||
const r = orig.apply(this, arguments);
|
||||
setTimeout(check, 0);
|
||||
return r;
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', check);
|
||||
window.addEventListener('hashchange', check);
|
||||
window.addEventListener('focus', () => send());
|
||||
['play', 'pause', 'seeked'].forEach((name) => {
|
||||
document.addEventListener(name, () => send(), true);
|
||||
});
|
||||
setInterval(check, 2000);
|
||||
setInterval(() => { if (document.hasFocus()) send(); }, 5000);
|
||||
check();
|
||||
})();
|
||||
"##;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -502,6 +592,13 @@ fn win_drag(window: tauri::Window) -> Result<(), String> {
|
||||
window.start_dragging().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn report_location(state: State<'_, NodeStdin>, payload: serde_json::Value) -> Result<(), String> {
|
||||
let line = serde_json::json!({ "type": "presence", "payload": payload }).to_string();
|
||||
debug_log(&format!("[PRESENCE] {}", line));
|
||||
write_node_line(&state, &line)
|
||||
}
|
||||
|
||||
fn normalize_url(url: &str) -> Option<tauri::Url> {
|
||||
let final_url = if url.starts_with("http") || url.starts_with("tauri://") {
|
||||
url.to_string()
|
||||
@@ -580,7 +677,7 @@ fn open_new_window(app: AppHandle, url: String) -> Result<(), String> {
|
||||
|
||||
fn handle_ready(w: &tauri::WebviewWindow, msg: &BackendMessage) {
|
||||
let url_str = match msg.data.get("url").and_then(|u| u.as_str()) {
|
||||
Some(u) => u,
|
||||
Some(u) => u.to_string(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
@@ -592,19 +689,36 @@ fn handle_ready(w: &tauri::WebviewWindow, msg: &BackendMessage) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use objc2::msg_send;
|
||||
use objc2::runtime::AnyObject;
|
||||
debug_log("Attempting with_webview on ready event");
|
||||
let result = w.with_webview(|webview| unsafe {
|
||||
debug_log("Inside with_webview closure");
|
||||
use objc2::runtime::{AnyObject, Sel};
|
||||
use objc2::sel;
|
||||
let _ = w.with_webview(|webview| unsafe {
|
||||
let wk_webview = webview.inner() as *mut AnyObject;
|
||||
let config: *mut AnyObject = msg_send![wk_webview, configuration];
|
||||
let _: () = msg_send![config, setUpgradeKnownHostsToHTTPS: false];
|
||||
debug_log("setUpgradeKnownHostsToHTTPS set to false");
|
||||
let selector: Sel = sel!(setUpgradeKnownHostsToHTTPS:);
|
||||
let supported: bool = msg_send![config, respondsToSelector: selector];
|
||||
if supported {
|
||||
let _: () = msg_send![config, setUpgradeKnownHostsToHTTPS: false];
|
||||
}
|
||||
});
|
||||
debug_log(&format!("with_webview result: {:?}", result));
|
||||
}
|
||||
|
||||
let _ = w.navigate(url_parsed);
|
||||
APP_LOADED.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
let _ = w.navigate(url_parsed.clone());
|
||||
|
||||
let watcher = w.clone();
|
||||
std::thread::spawn(move || {
|
||||
for attempt in 1..=8 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(4));
|
||||
if APP_LOADED.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
debug_log(&format!("App page not loaded yet, retry {}", attempt));
|
||||
let target = serde_json::to_string(&url_str).unwrap_or_default();
|
||||
let _ = watcher.eval(&format!("window.location.replace({});", target));
|
||||
let _ = watcher.navigate(url_parsed.clone());
|
||||
}
|
||||
debug_log("App page never loaded after retries");
|
||||
});
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -617,9 +731,11 @@ fn main() {
|
||||
win_maximize,
|
||||
win_close,
|
||||
win_drag,
|
||||
open_new_window
|
||||
open_new_window,
|
||||
report_location
|
||||
])
|
||||
.manage(NodeServices(Mutex::new(None)))
|
||||
.manage(NodeStdin(Mutex::new(None)))
|
||||
.setup(|app| {
|
||||
debug_log("Tauri setup started");
|
||||
let app_data = app.path().app_data_dir().unwrap_or_default();
|
||||
@@ -663,6 +779,9 @@ fn main() {
|
||||
.initialization_script("window.__VYLA_WINDOW_LABEL = 'main';")
|
||||
.on_page_load(move |wv, payload| {
|
||||
if matches!(payload.event(), tauri::webview::PageLoadEvent::Finished) {
|
||||
if payload.url().host_str() == Some("127.0.0.1") {
|
||||
APP_LOADED.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
if payload.url().host_str() != Some("127.0.0.1") {
|
||||
let known = READY_URL.lock().ok().and_then(|g| g.clone());
|
||||
if let Some(u) = known {
|
||||
@@ -726,6 +845,16 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Ok(meta) = std::fs::metadata(&node_bin) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = std::fs::set_permissions(&node_bin, perms);
|
||||
}
|
||||
}
|
||||
|
||||
let vyla_files_str = clean_path(&vyla_files);
|
||||
|
||||
let mut cmd = Command::new(&node_bin);
|
||||
@@ -746,6 +875,12 @@ fn main() {
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(mut child) => {
|
||||
debug_log(&format!(
|
||||
@@ -846,6 +981,13 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(stdin) = child.stdin.take() {
|
||||
let stdin_state: State<NodeStdin> = app.state();
|
||||
if let Ok(mut lock) = stdin_state.0.lock() {
|
||||
*lock = Some(stdin);
|
||||
};
|
||||
}
|
||||
|
||||
let state: State<NodeServices> = app.state();
|
||||
if let Ok(mut lock) = state.0.lock() {
|
||||
*lock = Some(child);
|
||||
@@ -862,6 +1004,14 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let presence_handle = app.handle().clone();
|
||||
app.listen("vyla-presence", move |event| {
|
||||
debug_log(&format!("[PRESENCE] {}", event.payload()));
|
||||
let line = format!("{{\"type\":\"presence\",\"payload\":{}}}", event.payload());
|
||||
let state: State<NodeStdin> = presence_handle.state();
|
||||
let _ = write_node_line(&state, &line);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
@@ -875,9 +1025,24 @@ fn main() {
|
||||
}
|
||||
};
|
||||
window.app_handle().exit(0);
|
||||
} else {
|
||||
let label = window.label().to_string();
|
||||
let line = serde_json::json!({ "type": "presence", "payload": { "label": label, "closed": true } }).to_string();
|
||||
let state: State<NodeStdin> = window.state();
|
||||
let _ = write_node_line(&state, &line);
|
||||
}
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app, event| {
|
||||
if let tauri::RunEvent::Exit = event {
|
||||
let state: State<NodeServices> = app.state();
|
||||
if let Ok(mut lock) = state.0.lock() {
|
||||
if let Some(mut child) = lock.take() {
|
||||
kill_child_process(&mut child);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Vyla Home",
|
||||
"version": "1.1.55",
|
||||
"version": "1.1.57",
|
||||
"identifier": "com.vyla.home",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
@@ -41,6 +41,7 @@
|
||||
"../crypto-utils.js",
|
||||
"../credential-manager.js",
|
||||
"../update-client.js",
|
||||
"../discord-rpc.js",
|
||||
"../config/**/*",
|
||||
"../node_modules/**/*"
|
||||
]
|
||||
|
||||
@@ -379,11 +379,15 @@ class UpdateClient {
|
||||
|
||||
const components = {};
|
||||
for (const [name, c] of Object.entries(manifest.components)) {
|
||||
const link = links.find((a) => a.name === c.file);
|
||||
if (!link) {
|
||||
throw new Error(`Release is missing asset ${c.file}.`);
|
||||
const direct = typeof c.url === 'string' &&
|
||||
c.url.startsWith('https://') &&
|
||||
isAllowedHost(c.url) &&
|
||||
c.url.includes('/packages/generic/vyla-home/');
|
||||
const url = direct ? c.url : null;
|
||||
if (!url) {
|
||||
throw new Error(`Release ${release.tag_name} manifest has no download URL for ${c.file}.`);
|
||||
}
|
||||
components[name] = { url: link.url, sha256: c.sha256, id: c.id, size: c.size };
|
||||
components[name] = { url, sha256: c.sha256, id: c.id, size: c.size };
|
||||
}
|
||||
|
||||
let installer = null;
|
||||
|
||||
Reference in New Issue
Block a user