diff --git a/assets/hub-downloads.js b/assets/hub-downloads.js index 0ef2aba..979f070 100644 --- a/assets/hub-downloads.js +++ b/assets/hub-downloads.js @@ -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) { - if (!r.ok) { - throw new Error('stable channel HTTP ' + r.status); - } - return r.json(); - }) + function fetchJson(url) { + return fetch(url, { cache: 'no-store' }).then(function (r) { + if (!r.ok) { + 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('stable.json missing manifestUrl'); + throw new Error(channelName + ' missing manifestUrl'); } - return fetch(manifestUrl, { cache: 'no-store' }).then(function (r) { - if (!r.ok) { - throw new Error('manifest HTTP ' + r.status); - } - return r.json(); - }); + 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; diff --git a/assets/hub-i18n.js b/assets/hub-i18n.js index afd13ad..ff0718e 100644 --- a/assets/hub-i18n.js +++ b/assets/hub-i18n.js @@ -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 }); } diff --git a/assets/i18n/hub-en.json b/assets/i18n/hub-en.json index 876543a..3d61ae4 100644 --- a/assets/i18n/hub-en.json +++ b/assets/i18n/hub-en.json @@ -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", diff --git a/assets/i18n/hub-ru.json b/assets/i18n/hub-ru.json index 70b6d32..8c92e99 100644 --- a/assets/i18n/hub-ru.json +++ b/assets/i18n/hub-ru.json @@ -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", diff --git a/hub-landing.css b/hub-landing.css index db993fc..91f5979 100644 --- a/hub-landing.css +++ b/hub-landing.css @@ -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; diff --git a/index.php b/index.php index 072e4dc..6f7137e 100644 --- a/index.php +++ b/index.php @@ -39,9 +39,9 @@ function hub_h(string $s): string { - - - + + + @@ -139,55 +139,14 @@ function hub_h(string $s): string {