Revemo always kana view

This commit is contained in:
2026-06-21 13:29:57 +03:00
parent ace3e89033
commit 60697aecb1
5 changed files with 53 additions and 27 deletions
+1
View File
@@ -22,3 +22,4 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
jwl.tar.gz
-10
View File
@@ -95,16 +95,6 @@ function renderWordsList(words: Word[]) {
let reading = word.key; let reading = word.key;
let translation = word.value; let translation = word.value;
if (word.more) {
try {
const parsed = JSON.parse(word.more);
reading = parsed.reading || word.key;
translation = parsed.translation || word.value;
} catch {
// ignore
}
}
li.innerHTML = ` li.innerHTML = `
<span class="word-key">${reading}</span> <span class="word-key">${reading}</span>
<span class="word-value">${escapeHtml(translation)}</span> <span class="word-value">${escapeHtml(translation)}</span>
+8 -1
View File
@@ -249,7 +249,6 @@ body {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
box-shadow: var(--shadow); box-shadow: var(--shadow);
transition: border-color 0.2s; transition: border-color 0.2s;
@@ -475,6 +474,14 @@ body {
margin: 0.75rem 0; margin: 0.75rem 0;
} }
.training-score {
font-size: 0.85rem;
color: var(--accent);
margin-top: 0.75rem;
font-weight: 500;
opacity: 0.8;
}
.training-actions { .training-actions {
padding: 0.75rem 0; padding: 0.75rem 0;
} }
+2
View File
@@ -45,6 +45,7 @@ export async function uploadDatabase(
console.log('[Sync] База данных успешно загружена.'); console.log('[Sync] База данных успешно загружена.');
} catch (err) { } catch (err) {
console.error('[Sync] Ошибка загрузки:', err); console.error('[Sync] Ошибка загрузки:', err);
console.log(err);
alert(`Ошибка загрузки: ${err instanceof Error ? err.message : String(err)}`); alert(`Ошибка загрузки: ${err instanceof Error ? err.message : String(err)}`);
throw err; throw err;
} }
@@ -67,6 +68,7 @@ export async function downloadDatabase(
try { try {
// 1. GET-запрос // 1. GET-запрос
const url = `${apiBaseUrl.replace(/\/+$/, '')}/download/${encodeURIComponent(id)}`; const url = `${apiBaseUrl.replace(/\/+$/, '')}/download/${encodeURIComponent(id)}`;
console.log(url);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
+42 -16
View File
@@ -1,4 +1,4 @@
import { CardSet } from './algorithm.ts'; import { CardSet, calculatedScore } from './algorithm.ts';
import type { CardSetSettings, CardStatistics, WordData, WordOpenMode } from './algorithm.ts'; import type { CardSetSettings, CardStatistics, WordData, WordOpenMode } from './algorithm.ts';
import { updateCardStat } from './db-manager.ts'; import { updateCardStat } from './db-manager.ts';
@@ -11,12 +11,6 @@ export interface WordItem {
group_id: number; group_id: number;
} }
/**
* Извлекает значения полей из слова.
* Сначала ищет в основных полях (key, value, tags),
* потом в more (JSON-словарь).
* Если поле не найдено — пропускается (возвращается пустая строка, фильтруется).
*/
function getWordFieldValues(word: WordItem, fields: string): string[] { function getWordFieldValues(word: WordItem, fields: string): string[] {
const directFields: Record<string, string> = { const directFields: Record<string, string> = {
key: word.key, key: word.key,
@@ -47,6 +41,8 @@ export class TrainingPage {
private totalCards: number; private totalCards: number;
private forwardFields: string; private forwardFields: string;
private backwardFields: string; private backwardFields: string;
private seenWords = new Set<number>();
private shownCount = 0;
constructor( constructor(
container: HTMLElement, container: HTMLElement,
@@ -81,7 +77,7 @@ export class TrainingPage {
this.container.innerHTML = ` this.container.innerHTML = `
<div class="training-header"> <div class="training-header">
<button id="btnTrainingBack" class="btn" style="flex:none; padding:0.4rem 0.8rem;">← Назад</button> <button id="btnTrainingBack" class="btn" style="flex:none; padding:0.4rem 0.8rem;">← Назад</button>
<span style="font-size:0.85rem;color:var(--text);">слов: ${total}</span> <span style="font-size:0.85rem;color:var(--text);">уникальных: 0 / ${total}</span>
</div> </div>
<div id="training-card" class="training-card"> <div id="training-card" class="training-card">
<div class="training-content" id="training-content"> <div class="training-content" id="training-content">
@@ -98,22 +94,37 @@ export class TrainingPage {
}); });
} }
private updateCounter(): void {
const total = this.totalCards;
const span = this.container.querySelector('.training-header span');
if (span) {
span.textContent = `уникальных: ${this.seenWords.size} / ${total}`;
}
}
private renderCard(): void { private renderCard(): void {
const result = this.cardSet.next(); const result = this.cardSet.next();
const word = this.words.find((w) => w.id === result.word.id); const word = this.words.find((w) => w.id === result.word.id);
if (!word) return; if (!word) return;
// Обновляем дату последнего открытия в БД this.shownCount++;
// Учитываем уникальные слова
if (!this.seenWords.has(result.word.id)) {
this.seenWords.add(result.word.id);
}
this.updateCounter();
const stat = this.cardSet.set[this.cardSet.currentWordIndex!]; const stat = this.cardSet.set[this.cardSet.currentWordIndex!];
if (stat) { if (stat) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
stat.last_open = Date.now(); // обновляем в памяти из алгоритма stat.last_open = Date.now();
updateCardStat(stat.id, stat.score, now).catch((err) => updateCardStat(stat.id, stat.score, now).catch((err) =>
console.error('[Training] Ошибка сохранения last_opened:', err), console.error('[Training] Ошибка сохранения last_opened:', err),
); );
} }
this.renderSide(word, this.forwardFields, this.backwardFields, false); this.renderSide(word, this.forwardFields, this.backwardFields, false, stat);
this.renderActions(false); this.renderActions(false);
} }
@@ -122,19 +133,31 @@ export class TrainingPage {
forwardFields: string, forwardFields: string,
backwardFields: string, backwardFields: string,
showBack: boolean, showBack: boolean,
stat?: CardStatistics,
): void { ): void {
const contentEl = document.getElementById('training-content'); const contentEl = document.getElementById('training-content');
if (!contentEl) return; if (!contentEl) return;
// Текущий балл слова (с учётом затухания)
let scoreStr = '';
if (stat) {
const cs = calculatedScore(stat);
scoreStr = `${cs.toFixed(1)}`;
}
if (!showBack) { if (!showBack) {
const values = getWordFieldValues(word, forwardFields); const values = getWordFieldValues(word, forwardFields);
if (values.length === 0) { if (values.length === 0) {
contentEl.innerHTML = '<div class="training-field" style="opacity:0.4;">Нет данных для отображения</div>'; contentEl.innerHTML = `
<div class="training-field" style="opacity:0.4;">Нет данных для отображения</div>
${scoreStr ? `<div class="training-score">${scoreStr}</div>` : ''}
`;
return; return;
} }
contentEl.innerHTML = values contentEl.innerHTML = `
.map((v) => `<div class="training-field">${this.escapeHtml(v)}</div>`) ${values.map((v) => `<div class="training-field">${this.escapeHtml(v)}</div>`).join('')}
.join(''); ${scoreStr ? `<div class="training-score">${scoreStr}</div>` : ''}
`;
} else { } else {
const fwdVals = getWordFieldValues(word, forwardFields); const fwdVals = getWordFieldValues(word, forwardFields);
const bwdVals = getWordFieldValues(word, backwardFields); const bwdVals = getWordFieldValues(word, backwardFields);
@@ -151,6 +174,7 @@ export class TrainingPage {
? bwdVals.map((v) => `<div class="training-field">${this.escapeHtml(v)}</div>`).join('') ? bwdVals.map((v) => `<div class="training-field">${this.escapeHtml(v)}</div>`).join('')
: '<div class="training-field" style="opacity:0.4;">—</div>'} : '<div class="training-field" style="opacity:0.4;">—</div>'}
</div> </div>
${scoreStr ? `<div class="training-score">${scoreStr}</div>` : ''}
`; `;
} }
} }
@@ -189,7 +213,9 @@ export class TrainingPage {
const word = this.words[current]; const word = this.words[current];
if (!word) return; if (!word) return;
this.renderSide(word, this.forwardFields, this.backwardFields, true); const stat = this.cardSet.set[current];
this.renderSide(word, this.forwardFields, this.backwardFields, true, stat);
this.renderActions(true); this.renderActions(true);
} }