bundle compact symbol font subsets, sw install hardening, back-exit history collapse, lang/ticker/theory fixes
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -34,6 +34,7 @@ const STRINGS = {
|
|||||||
congratulations: 'Congratulations!',
|
congratulations: 'Congratulations!',
|
||||||
pleaseSelect: 'Please select all options',
|
pleaseSelect: 'Please select all options',
|
||||||
installApp: 'Install App',
|
installApp: 'Install App',
|
||||||
|
theoryUnavailable: 'Content unavailable',
|
||||||
sec: 'sec',
|
sec: 'sec',
|
||||||
chooseAnswer: 'Choose answer',
|
chooseAnswer: 'Choose answer',
|
||||||
enterEquivalent: 'Enter equivalent',
|
enterEquivalent: 'Enter equivalent',
|
||||||
@@ -75,6 +76,7 @@ const STRINGS = {
|
|||||||
congratulations: '\u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C!',
|
congratulations: '\u041F\u043E\u0437\u0434\u0440\u0430\u0432\u043B\u044F\u0435\u043C!',
|
||||||
pleaseSelect: '\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B',
|
pleaseSelect: '\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0441\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B',
|
||||||
installApp: '\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435',
|
installApp: '\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435',
|
||||||
|
theoryUnavailable: '\u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D',
|
||||||
sec: '\u0441\u0435\u043A',
|
sec: '\u0441\u0435\u043A',
|
||||||
chooseAnswer: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442',
|
chooseAnswer: '\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442',
|
||||||
enterEquivalent: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E',
|
enterEquivalent: '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E',
|
||||||
@@ -113,6 +115,55 @@ function applyTranslations() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshDynamicText() {
|
||||||
|
const active = document.querySelector('.screen.active');
|
||||||
|
if (!active) return;
|
||||||
|
switch (active.id) {
|
||||||
|
case 'screen-setup-system':
|
||||||
|
renderSetupSystem();
|
||||||
|
break;
|
||||||
|
case 'screen-setup-range':
|
||||||
|
renderSetupRange();
|
||||||
|
break;
|
||||||
|
case 'screen-setup-options':
|
||||||
|
document.querySelectorAll('#setup-modes .option-btn').forEach((btn, i) => {
|
||||||
|
btn.textContent = t(MODES[i].labelKey);
|
||||||
|
});
|
||||||
|
[...document.getElementById('setup-timing').options].forEach((opt, i) => {
|
||||||
|
opt.textContent = TIMINGS[i].value === 0 ? t('noLimit') : TIMINGS[i].value + ' ' + t('sec');
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'screen-theory-system':
|
||||||
|
renderTheorySystem();
|
||||||
|
break;
|
||||||
|
case 'screen-theory-content':
|
||||||
|
document.getElementById('theory-title').textContent = systemName(lastTheoryKey);
|
||||||
|
break;
|
||||||
|
case 'screen-game':
|
||||||
|
if (game.system && game.range) {
|
||||||
|
document.getElementById('score-system-range').textContent = systemName(game.system) + ': ' + game.range.label;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'screen-results': {
|
||||||
|
const completed = game.currentRound >= game.rounds;
|
||||||
|
const total = completed ? game.rounds : game.currentRound;
|
||||||
|
const pct = total > 0 ? Math.round((game.correctCount / total) * 100) : 0;
|
||||||
|
document.getElementById('results-summary').innerHTML =
|
||||||
|
'<div class="big-number">' + game.correctCount + '/' + total + '</div>' +
|
||||||
|
'<div>' + t('pctCorrect', {pct: pct}) + '</div>';
|
||||||
|
const labelEl = document.getElementById('results-mistakes-label');
|
||||||
|
if (game.mistakes.length > 0 || labelEl.style.display !== 'none') {
|
||||||
|
labelEl.textContent = t('mistakes');
|
||||||
|
}
|
||||||
|
const congratsEl = document.getElementById('results-congrats');
|
||||||
|
if (congratsEl.textContent) {
|
||||||
|
congratsEl.textContent = t('congratulations');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SYSTEM_RANGES = {
|
const SYSTEM_RANGES = {
|
||||||
binary: [
|
binary: [
|
||||||
{ label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 },
|
{ label: '2\u2070 \u2013 2\u00B3', min: 1, max: 8 },
|
||||||
@@ -188,6 +239,7 @@ const SYSTEM_GROUPS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const theoryCache = {};
|
const theoryCache = {};
|
||||||
|
let lastTheoryKey = null;
|
||||||
|
|
||||||
const MODES = [
|
const MODES = [
|
||||||
{ labelKey: 'choose', value: 'choice' },
|
{ labelKey: 'choose', value: 'choice' },
|
||||||
@@ -222,11 +274,19 @@ let game = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let popstateHandled = false;
|
let popstateHandled = false;
|
||||||
|
let navDepth = 0;
|
||||||
|
|
||||||
function showScreen(id) {
|
function showScreen(id) {
|
||||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||||
document.getElementById('screen-' + id).classList.add('active');
|
document.getElementById('screen-' + id).classList.add('active');
|
||||||
if (id !== 'menu' && !popstateHandled) {
|
if (id === 'menu') {
|
||||||
|
if (navDepth > 0) {
|
||||||
|
const n = navDepth;
|
||||||
|
navDepth = 0;
|
||||||
|
history.go(-n);
|
||||||
|
}
|
||||||
|
} else if (!popstateHandled) {
|
||||||
|
navDepth++;
|
||||||
history.pushState({ screen: id }, '');
|
history.pushState({ screen: id }, '');
|
||||||
}
|
}
|
||||||
popstateHandled = false;
|
popstateHandled = false;
|
||||||
@@ -354,7 +414,7 @@ const MARK_INCORRECT = '\u2717 ';
|
|||||||
function startGame() {
|
function startGame() {
|
||||||
lastSetupState = { ...setupState };
|
lastSetupState = { ...setupState };
|
||||||
game.system = setupState.system;
|
game.system = setupState.system;
|
||||||
game.range = setupState.rangeData || RANGES[setupState.range];
|
game.range = setupState.rangeData;
|
||||||
game.mode = MODES[setupState.mode]?.value ?? setupState.mode;
|
game.mode = MODES[setupState.mode]?.value ?? setupState.mode;
|
||||||
game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing;
|
game.timing = TIMINGS[setupState.timing]?.value ?? setupState.timing;
|
||||||
game.rounds = ROUNDS[setupState.rounds];
|
game.rounds = ROUNDS[setupState.rounds];
|
||||||
@@ -754,12 +814,11 @@ function renderTheorySystem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showTheoryContent(key) {
|
async function showTheoryContent(key) {
|
||||||
|
lastTheoryKey = key;
|
||||||
const html = await loadTheory(key);
|
const html = await loadTheory(key);
|
||||||
if (!html) return;
|
|
||||||
|
|
||||||
document.getElementById('theory-title').textContent = systemName(key);
|
document.getElementById('theory-title').textContent = systemName(key);
|
||||||
document.getElementById('theory-scroll').innerHTML = html;
|
document.getElementById('theory-scroll').innerHTML =
|
||||||
|
html || '<div class="theory-unavailable">' + t('theoryUnavailable') + '</div>';
|
||||||
showScreen('theory-content');
|
showScreen('theory-content');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,9 +849,6 @@ function getPreferredTheme() {
|
|||||||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLightTheme = getPreferredTheme() === 'light';
|
|
||||||
const TICKER_COLORS = isLightTheme ? TICKER_COLORS_LIGHT : TICKER_COLORS_DARK;
|
|
||||||
|
|
||||||
const TICKER_SYSTEMS = [
|
const TICKER_SYSTEMS = [
|
||||||
{ key: 'binary', min: 1, max: 255, suffix: toSubscript(2) },
|
{ key: 'binary', min: 1, max: 255, suffix: toSubscript(2) },
|
||||||
{ key: 'base3', min: 1, max: 81, suffix: toSubscript(3) },
|
{ key: 'base3', min: 1, max: 81, suffix: toSubscript(3) },
|
||||||
@@ -806,6 +862,9 @@ const TICKER_SYSTEMS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function generateTickerText() {
|
function generateTickerText() {
|
||||||
|
const palette = document.documentElement.dataset.theme === 'light'
|
||||||
|
? TICKER_COLORS_LIGHT
|
||||||
|
: TICKER_COLORS_DARK;
|
||||||
const pairs = [];
|
const pairs = [];
|
||||||
let lastKey = null;
|
let lastKey = null;
|
||||||
let lastColor = null;
|
let lastColor = null;
|
||||||
@@ -818,7 +877,7 @@ function generateTickerText() {
|
|||||||
|
|
||||||
let color;
|
let color;
|
||||||
do {
|
do {
|
||||||
color = TICKER_COLORS[Math.floor(Math.random() * TICKER_COLORS.length)];
|
color = palette[Math.floor(Math.random() * palette.length)];
|
||||||
} while (color === lastColor);
|
} while (color === lastColor);
|
||||||
lastColor = color;
|
lastColor = color;
|
||||||
const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1));
|
const num = sys.min + Math.floor(Math.random() * (sys.max - sys.min + 1));
|
||||||
@@ -834,8 +893,10 @@ function generateTickerText() {
|
|||||||
return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0');
|
return pairs.join('\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshTicker() {
|
||||||
const tickerText = generateTickerText();
|
const tickerText = generateTickerText();
|
||||||
document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText;
|
document.getElementById('ticker').innerHTML = tickerText + '\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0' + tickerText;
|
||||||
|
}
|
||||||
|
|
||||||
const langSelect = document.getElementById('lang-select');
|
const langSelect = document.getElementById('lang-select');
|
||||||
langSelect.value = LANG;
|
langSelect.value = LANG;
|
||||||
@@ -843,6 +904,7 @@ langSelect.addEventListener('change', () => {
|
|||||||
LANG = langSelect.value;
|
LANG = langSelect.value;
|
||||||
localStorage.setItem('numnum-lang', LANG);
|
localStorage.setItem('numnum-lang', LANG);
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
|
refreshDynamicText();
|
||||||
});
|
});
|
||||||
|
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
@@ -850,6 +912,7 @@ applyTranslations();
|
|||||||
function applyTheme(theme) {
|
function applyTheme(theme) {
|
||||||
document.documentElement.dataset.theme = theme;
|
document.documentElement.dataset.theme = theme;
|
||||||
document.getElementById('theme-toggle').textContent = theme === 'dark' ? '\u2600' : '\u263E';
|
document.getElementById('theme-toggle').textContent = theme === 'dark' ? '\u2600' : '\u263E';
|
||||||
|
refreshTicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTheme(getPreferredTheme());
|
applyTheme(getPreferredTheme());
|
||||||
@@ -918,7 +981,8 @@ document.getElementById('btn-options-back').addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-play').addEventListener('click', () => {
|
document.getElementById('btn-play').addEventListener('click', () => {
|
||||||
if (setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) {
|
if (!setupState.system || !setupState.rangeData ||
|
||||||
|
setupState.mode === undefined || setupState.timing === undefined || setupState.rounds === undefined) {
|
||||||
showSetupMessage(t('pleaseSelect'));
|
showSetupMessage(t('pleaseSelect'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -997,6 +1061,7 @@ const BACK_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('popstate', () => {
|
window.addEventListener('popstate', () => {
|
||||||
|
navDepth = Math.max(0, navDepth - 1);
|
||||||
const active = document.querySelector('.screen.active');
|
const active = document.querySelector('.screen.active');
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@
|
|||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
||||||
<link rel="apple-touch-icon" href="icon-192.png">
|
<link rel="apple-touch-icon" href="icon-192.png">
|
||||||
<link rel="stylesheet" href="styles.css?v=26">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -193,10 +193,10 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="app.js?v=26"></script>
|
<script type="module" src="app.js"></script>
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js?v=26').catch(() => {});
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+28
-4
@@ -73,13 +73,31 @@ html[data-theme="light"] {
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans Symbols 2';
|
||||||
|
src: url('NotoSansSymbols2-subset.woff2') format('woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range: U+232B, U+23F1, U+2600, U+2713, U+2717, U+2801, U+2803, U+2809-280B, U+2811, U+2813, U+2819-281B, U+283C, U+2B95;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans Symbols 2';
|
||||||
|
src: url('NotoSansMath-subset.woff2') format('woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
unicode-range: U+2295-2296, U+263E;
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans Symbols 2', sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
@@ -189,7 +207,7 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
font-family: monospace;
|
font-family: 'Noto Sans Symbols 2', monospace;
|
||||||
animation: ticker-scroll 40s linear infinite;
|
animation: ticker-scroll 40s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +473,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.number-display.mono {
|
.number-display.mono {
|
||||||
font-family: monospace;
|
font-family: 'Noto Sans Symbols 2', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticker .slavonic-font {
|
.ticker .slavonic-font {
|
||||||
@@ -795,13 +813,19 @@ body {
|
|||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-family: monospace;
|
font-family: 'Noto Sans Symbols 2', monospace;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theory-unavailable {
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.65;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.theory-scroll .slavonic-font {
|
.theory-scroll .slavonic-font {
|
||||||
font-family: 'Ponomar', serif;
|
font-family: 'Ponomar', serif;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'numnum-v27';
|
const CACHE_NAME = 'numnum-v28';
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
@@ -9,6 +9,9 @@ const ASSETS = [
|
|||||||
'/icon.svg',
|
'/icon.svg',
|
||||||
'/icon-192.png',
|
'/icon-192.png',
|
||||||
'/icon-512.png',
|
'/icon-512.png',
|
||||||
|
'/Ponomar-Regular.woff',
|
||||||
|
'/NotoSansSymbols2-subset.woff2',
|
||||||
|
'/NotoSansMath-subset.woff2',
|
||||||
'/theory/base3.en.html',
|
'/theory/base3.en.html',
|
||||||
'/theory/base3.ru.html',
|
'/theory/base3.ru.html',
|
||||||
'/theory/binary.en.html',
|
'/theory/binary.en.html',
|
||||||
@@ -33,7 +36,15 @@ const ASSETS = [
|
|||||||
|
|
||||||
self.addEventListener('install', (e) => {
|
self.addEventListener('install', (e) => {
|
||||||
e.waitUntil(
|
e.waitUntil(
|
||||||
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
|
caches.open(CACHE_NAME).then(cache =>
|
||||||
|
Promise.allSettled(ASSETS.map(url => cache.add(url)))
|
||||||
|
).then(results => {
|
||||||
|
results.forEach((r, i) => {
|
||||||
|
if (r.status === 'rejected') {
|
||||||
|
console.warn('SW precache failed:', ASSETS[i], r.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user