1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:39:09 +03:00

landing sync, multipage

This commit is contained in:
Anton Afanasyeu
2026-06-06 12:09:34 +02:00
parent cd2e69ad6b
commit 5c2458dfcd
8 changed files with 1022 additions and 93 deletions

View File

@@ -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.
**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
cp .../android_cast/examples/app_hub/index.php \
.../htdocs/apps/app/androidcast_project/index.php

View 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);

View 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 };
})();

View 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}"
}

View 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}"
}

View 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; }
}

View File

@@ -11,16 +11,19 @@ unset($__platformDir);
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Android Cast — Project hub</title>
<title>Android Cast — Landing</title>
<script>
(function () {
var t = localStorage.getItem('crash_console_theme');
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>
<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-logo-layers.css?v=20260532globe">
<link rel="stylesheet" href="hub.css?v=20260606landing">
<link rel="stylesheet" href="hub-logo-layers.css?v=20260606landing">
<link rel="stylesheet" href="hub-landing.css?v=20260606landing">
<script type="importmap">
{
"imports": {
@@ -28,10 +31,12 @@ unset($__platformDir);
}
}
</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="/app/androidcast_project/crashes/assets/js/analytics.js" defer></script>
</head>
<body class="hub-page">
<body class="hub-page hub-landing">
<div id="lcd-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>
@@ -69,100 +74,90 @@ unset($__platformDir);
</div>
</section>
</div>
<div class="shell hub-shell">
<main class="main-pane hub-main">
<header class="hub-header">
<div class="hub-brand">
<img class="hub-logo-mark" src="assets/hub-logo-transparent.svg" alt="" width="112" height="84" decoding="async">
<div class="hub-title-with-thunder">
<h1 class="hub-title">Android Cast</h1>
<img class="hub-thunder" src="assets/hub-thunder.svg?v=20260527z" alt="" decoding="async">
</div>
<header class="landing-top" id="landing-top">
<a class="landing-top-logo" href="?page=1" data-goto-page="1">
<img src="assets/hub-logo-transparent.svg" alt="" width="36" height="27" decoding="async">
<strong>Android Cast</strong>
</a>
<nav class="landing-top-nav" aria-label="Landing menu">
<div class="landing-menu-item">
<button type="button" class="landing-menu-btn" aria-haspopup="true" aria-expanded="false" data-goto-page="2" data-i18n="nav.androidcast">AndroidCast</button>
<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>
<p class="muted hub-subtitle">Project hub</p>
<label class="toolbar-select hub-theme">
<span>Theme</span>
<select id="theme-select" aria-label="UI theme">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</label>
</header>
</div>
<div class="landing-menu-item">
<button type="button" class="landing-menu-btn" aria-haspopup="true" aria-expanded="false" data-goto-page="6" data-i18n="nav.resources">Resources</button>
<div class="landing-dropdown" role="menu">
<a href="#" role="menuitem" data-goto-page="7" data-i18n="nav.people">People</a>
<a href="#" role="menuitem" data-goto-page="8" data-i18n="nav.culture">Culture</a>
</div>
</div>
<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>
</div>
<nav class="hub-nav-dock" id="hub-nav-dock" aria-label="Android Cast apps">
<a class="hub-card card card--lift" href="git/">
<span class="hub-card-icon" aria-hidden="true">
<span class="nav-icon nav-icon--git"></span>
</span>
<span class="hub-card-label">AndroidCast git</span>
</a>
<a class="hub-card card card--lift" href="crashes/?view=tickets">
<span class="hub-card-icon" aria-hidden="true">
<span class="nav-icon nav-icon--tickets"></span>
</span>
<span class="hub-card-label">AndroidCast tickets</span>
</a>
<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]); ?>
<aside class="landing-rail" id="landing-rail" aria-label="Page navigation">
<button type="button" class="landing-rail-handle" id="landing-rail-handle" data-i18n-aria="rail.handle" aria-label="Show page navigation"></button>
<nav class="landing-rail-dots" aria-label="Pages">
<?php for ($p = 1; $p <= 10; $p++): ?>
<button type="button" class="landing-rail-dot<?= $p === 1 ? ' is-active' : '' ?>" data-page="<?= $p ?>" aria-label="Page <?= $p ?>"<?= $p === 1 ? ' aria-current="true"' : '' ?>></button>
<?php endfor; ?>
</nav>
</aside>
<?php platform_render_footer([
'use_i18n' => false,
'extra_class' => 'bottom-bar',
]); ?>
<script>
(function () {
var GLOBE_VIEW = { imgW: 1920, imgH: 1080, cx: 720, cy: 560, r: 330 };
function globeCoverRect() {
var vw = window.innerWidth;
var vh = window.innerHeight;
var scale = Math.max(vw / GLOBE_VIEW.imgW, vh / GLOBE_VIEW.imgH);
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 };
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');
}
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>
@@ -185,7 +180,7 @@ unset($__platformDir);
var supportsLayout = !!(window.CSS && CSS.supports && CSS.supports('object-fit', 'cover'));
var logoEnabled = !!(stage && supportsSvg && supportsLayout);
var logoBuild = '20260532globe';
var logoBuild = '20260606landing';
function hubAsset(rel) {
var link = document.querySelector('link[href*="hub.css"]');

View 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">WiFi 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&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>
</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&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>
</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>