mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
fixed UI issue
This commit is contained in:
196
main.js
196
main.js
@@ -113,55 +113,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() {
|
||||
debugLog("Inside start() function");
|
||||
@@ -298,79 +249,9 @@ async function start() {
|
||||
sendToTauri('status', { message: 'First run detected. Preparing initial installation...' });
|
||||
await installBuild(release);
|
||||
} else if (hasUpdate) {
|
||||
debugLog("Update available. Prompting user...");
|
||||
sendToTauri('status', { message: `New update available (${release.version}).` });
|
||||
|
||||
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...' });
|
||||
}
|
||||
debugLog("Update available. Installing update...");
|
||||
sendToTauri('status', { message: `Updating to ${release.version}...` });
|
||||
await installBuild(release);
|
||||
} else {
|
||||
debugLog("Already on the latest version.");
|
||||
sendToTauri('status', { message: 'Up to date.' });
|
||||
@@ -406,32 +287,6 @@ async function start() {
|
||||
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...' });
|
||||
const [filesCheck, sdkCheck] = await Promise.all([
|
||||
withTimeout(fileUpdater.checkFiles().catch((e) => {
|
||||
@@ -445,39 +300,26 @@ async function start() {
|
||||
]);
|
||||
|
||||
if (filesCheck && filesCheck.changes.length > 0) {
|
||||
sendToTauri('status', { message: `${filesCheck.changes.length} file update(s) available.` });
|
||||
const answer = await promptUser('files-available', {
|
||||
files: filesCheck.changes.map((c) => ({ path: c.path, status: c.status, size: c.size }))
|
||||
});
|
||||
if (answer && answer.accept) {
|
||||
const allowed = new Set(answer.files);
|
||||
const selected = filesCheck.changes.filter((c) => allowed.has(c.path));
|
||||
if (selected.length > 0) {
|
||||
try {
|
||||
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}` });
|
||||
}
|
||||
}
|
||||
sendToTauri('status', { message: `Updating ${filesCheck.changes.length} file(s)...` });
|
||||
try {
|
||||
await fileUpdater.applyFiles(filesCheck.changes, filesCheck.seq, getReplacementsConfig(), (file, done, total) => {
|
||||
sendToTauri('extract-progress', { file, done, total, percent: Math.round((done / total) * 100) });
|
||||
});
|
||||
sendToTauri('status', { message: `Updated ${filesCheck.changes.length} file(s).` });
|
||||
} catch (e) {
|
||||
debugLog(`File update failed: ${e.message}`);
|
||||
sendToTauri('status', { message: `File update failed: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
if (sdkCheck && sdkCheck.outdated) {
|
||||
sendToTauri('status', { message: `SDK update available (${sdkCheck.latest}).` });
|
||||
const answer = await promptUser('sdk-available', { current: sdkCheck.current, latest: sdkCheck.latest });
|
||||
if (answer && answer.accept) {
|
||||
try {
|
||||
sendToTauri('status', { message: `Installing SDK ${sdkCheck.latest}...` });
|
||||
await fileUpdater.installSdk(sdkCheck);
|
||||
sendToTauri('status', { message: `SDK updated to ${sdkCheck.latest}.` });
|
||||
} catch (e) {
|
||||
debugLog(`SDK install failed: ${e.message}`);
|
||||
sendToTauri('status', { message: `SDK update failed: ${e.message}` });
|
||||
}
|
||||
sendToTauri('status', { message: `Updating SDK (${sdkCheck.latest})...` });
|
||||
try {
|
||||
await fileUpdater.installSdk(sdkCheck);
|
||||
sendToTauri('status', { message: `SDK updated to ${sdkCheck.latest}.` });
|
||||
} catch (e) {
|
||||
debugLog(`SDK install failed: ${e.message}`);
|
||||
sendToTauri('status', { message: `SDK update failed: ${e.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
368
ui/index.html
368
ui/index.html
@@ -167,147 +167,7 @@
|
||||
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 {
|
||||
display: none;
|
||||
@@ -346,21 +206,6 @@
|
||||
<div class="progress-fill" id="progress-fill"></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>
|
||||
|
||||
@@ -1008,18 +853,7 @@ void main() {
|
||||
const progressTrack = document.getElementById("progress-track");
|
||||
const progressFill = document.getElementById("progress-fill");
|
||||
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 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)") {
|
||||
if (!statusEl) return;
|
||||
@@ -1036,8 +870,14 @@ void main() {
|
||||
}
|
||||
|
||||
function setProgress(pct) {
|
||||
if (progressTrack) progressTrack.style.display = "block";
|
||||
if (progressFill) progressFill.style.width = Math.min(100, Math.max(0, pct)) + "%";
|
||||
if (!progressTrack) return;
|
||||
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) {
|
||||
@@ -1078,56 +918,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) => {
|
||||
const data = e.payload || e;
|
||||
if (data && data.url) {
|
||||
if (progressTrack) progressTrack.style.display = "none";
|
||||
if (actionsEl) actionsEl.style.display = "none";
|
||||
if (fileLine) fileLine.textContent = "";
|
||||
setStatus("Launching...");
|
||||
}
|
||||
@@ -1142,154 +936,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', () => {
|
||||
const glassConfig = {
|
||||
borderRadius: 50,
|
||||
distortionScale: -180,
|
||||
displace: 0.5,
|
||||
backgroundOpacity: 0.1
|
||||
};
|
||||
initTauriEvents();
|
||||
document.querySelectorAll('.btn-apple').forEach((el) => {
|
||||
try {
|
||||
applyGlassSurface(el, glassConfig);
|
||||
} catch (e) {
|
||||
el.classList.add('glass-frost');
|
||||
}
|
||||
});
|
||||
try { initGradientAscii(); } catch (e) { }
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user