mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:37:52 +03:00
landing sync, multipage
This commit is contained in:
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}"
|
||||
}
|
||||
Reference in New Issue
Block a user