mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
d1e31c0c26
Operators can override or hide download.product_* on the public download page via bundle branding without rebuilding installers. Refs #386 Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
176 lines
9.5 KiB
HTML
176 lines
9.5 KiB
HTML
<%
|
|
// Public download portal — branded per bundle. No auth, no shared layout.
|
|
// Bundle ID itself is the access token.
|
|
const brand = bundle.branding || {};
|
|
const gb = typeof globalBranding !== 'undefined' && globalBranding ? globalBranding : {};
|
|
const primary = brand.primary_color || '#2563eb';
|
|
const accent = brand.accent_color || '#1e293b';
|
|
const companyName = brand.company_name || gb.appName || 'BetterDesk Support';
|
|
const shortText = brand.short_text || '';
|
|
const logo = brand.logo_data_url || gb.logoUrl || '';
|
|
const contactEmail = brand.contact_email || '';
|
|
const contactPhone = brand.contact_phone || '';
|
|
const contactUrl = brand.contact_url || '';
|
|
const portalBackground = gb.background || '';
|
|
const showPoweredBy = gb.showPoweredBy !== false;
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
%><!DOCTYPE html>
|
|
<html lang="<%= brand.default_lang || 'en' %>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><%= esc(companyName) %> — <%= t('download.title') %></title>
|
|
<link rel="stylesheet" href="/css/agent-download.css">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%232563eb'/%3E%3Ctext x='50' y='68' font-size='60' text-anchor='middle' fill='white' font-family='Arial'%3EB%3C/text%3E%3C/svg%3E">
|
|
<style>
|
|
:root {
|
|
--brand-primary: <%= esc(primary) %>;
|
|
--brand-accent: <%= esc(accent) %>;
|
|
}
|
|
<% if (portalBackground) { %>
|
|
body {
|
|
background: <%- portalBackground %>;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-attachment: fixed;
|
|
}
|
|
<% } %>
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="portal">
|
|
<header class="portal-header">
|
|
<% if (logo) { %>
|
|
<img src="<%= logo %>" alt="<%= esc(companyName) %>" class="portal-logo">
|
|
<% } else { %>
|
|
<div class="portal-logo portal-logo-fallback">
|
|
<svg viewBox="0 0 24 24" width="56" height="56" fill="currentColor" aria-hidden="true"><path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/></svg>
|
|
</div>
|
|
<% } %>
|
|
<h1><%= esc(companyName) %></h1>
|
|
<% if (shortText) { %>
|
|
<p class="portal-subtitle"><%= esc(shortText) %></p>
|
|
<% } %>
|
|
</header>
|
|
|
|
<section class="portal-intro">
|
|
<h2><%= t('download.choose_platform') %></h2>
|
|
<p><%= t('download.choose_platform_hint') %></p>
|
|
<% if (typeof productType !== 'undefined' && !brand.hide_product_type) { %>
|
|
<p class="portal-product-type">
|
|
<% if (brand.product_label) { %>
|
|
<%= esc(brand.product_label) %>
|
|
<% } else if (productType === 'rdclient') { %>
|
|
<%= t('download.product_rdclient') %>
|
|
<% } else if (productType === 'support-agent') { %>
|
|
<%= t('download.product_support_agent') %>
|
|
<% } else { %>
|
|
<%= t('download.product_agent_client') %>
|
|
<% } %>
|
|
</p>
|
|
<% } %>
|
|
</section>
|
|
|
|
<section class="portal-grid" id="portal-grid">
|
|
<% platforms.forEach(p => { %>
|
|
<article class="portal-card" data-platform="<%= p.platform %>" data-arch="<%= p.arch %>" data-format="<%= p.format %>" data-status="<%= p.status %>">
|
|
<div class="card-icon">
|
|
<% if (p.platform === 'windows') { %>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><path d="M2 5.4 9.6 4.3v7.4H2zM10.6 4.1 22 2.5v9.2H10.6zM2 12.7h7.6v7.4L2 18.9zM10.6 12.7H22v9.2l-11.4-1.6z"/></svg>
|
|
<% } else if (p.format === 'deb') { %>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><text x="12" y="16" text-anchor="middle" font-size="9" font-weight="bold">DEB</text></svg>
|
|
<% } else if (p.format === 'rpm') { %>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><text x="12" y="16" text-anchor="middle" font-size="9" font-weight="bold">RPM</text></svg>
|
|
<% } else { %>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><path d="M12 2 4 6v6c0 5 3.5 9.4 8 10 4.5-.6 8-5 8-10V6z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
|
<% } %>
|
|
</div>
|
|
<h3><%= esc(p.label) %></h3>
|
|
<p class="card-arch"><%= esc(p.arch) %></p>
|
|
<div class="card-status" data-status-slot>
|
|
<% if (p.status === 'ready') { %>
|
|
<a class="btn btn-primary" href="/api/d/<%= bundle.public_id %>/download/<%= p.platform %>/<%= p.arch %>/<%= p.format %>">
|
|
<%= t('download.download') %>
|
|
</a>
|
|
<% } else if (p.status === 'failed') { %>
|
|
<span class="status-badge status-failed"><%= t('download.failed') %></span>
|
|
<% } else { %>
|
|
<span class="status-badge status-pending">
|
|
<span class="spinner"></span>
|
|
<%= t('download.building') %>
|
|
</span>
|
|
<% } %>
|
|
</div>
|
|
</article>
|
|
<% }); %>
|
|
</section>
|
|
|
|
<% if (contactEmail || contactPhone || contactUrl) { %>
|
|
<section class="portal-contact">
|
|
<h2><%= t('download.need_help') %></h2>
|
|
<ul class="contact-list">
|
|
<% if (contactEmail) { %>
|
|
<li><span class="material-icons">mail</span><a href="mailto:<%= esc(contactEmail) %>"><%= esc(contactEmail) %></a></li>
|
|
<% } %>
|
|
<% if (contactPhone) { %>
|
|
<li><span class="material-icons">phone</span><a href="tel:<%= esc(contactPhone) %>"><%= esc(contactPhone) %></a></li>
|
|
<% } %>
|
|
<% if (contactUrl) { %>
|
|
<li><span class="material-icons">language</span><a href="<%= esc(contactUrl) %>" target="_blank" rel="noopener"><%= esc(contactUrl) %></a></li>
|
|
<% } %>
|
|
</ul>
|
|
</section>
|
|
<% } %>
|
|
|
|
<footer class="portal-footer">
|
|
<% if (showPoweredBy) { %>
|
|
<p><%= t('download.powered_by') %> <strong>BetterDesk</strong></p>
|
|
<% } %>
|
|
</footer>
|
|
</main>
|
|
|
|
<script nonce="<%= typeof cspNonce !== 'undefined' ? cspNonce : '' %>">
|
|
// Poll manifest every 8s while at least one platform is still building.
|
|
(function () {
|
|
const publicId = <%- JSON.stringify(bundle.public_id) %>;
|
|
const t = <%- JSON.stringify({
|
|
building: t('download.building'),
|
|
failed: t('download.failed'),
|
|
download: t('download.download'),
|
|
}) %>;
|
|
function render(p) {
|
|
const slot = document.querySelector(
|
|
`[data-platform="${p.platform}"][data-arch="${p.arch}"][data-format="${p.format}"] [data-status-slot]`
|
|
);
|
|
if (!slot) return;
|
|
if (p.status === 'ready') {
|
|
slot.innerHTML = `<a class="btn btn-primary" href="/api/d/${publicId}/download/${p.platform}/${p.arch}/${p.format}">${t.download}</a>`;
|
|
} else if (p.status === 'failed') {
|
|
slot.innerHTML = `<span class="status-badge status-failed">${t.failed}</span>`;
|
|
} else {
|
|
slot.innerHTML = `<span class="status-badge status-pending"><span class="spinner"></span>${t.building}</span>`;
|
|
}
|
|
}
|
|
let timer = null;
|
|
async function poll() {
|
|
try {
|
|
const r = await fetch(`/api/d/${publicId}/manifest`, { credentials: 'omit' });
|
|
if (!r.ok) return;
|
|
const j = await r.json();
|
|
if (!j || !j.success) return;
|
|
const platforms = j.data.platforms || [];
|
|
platforms.forEach(render);
|
|
const stillPending = platforms.some(p => p.status !== 'ready' && p.status !== 'failed');
|
|
if (!stillPending && timer) { clearInterval(timer); timer = null; }
|
|
} catch (_) { /* network blip — keep polling */ }
|
|
}
|
|
// Only start polling if there's something pending.
|
|
const hasPending = !!document.querySelector('.portal-card[data-status]:not([data-status="ready"]):not([data-status="failed"])');
|
|
if (hasPending) { timer = setInterval(poll, 8000); }
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|