1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-07-29 03:38:44 +03:00

feat(hub): shared nav shell, downloads UI, footer fix

Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
Anton Afanasyeu
2026-07-12 13:53:36 +02:00
parent 0042bb9870
commit db8832b417
7 changed files with 179 additions and 106 deletions

View File

@@ -1,6 +1,9 @@
(function () {
'use strict';
/** Orchestration default channel; stable is fallback for promoted releases. */
var OTA_CHANNELS = ['staging', 'stable'];
function setStatus(msg) {
var el = document.getElementById('hub-download-apk-status');
if (el) {
@@ -8,32 +11,79 @@
}
}
function resolveStableApkUrl() {
return fetch('/v0/ota/channel/stable.json', { cache: 'no-store' })
.then(function (r) {
function fetchJson(url) {
return fetch(url, { cache: 'no-store' }).then(function (r) {
if (!r.ok) {
throw new Error('stable channel HTTP ' + r.status);
}
return r.json();
})
.then(function (channel) {
var manifestUrl = channel && channel.manifestUrl;
if (!manifestUrl) {
throw new Error('stable.json missing manifestUrl');
}
return fetch(manifestUrl, { cache: 'no-store' }).then(function (r) {
if (!r.ok) {
throw new Error('manifest HTTP ' + r.status);
throw new Error('HTTP ' + r.status + ' for ' + url);
}
return r.json();
});
}
function resolveApkFromChannel(channelName) {
return fetchJson('/v0/ota/channel/' + encodeURIComponent(channelName) + '.json')
.then(function (channel) {
var manifestUrl = channel && channel.manifestUrl;
if (!manifestUrl) {
throw new Error(channelName + ' missing manifestUrl');
}
return fetchJson(manifestUrl);
})
.then(function (manifest) {
var url = manifest && (manifest.apkUrl || manifest.bundleUrl);
var url =
(manifest && (manifest.browserApkUrl || manifest.apkUrl)) || '';
if (!url) {
throw new Error('manifest missing apkUrl');
throw new Error('manifest missing browserApkUrl/apkUrl');
}
return url;
if (!manifest.browserApkUrl && /\.otapkg(\?|$)/.test(url)) {
url = url.replace(/\.otapkg(\?|$)/, '.apk$1');
}
var nameMatch = url.match(/\/([^/?#]+\.apk)(?:\?|$)/i);
var filename = nameMatch ? nameMatch[1] : 'android_cast-latest.apk';
return { url: url, filename: filename, channel: channelName };
});
}
function resolveApkUrl() {
var chain = Promise.reject(new Error('no channel'));
OTA_CHANNELS.forEach(function (ch) {
chain = chain.catch(function () {
return resolveApkFromChannel(ch);
});
});
return chain;
}
function triggerDownload(url, filename) {
if (/\.apk(\?|$)/i.test(url)) {
var a = document.createElement('a');
a.href = url;
a.download = filename;
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
return Promise.resolve();
}
return fetch(url, { cache: 'no-store' })
.then(function (r) {
if (!r.ok) {
throw new Error('download HTTP ' + r.status);
}
return r.blob();
})
.then(function (blob) {
var blobUrl = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
link.rel = 'noopener';
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(function () {
URL.revokeObjectURL(blobUrl);
}, 60000);
});
}
@@ -44,14 +94,17 @@
}
btn.addEventListener('click', function () {
btn.disabled = true;
setStatus('Resolving stable OTA channel…');
resolveStableApkUrl()
.then(function (url) {
setStatus('Downloading…');
window.location.href = url;
setStatus('Resolving OTA channel…');
resolveApkUrl()
.then(function (resolved) {
setStatus('Downloading ' + resolved.filename + ' (' + resolved.channel + ')…');
return triggerDownload(resolved.url, resolved.filename);
})
.catch(function (err) {
setStatus('APK unavailable — run builder OTA publish (stable). ' + (err.message || err));
setStatus(
'APK unavailable — run builder with OTA publish (staging). ' +
(err.message || err)
);
})
.finally(function () {
btn.disabled = false;

View File

@@ -67,7 +67,7 @@
document.documentElement.setAttribute('lang', lang);
var footerLeft = document.querySelector('.platform-footer__left[data-i18n]');
if (footerLeft) {
var holder = footerLeft.getAttribute('data-holder') || 'Anton Afanaasyeu';
var holder = footerLeft.getAttribute('data-holder') || 'Anton Afanasyeu';
var year = footerLeft.getAttribute('data-year') || String(new Date().getFullYear());
footerLeft.textContent = t('footer.copyright', { holder: holder, year: year });
}

View File

@@ -60,10 +60,10 @@
"page6.btn_docs": "Other documents",
"page6.hint": "Wiki opens in Gitea; other documents lists ac-docs drafts, DRs, and specs.",
"page7.title": "Downloads",
"page7.lead": "Latest stable builds from the orchestration pipeline (next integration).",
"page7.lead": "Latest builds from the orchestration pipeline (staging channel, branch next).",
"page7.btn_apk": "Android APK",
"page7.btn_studio": "Session Studio",
"page7.hint": "APK matches the stable OTA channel signature and version.",
"page7.hint": "APK is the installable package from the staging OTA channel (falls back to stable).",
"page8.title": "Resources",
"page8.lead": "Source control, consoles, and integration entry points.",
"page8.btn_git": "Git Access",

View File

@@ -60,10 +60,10 @@
"page6.btn_docs": "Прочие документы",
"page6.hint": "Wiki в Gitea; каталог — черновики ac-docs, DR и specs.",
"page7.title": "Загрузки",
"page7.lead": "Последние stable-сборки из orchestration (ветка next).",
"page7.lead": "Последние сборки из orchestration (канал staging, ветка next).",
"page7.btn_apk": "Android APK",
"page7.btn_studio": "Session Studio",
"page7.hint": "APK совпадает с stable OTA-каналом.",
"page7.hint": "APK — установочный пакет из staging OTA-канала (fallback на stable).",
"page8.title": "Ресурсы",
"page8.lead": "Исходники, консоли и точки интеграции.",
"page8.btn_git": "Git Access",

View File

@@ -259,12 +259,28 @@
gap: 12px;
margin-top: 20px;
}
.landing-gallery img {
.landing-gallery__tile {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 10px;
border: 1px solid var(--border);
background: linear-gradient(145deg, rgba(61, 139, 253, 0.22), rgba(12, 24, 48, 0.85));
}
.landing-gallery__tile--network {
background: linear-gradient(145deg, rgba(61, 139, 253, 0.35), rgba(8, 18, 36, 0.9));
}
.landing-gallery__tile--cluster {
background: linear-gradient(145deg, rgba(46, 196, 182, 0.28), rgba(8, 18, 36, 0.9));
}
.landing-gallery__tile--ops {
background: linear-gradient(145deg, rgba(255, 159, 67, 0.25), rgba(8, 18, 36, 0.9));
}
.landing-gallery__tile--team {
background: linear-gradient(145deg, rgba(155, 89, 182, 0.28), rgba(8, 18, 36, 0.9));
}
.landing-gallery__tile--collab {
background: linear-gradient(145deg, rgba(61, 139, 253, 0.22), rgba(46, 196, 182, 0.18));
}
.landing-feature-grid {
@@ -475,14 +491,63 @@
.landing-rail { transition: none; }
}
/* Downloads / documentation big rounded CTAs */
/* Downloads — tall cards: icon on top, resource name below */
.landing-download-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
gap: 20px;
margin: 20px 0 12px;
}
.landing-download-card {
min-width: 200px;
width: min(220px, 100%);
min-height: 168px;
padding: 24px 20px 20px;
border-radius: 18px;
font-size: 1rem;
font-weight: 600;
text-align: center;
text-decoration: none;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
transition: box-shadow 0.2s ease, transform 0.15s ease;
border: 1px solid rgba(61, 139, 253, 0.35);
}
.landing-download-card__icon {
width: 72px;
height: 72px;
flex-shrink: 0;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
opacity: 0.95;
}
.landing-download-card__icon--android {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%233DDC84'%3E%3Cpath d='M17.6 9.48l1.84-3.18c.16-.31-.04-.68-.38-.74a.495.495 0 0 0-.55.27l-1.87 3.24a11.43 11.43 0 0 0-8.12 0L6.65 5.83a.495.495 0 0 0-.55-.27c-.34.06-.54.43-.38.74L7.56 9.48C4.01 11.27 1.46 14.79 1 19h22c-.46-4.21-3.01-7.73-6.54-9.52zM7 16.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm10 0c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z'/%3E%3C/svg%3E");
}
.landing-download-card__icon--studio {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23F4A460'%3E%3Cpath d='M6 3h12l1 3H5l1-3zm-1 5h14v11c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2V8zm4 2v2h2v-2H9zm4 0v2h2v-2h-2z'/%3E%3Cpath d='M8 19h8v1H8v-1z' fill='%23654321'/%3E%3C/svg%3E");
}
.landing-download-card__label {
line-height: 1.25;
letter-spacing: 0.01em;
}
.landing-download-card:hover,
.landing-download-card:focus-visible {
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(61, 139, 253, 0.45);
}
/* Legacy pill buttons (documentation page, resources) */
.landing-download-btn {
min-width: 200px;
min-height: 56px;

View File

@@ -39,9 +39,9 @@ function hub_h(string $s): string {
<link rel="icon" type="image/x-icon" href="/app/androidcast_project/favicon.ico">
<link rel="apple-touch-icon" href="/app/androidcast_project/apple-touch-icon.ico">
<link rel="stylesheet" href="/app/androidcast_project/issues/assets/css/app.css">
<link rel="stylesheet" href="hub.css?v=20260610overlay">
<link rel="stylesheet" href="hub-logo-layers.css?v=20260610overlay">
<link rel="stylesheet" href="hub-landing.css?v=20260613layout">
<link rel="stylesheet" href="hub.css?v=20260711overlay">
<link rel="stylesheet" href="hub-logo-layers.css?v=20260711overlay">
<link rel="stylesheet" href="hub-landing.css?v=20260711layout">
<script src="/app/androidcast_project/issues/assets/js/nav_shell.js" defer></script>
<script src="assets/hub-i18n.js" defer></script>
<script src="assets/hub-landing.js" defer></script>
@@ -139,55 +139,14 @@ function hub_h(string $s): string {
</header>
<div class="landing-frame">
<nav class="nav-pane landing-left" id="landing-nav-pane" aria-label="Android Cast apps">
<button type="button" class="nav-handle" id="landing-nav-handle" data-i18n-aria="nav.toggle" data-i18n-title="nav.toggle" aria-label="Navigation" title="Navigation">
<span class="nav-icon nav-icon--menu" aria-hidden="true"></span>
</button>
<ul class="nav-list">
<li><a href="issues/?view=home" class="nav-link" data-i18n-title="nav.console" title="Console"><span class="nav-icon nav-icon--home"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.console">Console</span><span class="nav-desc" data-i18n="nav.console_desc">Crash triage home and workspace cards.</span></span></a></li>
<li><a href="git/" class="nav-link" data-i18n-title="nav.git" title="Git"><span class="nav-icon nav-icon--git"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.git">Git</span><span class="nav-desc" data-i18n="nav.git_desc">Browse repos, branches, and tags.</span></span></a></li>
<li><a href="issues/?view=reports" class="nav-link" data-i18n-title="nav.issues" title="Issues"><span class="nav-icon nav-icon--reports"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.issues">Issues</span><span class="nav-desc" data-i18n="nav.issues_desc">Browse and triage crash reports.</span></span></a></li>
<li><a href="issues/?view=tickets" class="nav-link" data-i18n-title="nav.tickets" title="Tickets"><span class="nav-icon nav-icon--tickets"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.tickets">Tickets</span><span class="nav-desc" data-i18n="nav.tickets_desc">Roadmap tasks, QA items, and tags.</span></span></a></li>
<li><a href="graphs/" class="nav-link" data-i18n-title="nav.graphs" title="Analytics"><span class="nav-icon nav-icon--graphs"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.graphs">Analytics</span><span class="nav-desc" data-i18n="nav.graphs_desc">Sessions, live cast stats, and device activity.</span></span></a></li>
<li><a href="monitor/" class="nav-link" data-i18n-title="nav.monitor" title="Monitoring"><span class="nav-icon nav-icon--monitor"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.monitor">Monitoring</span><span class="nav-desc" data-i18n="nav.monitor_desc">Grafana dashboards — cluster health, alerts, exporters.</span></span></a></li>
<li><a href="issues/?view=live_sessions" class="nav-link" data-i18n-title="nav.live_sessions" title="Live sessions"><span class="nav-icon nav-icon--live"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.live_sessions">Live sessions</span><span class="nav-desc" data-i18n="nav.live_sessions_desc">Cast intents, join stats, session history.</span></span></a></li>
<li><a href="issues/live/education" class="nav-link" data-i18n-title="nav.education" title="Education demo"><span class="nav-icon nav-icon--education"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.education">Education (demo)</span><span class="nav-desc" data-i18n="nav.education_desc">Browser screen-share trial (5 min).</span></span></a></li>
<li><a href="issues/?view=remote_access" class="nav-link" data-i18n-title="nav.remote" title="Remote access"><span class="nav-icon nav-icon--remote"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.remote">Remote access</span><span class="nav-desc" data-i18n="nav.remote_desc">WireGuard sessions and device reachability.</span></span></a></li>
<li><a href="monitor/" class="nav-link" data-i18n-title="nav.monitor" title="Monitoring"><span class="nav-icon nav-icon--monitor"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.monitor">Monitoring</span><span class="nav-desc" data-i18n="nav.monitor_desc">Grafana dashboards — cluster health, alerts, exporters.</span></span></a></li>
<li><a href="issues/?view=short_links" class="nav-link" data-i18n-title="nav.short_links" title="Short links"><span class="nav-icon nav-icon--link"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.short_links">Short links</span><span class="nav-desc" data-i18n="nav.short_links_desc">Mint bearer tokens and shorten URLs for s.f0xx.org.</span></span></a></li>
<li><a href="build/" class="nav-link" data-i18n-title="nav.builder" title="Builder"><span class="nav-icon nav-icon--builder"></span><span class="nav-text"><span class="nav-label" data-i18n="nav.builder">Builder</span><span class="nav-desc" data-i18n="nav.builder_desc">Docker CI for APK baking and OTA artifacts.</span></span></a></li>
</ul>
<div class="nav-user">
<?php if ($hubUsername !== ''): ?>
<span class="nav-user-name" title="<?= hub_h($hubUsername) ?>">
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
<span class="nav-label"><?= hub_h($hubUsername) ?></span>
</span>
<a href="<?= hub_h($hubIssuesBase) ?>/account-security"
class="nav-link"
data-i18n-aria="nav.security" data-i18n-title="nav.security"
aria-label="Security" title="Security">
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
<span class="nav-label" data-i18n="nav.security">Security</span>
</a>
<a href="<?= hub_h($hubProjectBase) ?>/logout"
class="nav-link nav-link--logout"
data-i18n-aria="nav.logout" data-i18n-title="nav.logout"
aria-label="Logout" title="Logout">
<span class="nav-icon nav-icon--logout" aria-hidden="true"></span>
<span class="nav-label" data-i18n="nav.logout">Logout</span>
</a>
<?php else: ?>
<a href="<?= hub_h($hubProjectBase) ?>/login"
class="nav-link"
data-i18n-aria="nav.sign_in" data-i18n-title="nav.sign_in"
aria-label="Sign in" title="Sign in">
<span class="nav-icon nav-icon--user" aria-hidden="true"></span>
<span class="nav-label" data-i18n="nav.sign_in">Sign in</span>
</a>
<?php endif; ?>
</div>
</nav>
<?php platform_render_nav_shell([
'project_base' => $hubProjectBase,
'issues_base' => $hubIssuesBase,
'active' => '',
'username' => $hubUsername,
'pane_id' => 'landing-nav-pane',
'handle_id' => 'landing-nav-handle',
]); ?>
<main class="landing-scroll" id="landing-scroll" tabindex="-1">
<?php require __DIR__ . '/landing-pages.inc.php'; ?>
@@ -208,16 +167,6 @@ function hub_h(string $s): string {
'use_i18n' => false,
'extra_class' => 'bottom-bar',
]); ?>
<script>
(function () {
var footerLeft = document.querySelector('.platform-footer__left');
if (footerLeft) {
footerLeft.setAttribute('data-i18n', 'footer.copyright');
footerLeft.setAttribute('data-holder', 'Anton Afanaasyeu');
footerLeft.setAttribute('data-year', '2026 - current');
}
})();
</script>
<script>
(function () {
var sel = document.getElementById('theme-select');

View File

@@ -32,10 +32,10 @@
<div class="landing-page-inner card card--lift">
<h2 data-i18n="page3.title">About</h2>
<p class="landing-lead" data-i18n="page3.lead">Android Cast connects devices, backends, and operators across your stack.</p>
<div class="landing-gallery">
<img src="https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=400&amp;h=300&amp;fit=crop" alt="" loading="lazy" decoding="async">
<img src="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&amp;h=300&amp;fit=crop" alt="" loading="lazy" decoding="async">
<img src="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=400&amp;h=300&amp;fit=crop" alt="" loading="lazy" decoding="async">
<div class="landing-gallery" aria-hidden="true">
<span class="landing-gallery__tile landing-gallery__tile--network"></span>
<span class="landing-gallery__tile landing-gallery__tile--cluster"></span>
<span class="landing-gallery__tile landing-gallery__tile--ops"></span>
</div>
</div>
</section>
@@ -80,12 +80,18 @@
<section class="landing-page" id="landing-page-7" data-page="7" aria-label="Downloads">
<div class="landing-page-inner card card--lift">
<h2 data-i18n="page7.title">Downloads</h2>
<p class="landing-lead" data-i18n="page7.lead">Latest stable builds from the orchestration pipeline (<code>next</code> integration).</p>
<p class="landing-lead" data-i18n="page7.lead">Latest builds from the orchestration pipeline (<code>staging</code> channel, branch <code>next</code>).</p>
<div class="landing-download-grid">
<button type="button" class="landing-download-btn btn btn-primary" id="hub-download-apk" data-i18n="page7.btn_apk">Android APK</button>
<a class="landing-download-btn btn btn-primary" href="/v0/downloads/ac-session-studio.jar" download data-i18n="page7.btn_studio">Session Studio</a>
<button type="button" class="landing-download-card btn btn-primary" id="hub-download-apk">
<span class="landing-download-card__icon landing-download-card__icon--android" aria-hidden="true"></span>
<span class="landing-download-card__label" data-i18n="page7.btn_apk">Android APK</span>
</button>
<a class="landing-download-card btn btn-primary" href="/v0/downloads/ac-session-studio.jar" download>
<span class="landing-download-card__icon landing-download-card__icon--studio" aria-hidden="true"></span>
<span class="landing-download-card__label" data-i18n="page7.btn_studio">Session Studio</span>
</a>
</div>
<p class="muted landing-download-hint" id="hub-download-apk-status" data-i18n="page7.hint">APK matches the stable OTA channel signature and version.</p>
<p class="muted landing-download-hint" id="hub-download-apk-status" data-i18n="page7.hint">APK is the installable package from the staging OTA channel (falls back to stable).</p>
</div>
</section>
@@ -104,9 +110,9 @@
<div class="landing-page-inner card card--lift">
<h2 data-i18n="page9.title">People</h2>
<p class="landing-lead" data-i18n="page9.lead">Engineers and operators shipping cast, crash, and build workflows every day.</p>
<div class="landing-gallery">
<img src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&amp;h=300&amp;fit=crop" alt="" loading="lazy" decoding="async">
<img src="https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=400&amp;h=300&amp;fit=crop" alt="" loading="lazy" decoding="async">
<div class="landing-gallery" aria-hidden="true">
<span class="landing-gallery__tile landing-gallery__tile--team"></span>
<span class="landing-gallery__tile landing-gallery__tile--collab"></span>
</div>
</div>
</section>