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 2f4657c0fb
commit 62624502e2
4 changed files with 48 additions and 6 deletions

View File

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

View File

@@ -870,9 +870,9 @@
"reviewTimeNow": "jetzt", "reviewTimeNow": "jetzt",
"reviewTimeTomorrow": "morgen", "reviewTimeTomorrow": "morgen",
"reviewTimeInDays": "in {count} Tagen", "reviewTimeInDays": "in {count} Tagen",
"srsDueStat": "Tageswiederholung: {scheduled} von {total}", "srsDueStat": "Tageswiederholung: {scheduled} Begriffe",
"srsEyebrow": "Langzeitgedächtnis", "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.", "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", "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." "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", "reviewTimeNow": "now",
"reviewTimeTomorrow": "tomorrow", "reviewTimeTomorrow": "tomorrow",
"reviewTimeInDays": "in {count} days", "reviewTimeInDays": "in {count} days",
"srsDueStat": "Daily review: {scheduled} of {total}", "srsDueStat": "Daily review: {scheduled} terms",
"srsEyebrow": "Long-term memory", "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.", "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", "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." "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> </div>
<div v-if="srsDueCount > 0" class="course-srs-plan"> <div v-if="srsDailyCount > 0" class="course-srs-plan">
<div> <div>
<span class="course-srs-plan__eyebrow">{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}</span> <span class="course-srs-plan__eyebrow">{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}</span>
<h4>{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}</h4> <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"> <div v-if="todayRecommendedSteps.length > 0" class="course-today-plan">
<h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4> <h4 class="course-today-plan__title">{{ $t('socialnetwork.vocab.courses.courseTodayPlanTitle') }}</h4>
<p class="course-today-plan__intro"> <p class="course-today-plan__intro">
{{ srsDueCount > 0 {{ srsDailyCount > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntroSrs') ? $t('socialnetwork.vocab.courses.courseTodayPlanIntroSrs')
: dueReviewLessons.length > 0 : dueReviewLessons.length > 0
? $t('socialnetwork.vocab.courses.courseTodayPlanIntro') ? $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 VocabPracticeDialog from '@/dialogues/socialnetwork/VocabPracticeDialog.vue';
import { localizeVocabCourseTitle } from '@/utils/vocabCourseTitle.js'; import { localizeVocabCourseTitle } from '@/utils/vocabCourseTitle.js';
const SRS_SESSION_STORAGE_VERSION = 2;
export default { export default {
name: 'VocabCourseView', name: 'VocabCourseView',
components: { VocabPracticeDialog }, components: { VocabPracticeDialog },
@@ -372,6 +374,7 @@ export default {
srsDueItems: [], srsDueItems: [],
srsDueTotal: 0, srsDueTotal: 0,
srsDailyLimit: 50, srsDailyLimit: 50,
srsTodayRemaining: null,
srsLoading: false, srsLoading: false,
showAddLessonDialog: false, showAddLessonDialog: false,
assistantSettings: null, assistantSettings: null,
@@ -422,6 +425,9 @@ export default {
return Array.isArray(this.srsDueItems) ? this.srsDueItems.length : 0; return Array.isArray(this.srsDueItems) ? this.srsDueItems.length : 0;
}, },
srsDailyCount() { srsDailyCount() {
if (Number.isFinite(Number(this.srsTodayRemaining))) {
return Math.max(0, Number(this.srsTodayRemaining));
}
return Math.min(this.srsDueCount, this.srsDailyLimit); return Math.min(this.srsDueCount, this.srsDailyLimit);
}, },
hardVocabCount() { hardVocabCount() {
@@ -563,6 +569,29 @@ export default {
displayCourseTitle(course) { displayCourseTitle(course) {
return localizeVocabCourseTitle(course?.title, this.$i18n?.locale) || ''; 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() { async refreshHardVocabList() {
if (!this.courseId) { if (!this.courseId) {
this.hardVocabList = []; this.hardVocabList = [];
@@ -586,6 +615,12 @@ export default {
}, },
handleWindowFocus() { handleWindowFocus() {
this.refreshHardVocabList(); this.refreshHardVocabList();
this.syncSrsSessionProgress();
},
handleSrsSessionChanged(event) {
if (String(event?.detail?.courseId || '') === String(this.courseId)) {
this.syncSrsSessionProgress();
}
}, },
handleHardVocabChanged(event) { handleHardVocabChanged(event) {
const detailCourseId = event?.detail?.courseId; const detailCourseId = event?.detail?.courseId;
@@ -635,6 +670,7 @@ export default {
this.srsDailyLimit = 50; this.srsDailyLimit = 50;
} finally { } finally {
this.srsLoading = false; this.srsLoading = false;
this.syncSrsSessionProgress();
} }
}, },
async loadChapters() { async loadChapters() {
@@ -1009,10 +1045,13 @@ export default {
this.loadAssistantSettings() this.loadAssistantSettings()
]); ]);
this.refreshHardVocabList(); this.refreshHardVocabList();
this.syncSrsSessionProgress();
window.addEventListener('focus', this.handleWindowFocus); window.addEventListener('focus', this.handleWindowFocus);
window.addEventListener('yourpart:vocab:srsSession:changed', this.handleSrsSessionChanged);
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener('focus', this.handleWindowFocus); window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('yourpart:vocab:srsSession:changed', this.handleSrsSessionChanged);
}, },
}; };
</script> </script>