mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
landing sync, multipage
This commit is contained in:
@@ -20,6 +20,8 @@ On FE, hub and (if you fix it) crashes should both target **`http://artc0.intra.
|
|||||||
|
|
||||||
Landing uses **`index.php`** (shared footer from `examples/platform/footer.config.php`). Copy **`index.html`** only as a redirect fallback.
|
Landing uses **`index.php`** (shared footer from `examples/platform/footer.config.php`). Copy **`index.html`** only as a redirect fallback.
|
||||||
|
|
||||||
|
**Multipage landing (2026-06):** sync `hub-landing.css`, `landing-pages.inc.php`, `assets/hub-landing.js`, `assets/hub-i18n.js`, `assets/i18n/hub-en.json`, `assets/i18n/hub-ru.json` with `index.php`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .../android_cast/examples/app_hub/index.php \
|
cp .../android_cast/examples/app_hub/index.php \
|
||||||
.../htdocs/apps/app/androidcast_project/index.php
|
.../htdocs/apps/app/androidcast_project/index.php
|
||||||
|
|||||||
104
examples/app_hub/assets/hub-i18n.js
Normal file
104
examples/app_hub/assets/hub-i18n.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
(function (global) {
|
||||||
|
'use strict';
|
||||||
|
var STORAGE_KEY = 'crash_console_lang';
|
||||||
|
var SUPPORTED = ['en', 'ru'];
|
||||||
|
var DEFAULT_LANG = 'en';
|
||||||
|
var LANG_DISPLAY = { en: { flag: '\uD83C\uDDEC\uD83C\uDDE7', code: 'EN' }, ru: { flag: '\uD83C\uDDF7\uD83C\uDDFA', code: 'RU' } };
|
||||||
|
var lang = DEFAULT_LANG;
|
||||||
|
var catalog = {};
|
||||||
|
var fallback = {};
|
||||||
|
|
||||||
|
function hubAsset(rel) {
|
||||||
|
var link = document.querySelector('link[href*="hub.css"]');
|
||||||
|
var base = link ? (link.getAttribute('href') || '').replace(/[^/]*$/, '') : '';
|
||||||
|
return base + rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function storedLang() {
|
||||||
|
try {
|
||||||
|
var v = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return SUPPORTED.indexOf(v) >= 0 ? v : DEFAULT_LANG;
|
||||||
|
} catch (e) {
|
||||||
|
return DEFAULT_LANG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function t(key, params) {
|
||||||
|
var s = catalog[key] || fallback[key] || key;
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach(function (k) {
|
||||||
|
s = s.split('{' + k + '}').join(String(params[k]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply(root) {
|
||||||
|
var scope = root || document;
|
||||||
|
scope.querySelectorAll('[data-i18n]').forEach(function (el) {
|
||||||
|
var key = el.getAttribute('data-i18n');
|
||||||
|
if (!key) return;
|
||||||
|
el.textContent = t(key);
|
||||||
|
});
|
||||||
|
scope.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
|
||||||
|
var key = el.getAttribute('data-i18n-placeholder');
|
||||||
|
if (key) el.setAttribute('placeholder', t(key));
|
||||||
|
});
|
||||||
|
scope.querySelectorAll('[data-i18n-aria]').forEach(function (el) {
|
||||||
|
var key = el.getAttribute('data-i18n-aria');
|
||||||
|
if (key) el.setAttribute('aria-label', t(key));
|
||||||
|
});
|
||||||
|
scope.querySelectorAll('[data-i18n-title]').forEach(function (el) {
|
||||||
|
var key = el.getAttribute('data-i18n-title');
|
||||||
|
if (key) el.setAttribute('title', t(key));
|
||||||
|
});
|
||||||
|
var flag = document.getElementById('locale-flag');
|
||||||
|
var code = document.getElementById('locale-code');
|
||||||
|
var disp = LANG_DISPLAY[lang] || LANG_DISPLAY.en;
|
||||||
|
if (flag) flag.textContent = disp.flag;
|
||||||
|
if (code) code.textContent = disp.code;
|
||||||
|
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 year = footerLeft.getAttribute('data-year') || String(new Date().getFullYear());
|
||||||
|
footerLeft.textContent = t('footer.copyright', { holder: holder, year: year });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(code) {
|
||||||
|
lang = SUPPORTED.indexOf(code) >= 0 ? code : DEFAULT_LANG;
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, lang); } catch (e) {}
|
||||||
|
return fetch(hubAsset('assets/i18n/hub-' + lang + '.json'), { credentials: 'same-origin' })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (json) {
|
||||||
|
catalog = json || {};
|
||||||
|
if (lang !== DEFAULT_LANG) {
|
||||||
|
return fetch(hubAsset('assets/i18n/hub-en.json'), { credentials: 'same-origin' })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (fb) { fallback = fb || {}; });
|
||||||
|
}
|
||||||
|
fallback = catalog;
|
||||||
|
})
|
||||||
|
.then(function () { apply(document); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var sel = document.getElementById('lang-select');
|
||||||
|
lang = storedLang();
|
||||||
|
if (sel) sel.value = lang;
|
||||||
|
load(lang).then(function () {
|
||||||
|
if (sel) {
|
||||||
|
sel.addEventListener('change', function () { load(sel.value); });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
global.HubI18n = { t: t, apply: apply, load: load, init: init };
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})(window);
|
||||||
166
examples/app_hub/assets/hub-landing.js
Normal file
166
examples/app_hub/assets/hub-landing.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var PAGE_COUNT = 10;
|
||||||
|
var scrollEl = null;
|
||||||
|
var railEl = null;
|
||||||
|
var currentPage = 1;
|
||||||
|
|
||||||
|
function pageEl(n) {
|
||||||
|
return document.getElementById('landing-page-' + n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivePage(n, opts) {
|
||||||
|
n = Math.max(1, Math.min(PAGE_COUNT, n | 0));
|
||||||
|
currentPage = n;
|
||||||
|
var page = pageEl(n);
|
||||||
|
if (page && scrollEl) {
|
||||||
|
if (!opts || !opts.skipScroll) {
|
||||||
|
page.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.landing-rail-dot').forEach(function (dot) {
|
||||||
|
var p = Number(dot.getAttribute('data-page'));
|
||||||
|
dot.classList.toggle('is-active', p === n);
|
||||||
|
dot.setAttribute('aria-current', p === n ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-goto-page]').forEach(function (el) {
|
||||||
|
var p = Number(el.getAttribute('data-goto-page'));
|
||||||
|
el.classList.toggle('is-active', p === n);
|
||||||
|
});
|
||||||
|
if (window.history && window.history.replaceState) {
|
||||||
|
var url = new URL(window.location.href);
|
||||||
|
if (n <= 1) {
|
||||||
|
url.searchParams.delete('page');
|
||||||
|
} else {
|
||||||
|
url.searchParams.set('page', String(n));
|
||||||
|
}
|
||||||
|
window.history.replaceState({ page: n }, '', url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageFromScroll() {
|
||||||
|
if (!scrollEl) return 1;
|
||||||
|
var mid = scrollEl.scrollTop + scrollEl.clientHeight * 0.4;
|
||||||
|
var best = 1;
|
||||||
|
var bestDist = Infinity;
|
||||||
|
for (var i = 1; i <= PAGE_COUNT; i++) {
|
||||||
|
var el = pageEl(i);
|
||||||
|
if (!el) continue;
|
||||||
|
var dist = Math.abs(el.offsetTop - mid);
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindScrollSync() {
|
||||||
|
if (!scrollEl) return;
|
||||||
|
var ticking = false;
|
||||||
|
scrollEl.addEventListener('scroll', function () {
|
||||||
|
if (ticking) return;
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
ticking = false;
|
||||||
|
var p = pageFromScroll();
|
||||||
|
if (p !== currentPage) {
|
||||||
|
setActivePage(p, { skipScroll: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRail() {
|
||||||
|
railEl = document.getElementById('landing-rail');
|
||||||
|
var handle = document.getElementById('landing-rail-handle');
|
||||||
|
if (handle && railEl) {
|
||||||
|
handle.addEventListener('mouseenter', function () {
|
||||||
|
railEl.classList.add('is-visible');
|
||||||
|
});
|
||||||
|
handle.addEventListener('focus', function () {
|
||||||
|
railEl.classList.add('is-visible');
|
||||||
|
});
|
||||||
|
var dragY = 0;
|
||||||
|
handle.addEventListener('pointerdown', function (ev) {
|
||||||
|
dragY = ev.clientY;
|
||||||
|
handle.setPointerCapture(ev.pointerId);
|
||||||
|
railEl.classList.add('is-visible');
|
||||||
|
});
|
||||||
|
handle.addEventListener('pointermove', function (ev) {
|
||||||
|
if (!handle.hasPointerCapture(ev.pointerId)) return;
|
||||||
|
var dy = ev.clientY - dragY;
|
||||||
|
if (Math.abs(dy) > 40) {
|
||||||
|
setActivePage(currentPage + (dy > 0 ? 1 : -1));
|
||||||
|
dragY = ev.clientY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.landing-rail-dot').forEach(function (dot) {
|
||||||
|
dot.addEventListener('click', function () {
|
||||||
|
setActivePage(Number(dot.getAttribute('data-page')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindGotoLinks() {
|
||||||
|
document.body.addEventListener('click', function (ev) {
|
||||||
|
var t = ev.target.closest('[data-goto-page]');
|
||||||
|
if (!t) return;
|
||||||
|
if (t.tagName === 'A' && t.getAttribute('href') === '#') {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
setActivePage(Number(t.getAttribute('data-goto-page')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLeftNav() {
|
||||||
|
var pane = document.getElementById('landing-nav-pane');
|
||||||
|
var handle = document.getElementById('landing-nav-handle');
|
||||||
|
if (handle && pane) {
|
||||||
|
handle.addEventListener('click', function () {
|
||||||
|
pane.classList.toggle('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFromUrl() {
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
var p = parseInt(params.get('page') || '1', 10);
|
||||||
|
if (p >= 1 && p <= PAGE_COUNT) {
|
||||||
|
setTimeout(function () {
|
||||||
|
setActivePage(p);
|
||||||
|
}, 80);
|
||||||
|
} else {
|
||||||
|
setActivePage(1, { skipScroll: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
scrollEl = document.getElementById('landing-scroll');
|
||||||
|
bindScrollSync();
|
||||||
|
bindRail();
|
||||||
|
bindGotoLinks();
|
||||||
|
bindLeftNav();
|
||||||
|
initFromUrl();
|
||||||
|
window.addEventListener('keydown', function (ev) {
|
||||||
|
if (ev.target && /input|textarea|select/i.test(ev.target.tagName)) return;
|
||||||
|
if (ev.key === 'PageDown' || ev.key === 'ArrowDown') {
|
||||||
|
ev.preventDefault();
|
||||||
|
setActivePage(currentPage + 1);
|
||||||
|
} else if (ev.key === 'PageUp' || ev.key === 'ArrowUp') {
|
||||||
|
ev.preventDefault();
|
||||||
|
setActivePage(currentPage - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.LandingNav = { goTo: setActivePage };
|
||||||
|
})();
|
||||||
44
examples/app_hub/assets/i18n/hub-en.json
Normal file
44
examples/app_hub/assets/i18n/hub-en.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"nav.androidcast": "AndroidCast",
|
||||||
|
"nav.about": "About",
|
||||||
|
"nav.why": "Why",
|
||||||
|
"nav.tech": "Technologies",
|
||||||
|
"nav.resources": "Resources",
|
||||||
|
"nav.people": "People",
|
||||||
|
"nav.culture": "Culture",
|
||||||
|
"nav.pricing": "Pricing",
|
||||||
|
"nav.contact": "Contact us",
|
||||||
|
"nav.git": "Git",
|
||||||
|
"nav.tickets": "Tickets",
|
||||||
|
"nav.crashes": "Crashes",
|
||||||
|
"nav.graphs": "Graphs",
|
||||||
|
"nav.remote": "Remote access",
|
||||||
|
"nav.builder": "Builder",
|
||||||
|
"nav.toggle": "Navigation",
|
||||||
|
"lang.label": "Language",
|
||||||
|
"theme.label": "Theme",
|
||||||
|
"rail.label": "Page navigation",
|
||||||
|
"rail.handle": "Show page navigation",
|
||||||
|
"page1.title": "Android Cast",
|
||||||
|
"page1.lead": "Screen casting, crash intelligence, and CI — one ecosystem.",
|
||||||
|
"page1.cta": "Explore AndroidCast",
|
||||||
|
"page2.title": "AndroidCast",
|
||||||
|
"page2.lead": "Mirror Android screens with low latency, native codecs, and operator-grade tooling.",
|
||||||
|
"page3.title": "About",
|
||||||
|
"page3.lead": "Android Cast connects devices, backends, and operators across your stack.",
|
||||||
|
"page4.title": "Why AndroidCast",
|
||||||
|
"page4.lead": "Built for teams who need reliability, observability, and control — not another black box.",
|
||||||
|
"page5.title": "Technologies",
|
||||||
|
"page5.lead": "WebRTC, Opus/Speex, WireGuard remote access, PHP consoles, and Docker CI pipelines.",
|
||||||
|
"page6.title": "Resources",
|
||||||
|
"page6.lead": "Documentation, deployment guides, and integration patterns for your environment.",
|
||||||
|
"page7.title": "People",
|
||||||
|
"page7.lead": "Engineers and operators shipping cast, crash, and build workflows every day.",
|
||||||
|
"page8.title": "Culture",
|
||||||
|
"page8.lead": "Open tooling, green master branches, and honest postmortems.",
|
||||||
|
"page9.title": "Pricing",
|
||||||
|
"page9.lead": "Self-hosted core; enterprise options for SLA, SSO, and dedicated builders.",
|
||||||
|
"page10.title": "Contact us",
|
||||||
|
"page10.lead": "Reach the maintainer for partnerships, support, or deployment help.",
|
||||||
|
"footer.copyright": "© {holder}, {year}"
|
||||||
|
}
|
||||||
44
examples/app_hub/assets/i18n/hub-ru.json
Normal file
44
examples/app_hub/assets/i18n/hub-ru.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"nav.androidcast": "AndroidCast",
|
||||||
|
"nav.about": "О проекте",
|
||||||
|
"nav.why": "Зачем",
|
||||||
|
"nav.tech": "Технологии",
|
||||||
|
"nav.resources": "Ресурсы",
|
||||||
|
"nav.people": "Люди",
|
||||||
|
"nav.culture": "Культура",
|
||||||
|
"nav.pricing": "Цены",
|
||||||
|
"nav.contact": "Контакты",
|
||||||
|
"nav.git": "Git",
|
||||||
|
"nav.tickets": "Тикеты",
|
||||||
|
"nav.crashes": "Крэши",
|
||||||
|
"nav.graphs": "Графики",
|
||||||
|
"nav.remote": "Удалённый доступ",
|
||||||
|
"nav.builder": "Сборщик",
|
||||||
|
"nav.toggle": "Навигация",
|
||||||
|
"lang.label": "Язык",
|
||||||
|
"theme.label": "Тема",
|
||||||
|
"rail.label": "Навигация по страницам",
|
||||||
|
"rail.handle": "Показать навигацию",
|
||||||
|
"page1.title": "Android Cast",
|
||||||
|
"page1.lead": "Трансляция экрана, аналитика сбоев и CI — одна экосистема.",
|
||||||
|
"page1.cta": "Узнать об AndroidCast",
|
||||||
|
"page2.title": "AndroidCast",
|
||||||
|
"page2.lead": "Зеркалирование Android с низкой задержкой, нативными кодеками и инструментами оператора.",
|
||||||
|
"page3.title": "О проекте",
|
||||||
|
"page3.lead": "Android Cast связывает устройства, бэкенды и операторов в едином стеке.",
|
||||||
|
"page4.title": "Зачем AndroidCast",
|
||||||
|
"page4.lead": "Для команд, которым нужны надёжность, наблюдаемость и контроль — не чёрный ящик.",
|
||||||
|
"page5.title": "Технологии",
|
||||||
|
"page5.lead": "WebRTC, Opus/Speex, WireGuard, PHP-консоли и Docker CI.",
|
||||||
|
"page6.title": "Ресурсы",
|
||||||
|
"page6.lead": "Документация, деплой и паттерны интеграции для вашей среды.",
|
||||||
|
"page7.title": "Люди",
|
||||||
|
"page7.lead": "Инженеры и операторы, которые каждый день развивают cast, crash и сборки.",
|
||||||
|
"page8.title": "Культура",
|
||||||
|
"page8.lead": "Открытые инструменты, зелёный master и честные postmortem.",
|
||||||
|
"page9.title": "Цены",
|
||||||
|
"page9.lead": "Self-hosted ядро; enterprise — SLA, SSO и выделенные builder-ы.",
|
||||||
|
"page10.title": "Контакты",
|
||||||
|
"page10.lead": "Связь с maintainer: партнёрство, поддержка, помощь с деплоем.",
|
||||||
|
"footer.copyright": "© {holder}, {year}"
|
||||||
|
}
|
||||||
444
examples/app_hub/hub-landing.css
Normal file
444
examples/app_hub/hub-landing.css
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
/* Multipage landing shell — f0xx.org / lovable-style layout on hub tokens */
|
||||||
|
|
||||||
|
.hub-landing {
|
||||||
|
--landing-header-h: 56px;
|
||||||
|
--landing-footer-h: 40px;
|
||||||
|
--landing-left-w: 56px;
|
||||||
|
--landing-rail-w: 44px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (1) Top header */
|
||||||
|
.landing-top {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 20;
|
||||||
|
height: var(--landing-header-h);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px 0 12px;
|
||||||
|
background: rgba(15, 20, 25, 0.72);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid rgba(45, 58, 79, 0.65);
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
[data-theme="light"] .landing-top {
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
border-bottom-color: rgba(197, 208, 224, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-top-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.landing-top-logo:hover { color: var(--text); text-decoration: none; }
|
||||||
|
.landing-top-logo img {
|
||||||
|
width: 36px;
|
||||||
|
height: 27px;
|
||||||
|
}
|
||||||
|
.landing-top-logo strong {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-top-nav {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-menu-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.landing-menu-btn,
|
||||||
|
.landing-menu-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
.landing-menu-link:hover,
|
||||||
|
.landing-menu-btn:hover,
|
||||||
|
.landing-menu-item:focus-within > .landing-menu-btn {
|
||||||
|
background: rgba(61, 139, 253, 0.14);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.landing-menu-btn::after {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-top: 5px solid currentColor;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.landing-menu-link::after { display: none; }
|
||||||
|
|
||||||
|
.landing-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(6px);
|
||||||
|
min-width: 168px;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), 0 0 20px rgba(61, 139, 253, 0.12);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
.landing-menu-item:hover .landing-dropdown,
|
||||||
|
.landing-menu-item:focus-within .landing-dropdown,
|
||||||
|
.landing-menu-item.is-open .landing-dropdown {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
.landing-dropdown a {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.landing-dropdown a:hover,
|
||||||
|
.landing-dropdown a.is-active {
|
||||||
|
background: rgba(61, 139, 253, 0.16);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-top-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.landing-top-tools .toolbar-select {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frame: (2) left + (3) body + space for (4) rail */
|
||||||
|
.landing-frame {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--landing-header-h);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--landing-footer-h);
|
||||||
|
display: flex;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (2) Left shell — console nav-pane */
|
||||||
|
.landing-left.nav-pane {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 var(--landing-left-w);
|
||||||
|
width: var(--landing-left-w);
|
||||||
|
height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: auto;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.landing-left.nav-pane.open {
|
||||||
|
width: 200px;
|
||||||
|
flex-basis: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (3) Body — full viewport pages, scroll snap */
|
||||||
|
.landing-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
pointer-events: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-page {
|
||||||
|
scroll-snap-align: start;
|
||||||
|
scroll-snap-stop: always;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 48px 32px 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-page-inner {
|
||||||
|
width: min(920px, 100%);
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-page-inner.card.card--lift {
|
||||||
|
padding: 28px 32px;
|
||||||
|
background: rgba(26, 35, 50, 0.78);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(45, 58, 79, 0.85);
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35), 0 0 32px rgba(61, 139, 253, 0.08);
|
||||||
|
}
|
||||||
|
[data-theme="light"] .landing-page-inner.card.card--lift {
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-page h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.landing-page .landing-lead {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 52ch;
|
||||||
|
}
|
||||||
|
.landing-page .btn { margin-left: 0; }
|
||||||
|
|
||||||
|
.landing-gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.landing-gallery img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.landing-feature-grid .card {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-price-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.landing-price-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface2);
|
||||||
|
}
|
||||||
|
.landing-price-card h3 { margin: 0 0 8px; }
|
||||||
|
.landing-price-card .price {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-contact-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
max-width: 420px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.landing-contact-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.landing-contact-form input,
|
||||||
|
.landing-contact-form textarea {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface2);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page 1 hero — lighter card, more globe visible */
|
||||||
|
.landing-page--hero .landing-page-inner {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
.landing-page--hero h2 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (4) Right page rail — hidden until handle hover/drag */
|
||||||
|
.landing-rail {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--landing-header-h);
|
||||||
|
right: 0;
|
||||||
|
bottom: var(--landing-footer-h);
|
||||||
|
width: var(--landing-rail-w);
|
||||||
|
z-index: 15;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(calc(var(--landing-rail-w) - 6px));
|
||||||
|
transition: transform 0.22s ease;
|
||||||
|
}
|
||||||
|
.landing-rail.is-visible,
|
||||||
|
.landing-rail:hover,
|
||||||
|
.landing-rail:focus-within {
|
||||||
|
transform: translateX(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-rail-handle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 14px;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
background: rgba(26, 35, 50, 0.92);
|
||||||
|
cursor: grab;
|
||||||
|
pointer-events: auto;
|
||||||
|
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.landing-rail-handle:active { cursor: grabbing; }
|
||||||
|
.landing-rail-handle::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 28px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--muted) 0,
|
||||||
|
var(--muted) 4px,
|
||||||
|
transparent 4px,
|
||||||
|
transparent 7px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-rail-dots {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(15, 20, 25, 0.82);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.landing-rail-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid var(--muted);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.landing-rail-dot:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
.landing-rail-dot.is-active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 10px rgba(61, 139, 253, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (5) Footer stays platform-footer; lift above globe */
|
||||||
|
.hub-landing .platform-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clock widget below header */
|
||||||
|
.hub-landing .hub-clock-widget {
|
||||||
|
top: calc(var(--landing-header-h) + 12px);
|
||||||
|
z-index: 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.landing-top-nav { display: none; }
|
||||||
|
.landing-page { padding: 24px 16px 24px 12px; }
|
||||||
|
.landing-rail { --landing-rail-w: 36px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-top-tools .locale-picker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: rgba(26, 35, 50, 0.5);
|
||||||
|
}
|
||||||
|
.landing-top-tools .locale-picker__select {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Globe stays fixed behind all landing chrome */
|
||||||
|
.hub-landing #hub-logo-stage {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.landing-scroll { scroll-behavior: auto; }
|
||||||
|
.landing-rail { transition: none; }
|
||||||
|
}
|
||||||
@@ -11,16 +11,19 @@ unset($__platformDir);
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Android Cast — Project hub</title>
|
<title>Android Cast — Landing</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var t = localStorage.getItem('crash_console_theme');
|
var t = localStorage.getItem('crash_console_theme');
|
||||||
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-theme', t);
|
||||||
|
var l = localStorage.getItem('crash_console_lang');
|
||||||
|
if (l === 'en' || l === 'ru') document.documentElement.setAttribute('lang', l);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
<link rel="stylesheet" href="/app/androidcast_project/crashes/assets/css/app.css">
|
||||||
<link rel="stylesheet" href="hub.css?v=20260532globe">
|
<link rel="stylesheet" href="hub.css?v=20260606landing">
|
||||||
<link rel="stylesheet" href="hub-logo-layers.css?v=20260532globe">
|
<link rel="stylesheet" href="hub-logo-layers.css?v=20260606landing">
|
||||||
|
<link rel="stylesheet" href="hub-landing.css?v=20260606landing">
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
@@ -28,10 +31,12 @@ unset($__platformDir);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="assets/hub-i18n.js" defer></script>
|
||||||
|
<script src="assets/hub-landing.js" defer></script>
|
||||||
<script src="assets/analytics.config.js"></script>
|
<script src="assets/analytics.config.js"></script>
|
||||||
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
<script src="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="hub-page">
|
<body class="hub-page hub-landing">
|
||||||
<div id="lcd-alphabet-host" hidden aria-hidden="true"></div>
|
<div id="lcd-alphabet-host" hidden aria-hidden="true"></div>
|
||||||
<div id="overtime-alphabet-host" hidden aria-hidden="true"></div>
|
<div id="overtime-alphabet-host" hidden aria-hidden="true"></div>
|
||||||
<div id="quartz-alphabet-host" hidden aria-hidden="true"></div>
|
<div id="quartz-alphabet-host" hidden aria-hidden="true"></div>
|
||||||
@@ -69,100 +74,90 @@ unset($__platformDir);
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="shell hub-shell">
|
<header class="landing-top" id="landing-top">
|
||||||
<main class="main-pane hub-main">
|
<a class="landing-top-logo" href="?page=1" data-goto-page="1">
|
||||||
<header class="hub-header">
|
<img src="assets/hub-logo-transparent.svg" alt="" width="36" height="27" decoding="async">
|
||||||
<div class="hub-brand">
|
<strong>Android Cast</strong>
|
||||||
<img class="hub-logo-mark" src="assets/hub-logo-transparent.svg" alt="" width="112" height="84" decoding="async">
|
</a>
|
||||||
<div class="hub-title-with-thunder">
|
<nav class="landing-top-nav" aria-label="Landing menu">
|
||||||
<h1 class="hub-title">Android Cast</h1>
|
<div class="landing-menu-item">
|
||||||
<img class="hub-thunder" src="assets/hub-thunder.svg?v=20260527z" alt="" decoding="async">
|
<button type="button" class="landing-menu-btn" aria-haspopup="true" aria-expanded="false" data-goto-page="2" data-i18n="nav.androidcast">AndroidCast</button>
|
||||||
</div>
|
<div class="landing-dropdown" role="menu">
|
||||||
|
<a href="#" role="menuitem" data-goto-page="3" data-i18n="nav.about">About</a>
|
||||||
|
<a href="#" role="menuitem" data-goto-page="4" data-i18n="nav.why">Why</a>
|
||||||
|
<a href="#" role="menuitem" data-goto-page="5" data-i18n="nav.tech">Technologies</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted hub-subtitle">Project hub</p>
|
</div>
|
||||||
<label class="toolbar-select hub-theme">
|
<div class="landing-menu-item">
|
||||||
<span>Theme</span>
|
<button type="button" class="landing-menu-btn" aria-haspopup="true" aria-expanded="false" data-goto-page="6" data-i18n="nav.resources">Resources</button>
|
||||||
<select id="theme-select" aria-label="UI theme">
|
<div class="landing-dropdown" role="menu">
|
||||||
<option value="dark">Dark</option>
|
<a href="#" role="menuitem" data-goto-page="7" data-i18n="nav.people">People</a>
|
||||||
<option value="light">Light</option>
|
<a href="#" role="menuitem" data-goto-page="8" data-i18n="nav.culture">Culture</a>
|
||||||
</select>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</header>
|
<a class="landing-menu-link" href="#" data-goto-page="9" data-i18n="nav.pricing">Pricing</a>
|
||||||
|
<a class="landing-menu-link" href="#" data-goto-page="10" data-i18n="nav.contact">Contact us</a>
|
||||||
|
</nav>
|
||||||
|
<div class="landing-top-tools">
|
||||||
|
<label class="toolbar-select hub-theme">
|
||||||
|
<span data-i18n="theme.label">Theme</span>
|
||||||
|
<select id="theme-select" aria-label="UI theme">
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="locale-picker" data-i18n-title="lang.label" title="Language">
|
||||||
|
<span class="locale-flag" id="locale-flag" aria-hidden="true">🇬🇧</span>
|
||||||
|
<span class="locale-code" id="locale-code" aria-hidden="true">EN</span>
|
||||||
|
<select class="lang-select locale-picker__select" id="lang-select" data-i18n-aria="lang.label" aria-label="Language">
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="ru">RU</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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="git/" class="nav-link"><span class="nav-icon nav-icon--git"></span><span class="nav-label" data-i18n="nav.git">Git</span></a></li>
|
||||||
|
<li><a href="crashes/?view=tickets" class="nav-link"><span class="nav-icon nav-icon--tickets"></span><span class="nav-label" data-i18n="nav.tickets">Tickets</span></a></li>
|
||||||
|
<li><a href="crashes/?view=reports" class="nav-link"><span class="nav-icon nav-icon--reports"></span><span class="nav-label" data-i18n="nav.crashes">Crashes</span></a></li>
|
||||||
|
<li><a href="graphs/" class="nav-link"><span class="nav-icon nav-icon--graphs"></span><span class="nav-label" data-i18n="nav.graphs">Graphs</span></a></li>
|
||||||
|
<li><a href="crashes/?view=remote_access" class="nav-link"><span class="nav-icon nav-icon--reports"></span><span class="nav-label" data-i18n="nav.remote">Remote access</span></a></li>
|
||||||
|
<li><a href="build/" class="nav-link"><span class="nav-icon nav-icon--builder"></span><span class="nav-label" data-i18n="nav.builder">Builder</span></a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="landing-scroll" id="landing-scroll" tabindex="-1">
|
||||||
|
<?php require __DIR__ . '/landing-pages.inc.php'; ?>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<nav class="hub-nav-dock" id="hub-nav-dock" aria-label="Android Cast apps">
|
|
||||||
<a class="hub-card card card--lift" href="git/">
|
<aside class="landing-rail" id="landing-rail" aria-label="Page navigation">
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
<button type="button" class="landing-rail-handle" id="landing-rail-handle" data-i18n-aria="rail.handle" aria-label="Show page navigation"></button>
|
||||||
<span class="nav-icon nav-icon--git"></span>
|
<nav class="landing-rail-dots" aria-label="Pages">
|
||||||
</span>
|
<?php for ($p = 1; $p <= 10; $p++): ?>
|
||||||
<span class="hub-card-label">AndroidCast git</span>
|
<button type="button" class="landing-rail-dot<?= $p === 1 ? ' is-active' : '' ?>" data-page="<?= $p ?>" aria-label="Page <?= $p ?>"<?= $p === 1 ? ' aria-current="true"' : '' ?>></button>
|
||||||
</a>
|
<?php endfor; ?>
|
||||||
<a class="hub-card card card--lift" href="crashes/?view=tickets">
|
</nav>
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
</aside>
|
||||||
<span class="nav-icon nav-icon--tickets"></span>
|
|
||||||
</span>
|
<?php platform_render_footer([
|
||||||
<span class="hub-card-label">AndroidCast tickets</span>
|
'use_i18n' => false,
|
||||||
</a>
|
'extra_class' => 'bottom-bar',
|
||||||
<a class="hub-card card card--lift" href="crashes/?view=reports">
|
]); ?>
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
|
||||||
<span class="nav-icon nav-icon--reports"></span>
|
|
||||||
</span>
|
|
||||||
<span class="hub-card-label">AndroidCast crashes</span>
|
|
||||||
</a>
|
|
||||||
<a class="hub-card card card--lift" href="graphs/">
|
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
|
||||||
<span class="nav-icon nav-icon--graphs"></span>
|
|
||||||
</span>
|
|
||||||
<span class="hub-card-label">AndroidCast graphs</span>
|
|
||||||
</a>
|
|
||||||
<a class="hub-card card card--lift" href="crashes/?view=remote_access">
|
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
|
||||||
<span class="nav-icon nav-icon--reports"></span>
|
|
||||||
</span>
|
|
||||||
<span class="hub-card-label">Remote access</span>
|
|
||||||
</a>
|
|
||||||
<a class="hub-card card card--lift" href="build/">
|
|
||||||
<span class="hub-card-icon" aria-hidden="true">
|
|
||||||
<span class="nav-icon nav-icon--builder"></span>
|
|
||||||
</span>
|
|
||||||
<span class="hub-card-label">AndroidCast builder</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
<?php platform_render_footer(['use_i18n' => false]); ?>
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 330 };
|
var footerLeft = document.querySelector('.platform-footer__left');
|
||||||
function globeCoverRect() {
|
if (footerLeft) {
|
||||||
var vw = window.innerWidth;
|
footerLeft.setAttribute('data-i18n', 'footer.copyright');
|
||||||
var vh = window.innerHeight;
|
footerLeft.setAttribute('data-holder', 'Anton Afanaasyeu');
|
||||||
var scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
|
footerLeft.setAttribute('data-year', '2026 - current');
|
||||||
var drawW = GLOBE_VIEW.imgW * scale;
|
|
||||||
var drawH = GLOBE_VIEW.imgH * scale;
|
|
||||||
var offX = (vw - drawW) / 2;
|
|
||||||
var offY = (vh - drawH) / 2;
|
|
||||||
var left = offX + (GLOBE_VIEW.cx - GLOBE_VIEW.r) * scale;
|
|
||||||
var top = offY + (GLOBE_VIEW.cy - GLOBE_VIEW.r) * scale;
|
|
||||||
var bottom = offY + (GLOBE_VIEW.cy + GLOBE_VIEW.r) * scale;
|
|
||||||
var right = offX + (GLOBE_VIEW.cx + GLOBE_VIEW.r) * scale;
|
|
||||||
return { left: left, top: top, bottom: bottom, right: right, width: right - left, height: bottom - top };
|
|
||||||
}
|
}
|
||||||
function layoutHubNavDock() {
|
|
||||||
var dock = document.getElementById('hub-nav-dock');
|
|
||||||
if (!dock) return;
|
|
||||||
var globe = globeCoverRect();
|
|
||||||
var dockW = Math.min(300, Math.max(210, globe.width * 0.38));
|
|
||||||
dock.style.width = dockW + 'px';
|
|
||||||
dock.style.left = globe.left + 'px';
|
|
||||||
dock.style.top = globe.top + 'px';
|
|
||||||
dock.style.height = globe.height + 'px';
|
|
||||||
dock.style.display = 'flex';
|
|
||||||
dock.style.flexDirection = 'column';
|
|
||||||
dock.style.flexWrap = 'nowrap';
|
|
||||||
dock.style.gap = '5px';
|
|
||||||
dock.style.justifyContent = 'flex-end';
|
|
||||||
}
|
|
||||||
layoutHubNavDock();
|
|
||||||
window.addEventListener('resize', layoutHubNavDock);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
@@ -185,7 +180,7 @@ unset($__platformDir);
|
|||||||
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
|
||||||
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
|
||||||
|
|
||||||
var logoBuild = '20260532globe';
|
var logoBuild = '20260606landing';
|
||||||
|
|
||||||
function hubAsset(rel) {
|
function hubAsset(rel) {
|
||||||
var link = document.querySelector('link[href*="hub.css"]');
|
var link = document.querySelector('link[href*="hub.css"]');
|
||||||
|
|||||||
130
examples/app_hub/landing-pages.inc.php
Normal file
130
examples/app_hub/landing-pages.inc.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php declare(strict_types=1); ?>
|
||||||
|
<section class="landing-page landing-page--hero" id="landing-page-1" data-page="1" aria-label="Home">
|
||||||
|
<div class="landing-page-inner">
|
||||||
|
<h2 data-i18n="page1.title">Android Cast</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page1.lead">Screen casting, crash intelligence, and CI — one ecosystem.</p>
|
||||||
|
<button type="button" class="btn btn-primary" data-goto-page="2" data-i18n="page1.cta">Explore AndroidCast</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-2" data-page="2" aria-label="AndroidCast">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page2.title">AndroidCast</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page2.lead">Mirror Android screens with low latency, native codecs, and operator-grade tooling.</p>
|
||||||
|
<div class="landing-feature-grid">
|
||||||
|
<article class="card card--lift">
|
||||||
|
<h3>Cast</h3>
|
||||||
|
<p class="muted">Wi‑Fi and USB transport with adaptive bitrate and session metrics.</p>
|
||||||
|
</article>
|
||||||
|
<article class="card card--lift">
|
||||||
|
<h3>Crashes</h3>
|
||||||
|
<p class="muted">Ingest, triage tickets, and correlate fingerprints across releases.</p>
|
||||||
|
</article>
|
||||||
|
<article class="card card--lift">
|
||||||
|
<h3>Builder</h3>
|
||||||
|
<p class="muted">Docker CI for APK baking, OTA artifacts, and pipeline history.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-3" data-page="3" aria-label="About">
|
||||||
|
<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&h=300&fit=crop" alt="" loading="lazy" decoding="async">
|
||||||
|
<img src="https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=400&h=300&fit=crop" alt="" loading="lazy" decoding="async">
|
||||||
|
<img src="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=400&h=300&fit=crop" alt="" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-4" data-page="4" aria-label="Why">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page4.title">Why AndroidCast</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page4.lead">Built for teams who need reliability, observability, and control — not another black box.</p>
|
||||||
|
<ul class="muted" style="line-height:1.7;margin:0;padding-left:1.2rem">
|
||||||
|
<li>Self-hosted consoles on your BE — no mandatory cloud lock-in</li>
|
||||||
|
<li>Graphs, remote access, and builder share one auth and theme</li>
|
||||||
|
<li>Green <code>master</code> git flow with tagged releases</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-5" data-page="5" aria-label="Technologies">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page5.title">Technologies</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page5.lead">WebRTC, Opus/Speex, WireGuard remote access, PHP consoles, and Docker CI pipelines.</p>
|
||||||
|
<div class="landing-feature-grid">
|
||||||
|
<article class="card"><strong>WebRTC / UDP</strong><p class="muted">Low-latency mirror paths</p></article>
|
||||||
|
<article class="card"><strong>Opus · Speex</strong><p class="muted">Native codec validation</p></article>
|
||||||
|
<article class="card"><strong>WireGuard</strong><p class="muted">Remote device sessions</p></article>
|
||||||
|
<article class="card"><strong>PHP · SQLite</strong><p class="muted">Crash + ticket console</p></article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-6" data-page="6" aria-label="Resources">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page6.title">Resources</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page6.lead">Documentation, deployment guides, and integration patterns for your environment.</p>
|
||||||
|
<p><a class="btn" href="crashes/">Crashes console</a> <a class="btn" href="build/">Builder</a> <a class="btn" href="git/">Git browser</a></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-7" data-page="7" aria-label="People">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page7.title">People</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page7.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&h=300&fit=crop" alt="" loading="lazy" decoding="async">
|
||||||
|
<img src="https://images.unsplash.com/photo-1600880292203-757bb62b4baf?w=400&h=300&fit=crop" alt="" loading="lazy" decoding="async">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-8" data-page="8" aria-label="Culture">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page8.title">Culture</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page8.lead">Open tooling, green master branches, and honest postmortems.</p>
|
||||||
|
<p class="muted">We ship on <code>next</code>, tag on <code>master</code>, and keep infra docs next to the code.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-9" data-page="9" aria-label="Pricing">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page9.title">Pricing</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page9.lead">Self-hosted core; enterprise options for SLA, SSO, and dedicated builders.</p>
|
||||||
|
<div class="landing-price-grid">
|
||||||
|
<div class="landing-price-card">
|
||||||
|
<h3>Community</h3>
|
||||||
|
<div class="price">$0</div>
|
||||||
|
<p class="muted">Full source, self-hosted BE, community support.</p>
|
||||||
|
</div>
|
||||||
|
<div class="landing-price-card">
|
||||||
|
<h3>Team</h3>
|
||||||
|
<div class="price">Custom</div>
|
||||||
|
<p class="muted">Priority fixes, deployment assistance, staging builders.</p>
|
||||||
|
</div>
|
||||||
|
<div class="landing-price-card">
|
||||||
|
<h3>Enterprise</h3>
|
||||||
|
<div class="price">Custom</div>
|
||||||
|
<p class="muted">SLA, SSO, dedicated infra review, on-call options.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="landing-page" id="landing-page-10" data-page="10" aria-label="Contact">
|
||||||
|
<div class="landing-page-inner card card--lift">
|
||||||
|
<h2 data-i18n="page10.title">Contact us</h2>
|
||||||
|
<p class="landing-lead" data-i18n="page10.lead">Reach the maintainer for partnerships, support, or deployment help.</p>
|
||||||
|
<form class="landing-contact-form" action="#" method="post" onsubmit="return false">
|
||||||
|
<label>Name<input type="text" name="name" autocomplete="name"></label>
|
||||||
|
<label>Email<input type="email" name="email" autocomplete="email"></label>
|
||||||
|
<label>Message<textarea name="message" rows="4"></textarea></label>
|
||||||
|
<button type="submit" class="btn btn-primary">Send (placeholder)</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user