feat(vocab): synchronize SRS session progress and update related UI elements

This commit is contained in:
Torsten Schulz (local)
2026-08-21 11:31:30 +02:00
parent bb6e3db7c7
commit 651cba05de
4 changed files with 48 additions and 6 deletions

View File

@@ -472,6 +472,9 @@ export default {
session.correctCount = this.correctCount;
session.wrongCount = this.wrongCount;
localStorage.setItem(key, JSON.stringify(session));
window.dispatchEvent(new CustomEvent('yourpart:vocab:srsSession:changed', {
detail: { courseId: this.openParams.courseId }
}));
} catch (_) {
// ignore storage issues (private mode, quota, etc.)
}

View File

@@ -870,9 +870,9 @@
"reviewTimeNow": "jetzt",
"reviewTimeTomorrow": "morgen",
"reviewTimeInDays": "in {count} Tagen",
"srsDueStat": "Tageswiederholung: {scheduled} von {total}",
"srsDueStat": "Tageswiederholung: {scheduled} Begriffe",
"srsEyebrow": "Langzeitgedächtnis",
"srsTitle": "Heute: {scheduled} von {total} fälligen Begriffen",
"srsTitle": "Heute: {scheduled} fällige Begriffe",
"srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Pro Tag werden höchstens 50 Begriffe eingeplant; weitere fällige Begriffe bleiben für die nächste Tageswiederholung erhalten.",
"srsStart": "Tageswiederholung starten",
"courseTodayPlanIntroSrs": "Didaktische Reihenfolge: Zuerst die fällige SRS-Tageswiederholung einzelner Begriffe. Danach kommen Kurz-Wiederholungen, Blockfortschritt und ggf. intensive Wiederholung. So wird altes Material stabilisiert, bevor neues Material dazukommt."

View File

@@ -870,9 +870,9 @@
"reviewTimeNow": "now",
"reviewTimeTomorrow": "tomorrow",
"reviewTimeInDays": "in {count} days",
"srsDueStat": "Daily review: {scheduled} of {total}",
"srsDueStat": "Daily review: {scheduled} terms",
"srsEyebrow": "Long-term memory",
"srsTitle": "Today: {scheduled} of {total} due terms",
"srsTitle": "Today: {scheduled} due terms",
"srsIntro": "This review comes from the SRS schedule of individual terms. At most 50 terms are planned per day; any additional due terms remain for the next daily review.",
"srsStart": "Start daily review",
"courseTodayPlanIntroSrs": "Pedagogical order: start with the due SRS daily review for individual terms. Then quick reviews, block progress, and intensive review if needed. This stabilizes old material before new material is added."

View File

@@ -84,7 +84,7 @@
</div>
</div>
<div v-if="srsDueCount > 0" class="course-srs-plan">
<div v-if="srsDailyCount > 0" class="course-srs-plan">
<div>
<span class="course-srs-plan__eyebrow">{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}</span>
<h4>{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}</h4>
@@ -98,7 +98,7 @@
<div v-if="todayRecommendedSteps.length > 0" class="course-today-plan">
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4>
<p class="course-today-plan__intro">
{{ srsDueCount > 0
{{ srsDailyCount > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntroSrs')
: dueReviewLessons.length > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntro')
@@ -354,6 +354,8 @@ import { confirmAction, showApiError, showInfo, showSuccess } from '@/utils/feed
import VocabPracticeDialog from '@/dialogues/socialnetwork/VocabPracticeDialog.vue';
import { localizeVocabCourseTitle } from '@/utils/vocabCourseTitle.js';
const SRS_SESSION_STORAGE_VERSION = 2;
export default {
name: 'VocabCourseView',
components: { VocabPracticeDialog },
@@ -372,6 +374,7 @@ export default {
srsDueItems: [],
srsDueTotal: 0,
srsDailyLimit: 50,
srsTodayRemaining: null,
srsLoading: false,
showAddLessonDialog: false,
assistantSettings: null,
@@ -422,6 +425,9 @@ export default {
return Array.isArray(this.srsDueItems) ? this.srsDueItems.length : 0;
},
srsDailyCount() {
if (Number.isFinite(Number(this.srsTodayRemaining))) {
return Math.max(0, Number(this.srsTodayRemaining));
}
return Math.min(this.srsDueCount, this.srsDailyLimit);
},
hardVocabCount() {
@@ -563,6 +569,29 @@ export default {
displayCourseTitle(course) {
return localizeVocabCourseTitle(course?.title, this.$i18n?.locale) || '';
},
getLocalDateKey() {
const date = new Date();
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
},
syncSrsSessionProgress() {
const key = `yourpart:vocab:srsSession:v${SRS_SESSION_STORAGE_VERSION}:${this.courseId}:${this.getLocalDateKey()}`;
try {
const session = JSON.parse(localStorage.getItem(key) || 'null');
const isCurrentSession = session
&& session.version === SRS_SESSION_STORAGE_VERSION
&& String(session.courseId) === String(this.courseId)
&& session.dateKey === this.getLocalDateKey();
if (!isCurrentSession) {
this.srsTodayRemaining = null;
return;
}
const total = Math.max(0, Number(session.initialTotalDue) || 0);
const done = new Set(Array.isArray(session.doneIds) ? session.doneIds : []).size;
this.srsTodayRemaining = Math.max(0, total - done);
} catch (_) {
this.srsTodayRemaining = null;
}
},
async refreshHardVocabList() {
if (!this.courseId) {
this.hardVocabList = [];
@@ -586,6 +615,12 @@ export default {
},
handleWindowFocus() {
this.refreshHardVocabList();
this.syncSrsSessionProgress();
},
handleSrsSessionChanged(event) {
if (String(event?.detail?.courseId || '') === String(this.courseId)) {
this.syncSrsSessionProgress();
}
},
handleHardVocabChanged(event) {
const detailCourseId = event?.detail?.courseId;
@@ -635,6 +670,7 @@ export default {
this.srsDailyLimit = 50;
} finally {
this.srsLoading = false;
this.syncSrsSessionProgress();
}
},
async loadChapters() {
@@ -1009,10 +1045,13 @@ export default {
this.loadAssistantSettings()
]);
this.refreshHardVocabList();
this.syncSrsSessionProgress();
window.addEventListener('focus', this.handleWindowFocus);
window.addEventListener('yourpart:vocab:srsSession:changed', this.handleSrsSessionChanged);
},
beforeUnmount() {
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('yourpart:vocab:srsSession:changed', this.handleSrsSessionChanged);
},
};
</script>