mirror of
git://f0xx.org/ac/ac-platform-web
synced 2026-07-29 04:18:31 +03:00
initial
This commit is contained in:
157
assets/js/analytics.js
Normal file
157
assets/js/analytics.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Google Analytics 4 for Android Cast web platform (hub + crash console).
|
||||
* Set window.ANDROIDCAST_ANALYTICS before this script loads:
|
||||
* { measurementId: 'G-XXXXXXXX', platform: 'hub'|'crashes', debug: false }
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var cfg = global.ANDROIDCAST_ANALYTICS || {};
|
||||
var measurementId = (cfg.measurementId || '').trim();
|
||||
var platform = cfg.platform || 'web';
|
||||
var debug = !!cfg.debug;
|
||||
|
||||
function log() {
|
||||
if (debug && global.console) {
|
||||
global.console.log.apply(global.console, ['[analytics]'].concat([].slice.call(arguments)));
|
||||
}
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
if (global.AndroidCastCookieConsent && !global.AndroidCastCookieConsent.allowsAnalytics()) {
|
||||
var choice = global.AndroidCastCookieConsent.getChoice && global.AndroidCastCookieConsent.getChoice();
|
||||
if (choice && choice !== 'all') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return measurementId.length > 0 && /^G-[A-Z0-9]+$/i.test(measurementId);
|
||||
}
|
||||
|
||||
function ensureGtag(cb) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
global.dataLayer = global.dataLayer || [];
|
||||
if (!global.gtag) {
|
||||
global.gtag = function () {
|
||||
global.dataLayer.push(arguments);
|
||||
};
|
||||
}
|
||||
if (global.__acAnalyticsLoaded) {
|
||||
if (cb) cb();
|
||||
return;
|
||||
}
|
||||
var s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(measurementId);
|
||||
s.onload = function () {
|
||||
global.__acAnalyticsLoaded = true;
|
||||
global.gtag('js', new Date());
|
||||
global.gtag('config', measurementId, {
|
||||
send_page_view: false,
|
||||
anonymize_ip: true
|
||||
});
|
||||
log('loaded', measurementId);
|
||||
if (cb) cb();
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function pagePath() {
|
||||
var base = (cfg.basePath || '').replace(/\/$/, '');
|
||||
var path = global.location.pathname || '/';
|
||||
if (base && path.indexOf(base) === 0) {
|
||||
path = path.slice(base.length) || '/';
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function pageView(extra) {
|
||||
if (!enabled()) return;
|
||||
ensureGtag(function () {
|
||||
var params = {
|
||||
page_title: document.title || '',
|
||||
page_location: global.location.href,
|
||||
page_path: pagePath(),
|
||||
platform: platform
|
||||
};
|
||||
if (extra) {
|
||||
for (var k in extra) {
|
||||
if (Object.prototype.hasOwnProperty.call(extra, k)) params[k] = extra[k];
|
||||
}
|
||||
}
|
||||
global.gtag('event', 'page_view', params);
|
||||
log('page_view', params);
|
||||
});
|
||||
}
|
||||
|
||||
function trackEvent(name, params) {
|
||||
if (!enabled() || !name) return;
|
||||
ensureGtag(function () {
|
||||
var payload = params || {};
|
||||
payload.platform = platform;
|
||||
global.gtag('event', name, payload);
|
||||
log('event', name, payload);
|
||||
});
|
||||
}
|
||||
|
||||
function bindConsoleView() {
|
||||
var body = document.body;
|
||||
if (!body) return;
|
||||
var view = body.getAttribute('data-view');
|
||||
if (view) {
|
||||
trackEvent('console_view', { view_name: view });
|
||||
}
|
||||
}
|
||||
|
||||
function bindHubInteractions() {
|
||||
document.addEventListener('click', function (ev) {
|
||||
var t = ev.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
var card = t.closest('.hub-card, .hub-card-link');
|
||||
if (card) {
|
||||
var label = card.getAttribute('data-analytics-label')
|
||||
|| (card.textContent || '').trim().slice(0, 64);
|
||||
trackEvent('hub_nav_click', { link_text: label });
|
||||
return;
|
||||
}
|
||||
if (t.closest('[data-open-docs], #hub-compass-layer, .hub-logo-layer--compass')) {
|
||||
trackEvent('hub_open_docs', { source: 'compass' });
|
||||
}
|
||||
if (t.closest('[data-close-docs]')) {
|
||||
trackEvent('hub_close_docs', {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!enabled()) {
|
||||
log('disabled (no measurement id or cookie consent)');
|
||||
return;
|
||||
}
|
||||
pageView();
|
||||
if (platform === 'crashes' || String(platform).indexOf('live_') === 0) {
|
||||
bindConsoleView();
|
||||
} else if (platform === 'hub') {
|
||||
bindHubInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
global.addEventListener('ac-cookie-consent', function () {
|
||||
if (enabled()) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
global.AndroidCastAnalytics = {
|
||||
enabled: enabled,
|
||||
pageView: pageView,
|
||||
trackEvent: trackEvent
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
2067
assets/js/app.js
Normal file
2067
assets/js/app.js
Normal file
File diff suppressed because it is too large
Load Diff
79
assets/js/cookie_consent.js
Normal file
79
assets/js/cookie_consent.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Cookie consent banner — stores choice in localStorage (ac_cookie_consent).
|
||||
* Values: all | necessary | reject
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'ac_cookie_consent';
|
||||
var banner = null;
|
||||
|
||||
function getChoice() {
|
||||
try {
|
||||
var v = localStorage.getItem(STORAGE_KEY);
|
||||
if (v === 'all' || v === 'necessary' || v === 'reject') {
|
||||
return v;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
function setChoice(value) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, value);
|
||||
} catch (e) { /* ignore */ }
|
||||
global.dispatchEvent(new CustomEvent('ac-cookie-consent', { detail: { choice: value } }));
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function allowsAnalytics() {
|
||||
return getChoice() === 'all';
|
||||
}
|
||||
|
||||
function allowsPreferenceCookies() {
|
||||
var c = getChoice();
|
||||
return c === 'all' || c === 'necessary';
|
||||
}
|
||||
|
||||
function hideBanner() {
|
||||
if (banner) {
|
||||
banner.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBanner() {
|
||||
if (banner || getChoice()) {
|
||||
return;
|
||||
}
|
||||
banner = document.getElementById('cookie-consent-banner');
|
||||
if (!banner) {
|
||||
return;
|
||||
}
|
||||
banner.hidden = false;
|
||||
var allBtn = document.getElementById('cookie-consent-all');
|
||||
var necBtn = document.getElementById('cookie-consent-necessary');
|
||||
var rejBtn = document.getElementById('cookie-consent-reject');
|
||||
if (allBtn) {
|
||||
allBtn.addEventListener('click', function () { setChoice('all'); });
|
||||
}
|
||||
if (necBtn) {
|
||||
necBtn.addEventListener('click', function () { setChoice('necessary'); });
|
||||
}
|
||||
if (rejBtn) {
|
||||
rejBtn.addEventListener('click', function () { setChoice('reject'); });
|
||||
}
|
||||
}
|
||||
|
||||
global.AndroidCastCookieConsent = {
|
||||
getChoice: getChoice,
|
||||
allowsAnalytics: allowsAnalytics,
|
||||
allowsPreferenceCookies: allowsPreferenceCookies,
|
||||
setChoice: setChoice
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', ensureBanner);
|
||||
} else {
|
||||
ensureBanner();
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
207
assets/js/i18n.js
Normal file
207
assets/js/i18n.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Crash console i18n — JSON catalogs (en default), same key style as Android strings.xml.
|
||||
*/
|
||||
(function (global) {
|
||||
const STORAGE_KEY = 'crash_console_lang';
|
||||
const SUPPORTED = ['en', 'ru'];
|
||||
const DEFAULT_LANG = 'en';
|
||||
|
||||
/** @type {Record<string, { flag: string, code: string }>} */
|
||||
const LANG_DISPLAY = {
|
||||
en: { flag: '\uD83C\uDDEC\uD83C\uDDE7', code: 'EN' },
|
||||
ru: { flag: '\uD83C\uDDF7\uD83C\uDDFA', code: 'RU' },
|
||||
};
|
||||
|
||||
let lang = DEFAULT_LANG;
|
||||
let catalog = {};
|
||||
let fallback = {};
|
||||
let readyResolve;
|
||||
const ready = new Promise((resolve) => {
|
||||
readyResolve = resolve;
|
||||
});
|
||||
|
||||
function basePath() {
|
||||
const body = document.body;
|
||||
return body ? body.getAttribute('data-base-path') || '' : '';
|
||||
}
|
||||
|
||||
/** Close stray modal layers so fixed backdrops never block login/console UI. */
|
||||
function dismissPlatformOverlays() {
|
||||
document.querySelectorAll('#graph-detail-overlay, #tag-modal, #hub-docs-modal').forEach((el) => {
|
||||
el.hidden = true;
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
const dialog = document.getElementById('ticket-create-dialog');
|
||||
if (dialog && dialog.open && typeof dialog.close === 'function') {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
function storedLang() {
|
||||
try {
|
||||
const v = localStorage.getItem(STORAGE_KEY);
|
||||
return SUPPORTED.includes(v) ? v : DEFAULT_LANG;
|
||||
} catch {
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
}
|
||||
|
||||
function persistLang(code) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, code);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function fetchCatalog(code) {
|
||||
const url = basePath() + '/assets/i18n/' + encodeURIComponent(code) + '.json';
|
||||
return fetch(url, { credentials: 'same-origin' }).then((res) => {
|
||||
if (!res.ok) throw new Error('i18n load failed: ' + code);
|
||||
return res.json();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Record<string, string|number>|undefined} params
|
||||
*/
|
||||
function t(key, params) {
|
||||
let s = catalog[key] ?? fallback[key] ?? key;
|
||||
if (params) {
|
||||
Object.keys(params).forEach((k) => {
|
||||
s = s.split('{' + k + '}').join(String(params[k]));
|
||||
});
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function apply(root) {
|
||||
const scope = root || document;
|
||||
scope.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (!key) return;
|
||||
if (el.hasAttribute('data-i18n-html')) {
|
||||
el.innerHTML = t(key);
|
||||
} else {
|
||||
el.textContent = t(key);
|
||||
}
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
if (key) el.setAttribute('placeholder', t(key));
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-title]').forEach((el) => {
|
||||
const key = el.getAttribute('data-i18n-title');
|
||||
if (key) el.setAttribute('title', t(key));
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n-aria]').forEach((el) => {
|
||||
const key = el.getAttribute('data-i18n-aria');
|
||||
if (key) el.setAttribute('aria-label', t(key));
|
||||
});
|
||||
scope.querySelectorAll('select[data-i18n-options]').forEach((sel) => {
|
||||
const prefix = sel.getAttribute('data-i18n-options');
|
||||
if (!prefix) return;
|
||||
sel.querySelectorAll('option[data-i18n]').forEach((opt) => {
|
||||
const k = opt.getAttribute('data-i18n');
|
||||
if (k) opt.textContent = t(k);
|
||||
});
|
||||
});
|
||||
scope.querySelectorAll('.platform-footer__left[data-i18n], footer[data-i18n="footer.copyright"]').forEach((footer) => {
|
||||
const year = footer.getAttribute('data-year') || String(new Date().getFullYear());
|
||||
footer.textContent = t(footer.getAttribute('data-i18n') || 'footer.copyright', { year });
|
||||
});
|
||||
scope.querySelectorAll('[data-i18n="detail.report_meta"]').forEach((el) => {
|
||||
el.textContent = t('detail.report_meta', {
|
||||
id: el.getAttribute('data-i18n-id') || '',
|
||||
type: el.getAttribute('data-i18n-type') || '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function langOptionLabel(code) {
|
||||
const meta = LANG_DISPLAY[code] || LANG_DISPLAY[DEFAULT_LANG];
|
||||
return meta.flag + ' ' + meta.code;
|
||||
}
|
||||
|
||||
function syncLangSelect() {
|
||||
document.querySelectorAll('.lang-select').forEach((sel) => {
|
||||
SUPPORTED.forEach((code) => {
|
||||
const opt = sel.querySelector('option[value="' + code + '"]');
|
||||
if (opt) opt.textContent = langOptionLabel(code);
|
||||
});
|
||||
if (sel.value !== lang) sel.value = lang;
|
||||
});
|
||||
syncLocaleDisplay(lang);
|
||||
}
|
||||
|
||||
function syncLocaleDisplay(code) {
|
||||
const meta = LANG_DISPLAY[code] || LANG_DISPLAY[DEFAULT_LANG];
|
||||
document.querySelectorAll('.locale-flag').forEach((el) => {
|
||||
el.textContent = meta.flag;
|
||||
});
|
||||
document.querySelectorAll('.locale-code').forEach((el) => {
|
||||
el.textContent = meta.code;
|
||||
});
|
||||
}
|
||||
|
||||
async function setLang(code, options) {
|
||||
const opts = options || {};
|
||||
if (!SUPPORTED.includes(code)) code = DEFAULT_LANG;
|
||||
fallback = await fetchCatalog(DEFAULT_LANG);
|
||||
catalog = code === DEFAULT_LANG ? { ...fallback } : await fetchCatalog(code);
|
||||
lang = code;
|
||||
document.documentElement.setAttribute('lang', code);
|
||||
persistLang(code);
|
||||
syncLangSelect();
|
||||
if (!opts.silent) {
|
||||
apply(document);
|
||||
document.dispatchEvent(new CustomEvent('crash-i18n-change', { detail: { lang: code } }));
|
||||
}
|
||||
}
|
||||
|
||||
function getLang() {
|
||||
return lang;
|
||||
}
|
||||
|
||||
function initLangSelect() {
|
||||
document.querySelectorAll('.lang-select').forEach((sel) => {
|
||||
sel.value = lang;
|
||||
if (sel.dataset.i18nBound === '1') return;
|
||||
sel.dataset.i18nBound = '1';
|
||||
sel.addEventListener('change', () => {
|
||||
setLang(sel.value).catch(() => setLang(DEFAULT_LANG));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
dismissPlatformOverlays();
|
||||
lang = storedLang();
|
||||
document.documentElement.setAttribute('lang', lang);
|
||||
await setLang(lang, { silent: true });
|
||||
apply(document);
|
||||
initLangSelect();
|
||||
syncLocaleDisplay(lang);
|
||||
readyResolve();
|
||||
}
|
||||
|
||||
global.CrashI18n = {
|
||||
t,
|
||||
apply,
|
||||
setLang,
|
||||
getLang,
|
||||
init,
|
||||
ready,
|
||||
SUPPORTED,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init().catch(() => {
|
||||
lang = DEFAULT_LANG;
|
||||
readyResolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
init().catch(() => readyResolve());
|
||||
}
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
83
assets/js/nav_shell.js
Normal file
83
assets/js/nav_shell.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Collapsible left nav: click toggle + horizontal drag on handle (open on drag right).
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var DRAG_THRESHOLD_PX = 20;
|
||||
|
||||
function setOpen(nav, handle, open) {
|
||||
nav.classList.toggle('open', open);
|
||||
if (handle) {
|
||||
handle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function bindNavShell(paneId, handleId) {
|
||||
var nav = document.getElementById(paneId);
|
||||
var handle = document.getElementById(handleId);
|
||||
if (!nav || !handle) return;
|
||||
|
||||
setOpen(nav, handle, nav.classList.contains('open'));
|
||||
|
||||
var drag = null;
|
||||
var suppressClick = false;
|
||||
|
||||
handle.addEventListener('click', function (ev) {
|
||||
if (suppressClick) {
|
||||
suppressClick = false;
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
setOpen(nav, handle, !nav.classList.contains('open'));
|
||||
});
|
||||
|
||||
handle.addEventListener('pointerdown', function (ev) {
|
||||
if (ev.button !== 0) return;
|
||||
drag = { x: ev.clientX, moved: false };
|
||||
handle.setPointerCapture(ev.pointerId);
|
||||
handle.classList.add('is-dragging');
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
handle.addEventListener('pointermove', function (ev) {
|
||||
if (!drag || !handle.hasPointerCapture(ev.pointerId)) return;
|
||||
var dx = ev.clientX - drag.x;
|
||||
if (Math.abs(dx) < 4) return;
|
||||
drag.moved = true;
|
||||
if (dx > DRAG_THRESHOLD_PX) {
|
||||
setOpen(nav, handle, true);
|
||||
drag.x = ev.clientX;
|
||||
} else if (dx < -DRAG_THRESHOLD_PX) {
|
||||
setOpen(nav, handle, false);
|
||||
drag.x = ev.clientX;
|
||||
}
|
||||
});
|
||||
|
||||
function endDrag(ev) {
|
||||
if (!drag) return;
|
||||
if (drag.moved) suppressClick = true;
|
||||
drag = null;
|
||||
handle.classList.remove('is-dragging');
|
||||
if (handle.hasPointerCapture(ev.pointerId)) {
|
||||
handle.releasePointerCapture(ev.pointerId);
|
||||
}
|
||||
}
|
||||
|
||||
handle.addEventListener('pointerup', endDrag);
|
||||
handle.addEventListener('pointercancel', endDrag);
|
||||
}
|
||||
|
||||
window.initNavShell = bindNavShell;
|
||||
|
||||
function autoInit() {
|
||||
bindNavShell('nav-pane', 'nav-handle');
|
||||
bindNavShell('landing-nav-pane', 'landing-nav-handle');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', autoInit);
|
||||
} else {
|
||||
autoInit();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user