diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index 9e69f88..3d7c9f9 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -1069,6 +1069,52 @@ function bumpProgress(key, delta){ STATE.progress[key] = v; saveProgress(); refreshProgressUI(); + if(v >= 50) markParaRead(key); +} + +/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */ +const _TB_SLUG = 'algebra-8'; +const _markedRead = new Set(); +let _pendingProgressBody = null, _progressTimer = null; +function _flushProgress(){ + const body = _pendingProgressBody; + _pendingProgressBody = null; + if(!body) return; + const tok = (window.LS && LS.getToken) ? LS.getToken() : ''; + if(!tok) return; + fetch('/api/textbooks/' + _TB_SLUG + '/progress', { + method:'POST', + headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok }, + body: JSON.stringify(body), + keepalive: true, + }).catch(()=>{}); +} +function _queueProgress(patch){ + _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch); + if(_progressTimer) clearTimeout(_progressTimer); + _progressTimer = setTimeout(_flushProgress, 600); +} +function markLastPara(id){ _queueProgress({ last_para: id }); } +function markParaRead(id){ + if(_markedRead.has(id)) return; + _markedRead.add(id); + _queueProgress({ mark_read: id }); +} +window.addEventListener('beforeunload', _flushProgress); +function loadServerReadState(){ + const tok = (window.LS && LS.getToken) ? LS.getToken() : ''; + if(!tok) return; + fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } }) + .then(r => r.ok ? r.json() : null) + .then(d => { + if(!d || !d.progress) return; + (d.progress.read || []).forEach(k => { + _markedRead.add(k); + if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; + }); + saveProgress(); refreshProgressUI(); + }) + .catch(()=>{}); } function refreshProgressUI(){ const total = Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7; @@ -1204,6 +1250,7 @@ function _goToFinish(id){ if(window.renderMathInElement){ setTimeout(()=>renderMathInElement(el, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}),0); } + markLastPara(id); } /* ════════════════════════════════════════════════════════ @@ -1460,6 +1507,7 @@ function init(){ buildParaSelector(); refreshProgressUI(); initMobileSidebar(); + loadServerReadState(); goTo('p1'); // строит только §1, остальные — лениво при переходе setTimeout(()=>achievement('start','Начало пути по корням!'), 800); // Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс diff --git a/frontend/textbooks/algebra_8_ch2.html b/frontend/textbooks/algebra_8_ch2.html index 84f5320..a6e3417 100644 --- a/frontend/textbooks/algebra_8_ch2.html +++ b/frontend/textbooks/algebra_8_ch2.html @@ -541,6 +541,52 @@ function bumpProgress(key, delta){ STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta)); saveProgress(); refreshProgressUI(); + if(STATE.progress[key] >= 50) markParaRead(key); +} + +/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */ +const _TB_SLUG = 'algebra-8-ch2'; +const _markedRead = new Set(); +let _pendingProgressBody = null, _progressTimer = null; +function _flushProgress(){ + const body = _pendingProgressBody; + _pendingProgressBody = null; + if(!body) return; + const tok = (window.LS && LS.getToken) ? LS.getToken() : ''; + if(!tok) return; + fetch('/api/textbooks/' + _TB_SLUG + '/progress', { + method:'POST', + headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok }, + body: JSON.stringify(body), + keepalive: true, + }).catch(()=>{}); +} +function _queueProgress(patch){ + _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch); + if(_progressTimer) clearTimeout(_progressTimer); + _progressTimer = setTimeout(_flushProgress, 600); +} +function markLastPara(id){ _queueProgress({ last_para: id }); } +function markParaRead(id){ + if(_markedRead.has(id)) return; + _markedRead.add(id); + _queueProgress({ mark_read: id }); +} +window.addEventListener('beforeunload', _flushProgress); +function loadServerReadState(){ + const tok = (window.LS && LS.getToken) ? LS.getToken() : ''; + if(!tok) return; + fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } }) + .then(r => r.ok ? r.json() : null) + .then(d => { + if(!d || !d.progress) return; + (d.progress.read || []).forEach(k => { + _markedRead.add(k); + if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; + }); + saveProgress(); refreshProgressUI(); + }) + .catch(()=>{}); } function addXp(n, src){ if(!n) return; @@ -646,6 +692,7 @@ function goTo(id){ if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0); // glossary wrap — пройти по тексту секции и обернуть термины setTimeout(()=>{ try { wrapGlossary(el); } catch(e){} }, 60); + markLastPara(id); } /* SIDEBAR */ @@ -1206,6 +1253,7 @@ function init(){ initSearch(); buildParaSelector(); refreshProgressUI(); + loadServerReadState(); goTo('p7'); setTimeout(()=>achievement('start','Начало главы 2!'), 600); // Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс