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

@@ -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>