From 62624502e25511241c1177874150fae3766712a2 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 21 Aug 2026 11:31:30 +0200 Subject: [PATCH] feat(vocab): synchronize SRS session progress and update related UI elements --- .../socialnetwork/VocabPracticeDialog.vue | 3 ++ .../src/i18n/locales/de/socialnetwork.json | 4 +- .../src/i18n/locales/en/socialnetwork.json | 4 +- frontend/src/views/social/VocabCourseView.vue | 43 ++++++++++++++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue index e951436..754b626 100755 --- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue +++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue @@ -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.) } diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json index fdd5c33..b4a330e 100755 --- a/frontend/src/i18n/locales/de/socialnetwork.json +++ b/frontend/src/i18n/locales/de/socialnetwork.json @@ -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." diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json index 935c5c1..1773e25 100755 --- a/frontend/src/i18n/locales/en/socialnetwork.json +++ b/frontend/src/i18n/locales/en/socialnetwork.json @@ -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." diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue index ff3ea15..770a1a3 100755 --- a/frontend/src/views/social/VocabCourseView.vue +++ b/frontend/src/views/social/VocabCourseView.vue @@ -84,7 +84,7 @@ -
+
{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}

{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}

@@ -98,7 +98,7 @@

{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}

- {{ 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); }, };