|
@@ -30,7 +30,7 @@ import {
|
|
|
Volume2,
|
|
Volume2,
|
|
|
X,
|
|
X,
|
|
|
} from 'lucide-angular';
|
|
} from 'lucide-angular';
|
|
|
-import { catchError, finalize, forkJoin, map, Observable, of, Subscription, switchMap, tap, throwError } from 'rxjs';
|
|
|
|
|
|
|
+import { catchError, concatMap, finalize, forkJoin, from, map, Observable, of, Subscription, switchMap, tap, throwError, toArray } from 'rxjs';
|
|
|
import { listeningChoices, shuffledStudy } from '../../core/learning-random';
|
|
import { listeningChoices, shuffledStudy } from '../../core/learning-random';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import {
|
|
import {
|
|
@@ -128,6 +128,11 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly selected = signal<Set<string>>(new Set());
|
|
readonly selected = signal<Set<string>>(new Set());
|
|
|
readonly selectionRevealed = signal<Set<string>>(new Set());
|
|
readonly selectionRevealed = signal<Set<string>>(new Set());
|
|
|
readonly selectionAssessment = signal<Record<string, 'known' | 'unknown'>>({});
|
|
readonly selectionAssessment = signal<Record<string, 'known' | 'unknown'>>({});
|
|
|
|
|
+ readonly bulkSelection = signal<Set<string> | null>(null);
|
|
|
|
|
+ readonly knownSaveRetry = signal(false);
|
|
|
|
|
+ private knownSaveContinuation: { continueLearning: boolean; openKnownPass: boolean } | null = null;
|
|
|
|
|
+ private courseSelectionNeedsRebase = false;
|
|
|
|
|
+ readonly bulkCandidates = computed(() => this.visibleCourseWords().filter(word => !this.wordHistoricallyLearned(word)));
|
|
|
readonly activeSelectionBucket = signal<'pending' | 'completed'>('pending');
|
|
readonly activeSelectionBucket = signal<'pending' | 'completed'>('pending');
|
|
|
readonly coursePassTabs = ['未学习', '已学一遍', '已学第二遍', '已学第三遍'];
|
|
readonly coursePassTabs = ['未学习', '已学一遍', '已学第二遍', '已学第三遍'];
|
|
|
readonly coursePass = signal(0);
|
|
readonly coursePass = signal(0);
|
|
@@ -159,6 +164,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly calendarLoading = signal(false);
|
|
readonly calendarLoading = signal(false);
|
|
|
readonly calendarError = signal('');
|
|
readonly calendarError = signal('');
|
|
|
readonly submitting = signal(false);
|
|
readonly submitting = signal(false);
|
|
|
|
|
+ readonly selectionLocked = computed(() => this.submitting() || this.knownSaveRetry());
|
|
|
readonly selectionTotal=signal(0);readonly selectionPage=signal(1);readonly selectionLoading=signal(false);private selectionRequest?:Subscription;
|
|
readonly selectionTotal=signal(0);readonly selectionPage=signal(1);readonly selectionLoading=signal(false);private selectionRequest?:Subscription;
|
|
|
readonly error = signal('');
|
|
readonly error = signal('');
|
|
|
readonly recordWordless = signal(false);
|
|
readonly recordWordless = signal(false);
|
|
@@ -257,6 +263,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly allVisibleNewWordsSelected = computed(() => this.visibleNewWords().length > 0 && this.visibleNewWords().every((word) => this.selected().has(word.id)));
|
|
readonly allVisibleNewWordsSelected = computed(() => this.visibleNewWords().length > 0 && this.visibleNewWords().every((word) => this.selected().has(word.id)));
|
|
|
readonly newlyKnownWords = computed(() => (this.isRefineSelectionPage ? this.courseSelectionCache() : this.words()).filter((word) => !this.wordHistoricallyLearned(word) && this.selectionAssessment()[word.id] === 'known'));
|
|
readonly newlyKnownWords = computed(() => (this.isRefineSelectionPage ? this.courseSelectionCache() : this.words()).filter((word) => !this.wordHistoricallyLearned(word) && this.selectionAssessment()[word.id] === 'known'));
|
|
|
readonly selectionActionLabel = computed(() => {
|
|
readonly selectionActionLabel = computed(() => {
|
|
|
|
|
+ if (this.knownSaveRetry()) return '重试保存';
|
|
|
const hasLearningWords = this.selected().size > 0;
|
|
const hasLearningWords = this.selected().size > 0;
|
|
|
const hasKnownWords = this.newlyKnownWords().length > 0;
|
|
const hasKnownWords = this.newlyKnownWords().length > 0;
|
|
|
if (hasLearningWords && hasKnownWords) return '保存并开始学习';
|
|
if (hasLearningWords && hasKnownWords) return '保存并开始学习';
|
|
@@ -628,6 +635,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
toggleWord(word: Word): void {
|
|
toggleWord(word: Word): void {
|
|
|
|
|
+ if (this.selectionLocked()) return;
|
|
|
|
|
+ const bulk = this.bulkSelection();
|
|
|
|
|
+ if (this.isRefineSelectionPage && bulk !== null) {
|
|
|
|
|
+ const next = new Set(bulk);
|
|
|
|
|
+ if (next.has(word.id)) next.delete(word.id); else next.add(word.id);
|
|
|
|
|
+ this.bulkSelection.set(next);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
if (this.isRefineSelectionPage && !this.canRelearnCourseWord(word)) return;
|
|
if (this.isRefineSelectionPage && !this.canRelearnCourseWord(word)) return;
|
|
|
const shouldSelect = !this.selected().has(word.id);
|
|
const shouldSelect = !this.selected().has(word.id);
|
|
|
if (!this.setWordSelected(word, shouldSelect) || !this.isRefineSelectionPage) return;
|
|
if (!this.setWordSelected(word, shouldSelect) || !this.isRefineSelectionPage) return;
|
|
@@ -668,13 +683,15 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
get coursePassHint(): string {
|
|
get coursePassHint(): string {
|
|
|
const pass = this.coursePass();
|
|
const pass = this.coursePass();
|
|
|
- if (pass === 0) return this.isReadOnlySelectionPage ? '这些单词尚未完成第一遍学习,点击可查看释义。' : '先回忆中文,核对后选择“认识”或“加入学习”。';
|
|
|
|
|
|
|
+ if (pass === 0) return this.isReadOnlySelectionPage ? '这些单词尚未完成第一遍学习,点击可查看释义。' : '可直接选择“认识”或“不认识”,也可全选本页后批量判断。';
|
|
|
if (pass === 3) return '这些单词已完成三遍学习,可查看和听读音。';
|
|
if (pass === 3) return '这些单词已完成三遍学习,可查看和听读音。';
|
|
|
return this.isReadOnlySelectionPage ? `这些单词已学${pass}遍,可由老师选词继续学习。` : `选择单词开始第${pass === 1 ? '二' : '三'}遍学习。`;
|
|
return this.isReadOnlySelectionPage ? `这些单词已学${pass}遍,可由老师选词继续学习。` : `选择单词开始第${pass === 1 ? '二' : '三'}遍学习。`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
selectCoursePass(pass: number): void {
|
|
selectCoursePass(pass: number): void {
|
|
|
- if (pass === this.coursePass() || pass < 0 || pass > 3) return;
|
|
|
|
|
|
|
+ if (this.selectionLocked() || pass === this.coursePass() || pass < 0 || pass > 3) return;
|
|
|
|
|
+ this.bulkSelection.set(null);
|
|
|
|
|
+ this.courseSelectionNeedsRebase = false;
|
|
|
this.selectionRequest?.unsubscribe();
|
|
this.selectionRequest?.unsubscribe();
|
|
|
this.coursePass.set(pass);
|
|
this.coursePass.set(pass);
|
|
|
this.selectionPage.set(1);
|
|
this.selectionPage.set(1);
|
|
@@ -698,14 +715,53 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
markWordKnown(word: Word): void {
|
|
markWordKnown(word: Word): void {
|
|
|
- if (this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
|
|
|
|
+ if (this.selectionLocked() || this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
this.setWordSelected(word, false);
|
|
this.setWordSelected(word, false);
|
|
|
this.setSelectionAssessment(word, 'known');
|
|
this.setSelectionAssessment(word, 'known');
|
|
|
|
|
+ this.bulkSelection.update(current => current === null ? null : new Set([...current].filter(id => id !== word.id)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
markWordUnknown(word: Word): void {
|
|
markWordUnknown(word: Word): void {
|
|
|
- if (this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
|
|
- if (this.setWordSelected(word, true)) this.setSelectionAssessment(word, 'unknown');
|
|
|
|
|
|
|
+ if (this.selectionLocked() || this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
|
|
+ if (this.setWordSelected(word, true)) {
|
|
|
|
|
+ this.setSelectionAssessment(word, 'unknown');
|
|
|
|
|
+ this.bulkSelection.update(current => current === null ? null : new Set([...current].filter(id => id !== word.id)));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ courseWordChecked(word: Word): boolean {
|
|
|
|
|
+ return (this.bulkSelection() ?? this.selected()).has(word.id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toggleBulkSelection(): void {
|
|
|
|
|
+ if (this.selectionLocked() || this.selectionLoading() || this.isReadOnlySelectionPage || this.coursePass() !== 0) return;
|
|
|
|
|
+ this.bulkSelection.set(this.bulkSelection() === null ? new Set(this.bulkCandidates().map(word => word.id)) : null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ applyBulkAssessment(assessment: 'known' | 'unknown'): void {
|
|
|
|
|
+ if (this.selectionLocked() || this.isReadOnlySelectionPage || this.coursePass() !== 0) return;
|
|
|
|
|
+ const checked = this.bulkSelection();
|
|
|
|
|
+ if (!checked?.size) return;
|
|
|
|
|
+ const candidates = this.bulkCandidates();
|
|
|
|
|
+ const chosen = candidates.filter(word => checked.has(word.id));
|
|
|
|
|
+ if (!chosen.length) return;
|
|
|
|
|
+ if (assessment === 'unknown') {
|
|
|
|
|
+ const combined = new Set([...this.selected(), ...chosen.map(word => word.id)]);
|
|
|
|
|
+ if (combined.size > this.selectionLimit) {
|
|
|
|
|
+ this.platform.toast(`本轮最多学习 ${this.selectionLimit} 个单词,请减少勾选后重试`, 'error');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.bulkSelection.set(null);
|
|
|
|
|
+ for (const word of chosen) this.markWordUnknown(word);
|
|
|
|
|
+ this.startSelected();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const remaining = candidates.filter(word => !checked.has(word.id));
|
|
|
|
|
+ this.bulkSelection.set(null);
|
|
|
|
|
+ for (const word of chosen) this.markWordKnown(word);
|
|
|
|
|
+ const canContinue = new Set([...this.selected(), ...remaining.map(word => word.id)]).size <= this.selectionLimit;
|
|
|
|
|
+ if (canContinue) for (const word of remaining) this.markWordUnknown(word);
|
|
|
|
|
+ this.startSelected(canContinue, canContinue && this.selected().size === 0);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
toggleAllWords(): void {
|
|
toggleAllWords(): void {
|
|
@@ -730,8 +786,11 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.selected.set(new Set(this.words().slice(0, this.selectionLimit).map((word) => word.id)));
|
|
this.selected.set(new Set(this.words().slice(0, this.selectionLimit).map((word) => word.id)));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- startSelected(): void {
|
|
|
|
|
- if (this.submitting() || this.isReadOnlySelectionPage) return;
|
|
|
|
|
|
|
+ startSelected(continueLearning = true, openKnownPass = false): void {
|
|
|
|
|
+ if (this.submitting() || this.selectionLoading() || this.isReadOnlySelectionPage) return;
|
|
|
|
|
+ if (this.knownSaveRetry() && this.knownSaveContinuation) {
|
|
|
|
|
+ ({ continueLearning, openKnownPass } = this.knownSaveContinuation);
|
|
|
|
|
+ }
|
|
|
const available = this.isNewWordSelectionPage ? [...this.newWordSelectionCache.values()] : this.isRefineSelectionPage ? this.courseSelectionCache().filter(word => this.canRelearnCourseWord(word) && this.selectionAssessment()[word.id] !== 'known') : this.words();
|
|
const available = this.isNewWordSelectionPage ? [...this.newWordSelectionCache.values()] : this.isRefineSelectionPage ? this.courseSelectionCache().filter(word => this.canRelearnCourseWord(word) && this.selectionAssessment()[word.id] !== 'known') : this.words();
|
|
|
const chosen = available
|
|
const chosen = available
|
|
|
.filter((word) => this.selected().has(word.id));
|
|
.filter((word) => this.selected().has(word.id));
|
|
@@ -748,16 +807,24 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
this.submitting.set(true);
|
|
this.submitting.set(true);
|
|
|
- this.persistWordsAsLearned(newlyKnown, student).pipe(finalize(() => this.submitting.set(false))).subscribe({
|
|
|
|
|
|
|
+ this.knownSaveContinuation = { continueLearning, openKnownPass };
|
|
|
|
|
+ this.persistWordsAsLearned(newlyKnown, student, batch => this.commitKnownWords(batch)).subscribe({
|
|
|
next: () => {
|
|
next: () => {
|
|
|
- if (newlyKnown.length) this.commitKnownWords(newlyKnown);
|
|
|
|
|
- if (chosen.length) {
|
|
|
|
|
|
|
+ this.knownSaveRetry.set(false);
|
|
|
|
|
+ this.knownSaveContinuation = null;
|
|
|
|
|
+ this.submitting.set(false);
|
|
|
|
|
+ if (continueLearning && chosen.length) {
|
|
|
this.beginSelectedFlow(chosen, '');
|
|
this.beginSelectedFlow(chosen, '');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (openKnownPass) this.selectCoursePass(1);
|
|
|
this.platform.toast('识词结果已保存', 'success');
|
|
this.platform.toast('识词结果已保存', 'success');
|
|
|
},
|
|
},
|
|
|
- error: (error: ApiError) => this.platform.toast(error.message, 'error'),
|
|
|
|
|
|
|
+ error: (error: ApiError) => {
|
|
|
|
|
+ this.knownSaveRetry.set(true);
|
|
|
|
|
+ this.submitting.set(false);
|
|
|
|
|
+ this.platform.toast(`${error.message};已保存的批次会保留,请点击“重试保存”完成核对`, 'error');
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -1348,7 +1415,18 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- loadMoreSelection():void{if(!this.selectionLoading()&&this.words().length<this.selectionTotal()){this.selectionPage.update(p=>p+1);this.loadSelection(true);}}
|
|
|
|
|
|
|
+ loadMoreSelection(): void {
|
|
|
|
|
+ if (this.selectionLocked() || this.selectionLoading() || this.words().length >= this.selectionTotal()) return;
|
|
|
|
|
+ this.bulkSelection.set(null);
|
|
|
|
|
+ if (this.isRefineSelectionPage && this.courseSelectionNeedsRebase) {
|
|
|
|
|
+ const previousPage = this.selectionPage();
|
|
|
|
|
+ this.selectionPage.set(1);
|
|
|
|
|
+ this.loadSelection(false, previousPage);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.selectionPage.update(page => page + 1);
|
|
|
|
|
+ this.loadSelection(true);
|
|
|
|
|
+ }
|
|
|
selectNewWordBucket(bucket: 'pending' | 'completed'): void {
|
|
selectNewWordBucket(bucket: 'pending' | 'completed'): void {
|
|
|
if (this.activeSelectionBucket() === bucket) return;
|
|
if (this.activeSelectionBucket() === bucket) return;
|
|
|
this.selectionRequest?.unsubscribe();
|
|
this.selectionRequest?.unsubscribe();
|
|
@@ -1410,13 +1488,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private loadSelection(more=false): void {
|
|
|
|
|
|
|
+ private loadSelection(more=false, rebaseFromPage?: number): void {
|
|
|
const accept=(result:unknown,total:unknown)=>{const incoming=this.findList(result).map(item=>this.meta.path==='pages/member/starshop'?this.toNewWord(item):this.toWord(item)).filter(w=>w.word);this.words.set(more?[...this.words(),...incoming]:incoming);this.selectionTotal.set(Number(total)||this.words().length);};
|
|
const accept=(result:unknown,total:unknown)=>{const incoming=this.findList(result).map(item=>this.meta.path==='pages/member/starshop'?this.toNewWord(item):this.toWord(item)).filter(w=>w.word);this.words.set(more?[...this.words(),...incoming]:incoming);this.selectionTotal.set(Number(total)||this.words().length);};
|
|
|
- const failure=(error:ApiError)=>{if(more){this.selectionPage.update(p=>p-1);this.platform.toast(error.message,'error');}else this.error.set(error.message);};
|
|
|
|
|
|
|
+ const failure=(error:ApiError)=>{if(rebaseFromPage!==undefined){this.selectionPage.set(rebaseFromPage);this.platform.toast(error.message,'error');}else if(more){this.selectionPage.update(p=>p-1);this.platform.toast(error.message,'error');}else this.error.set(error.message);};
|
|
|
if (this.meta.path === 'pages/stu/stu_abc') {
|
|
if (this.meta.path === 'pages/stu/stu_abc') {
|
|
|
const student = this.session.student() ?? this.session.user();this.selectionLoading.set(true);
|
|
const student = this.session.student() ?? this.session.user();this.selectionLoading.set(true);
|
|
|
this.selectionRequest=this.api.get<unknown>('e_ck_list',{checkwords:2,nids:this.query('nids')||this.query('nid'),pnid:this.query('pnid'),learningPass:this.coursePass(),modelId:52,cpage:this.selectionPage(),psize:100,uid:student?.userId}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({next:({result,page})=>{
|
|
this.selectionRequest=this.api.get<unknown>('e_ck_list',{checkwords:2,nids:this.query('nids')||this.query('nid'),pnid:this.query('pnid'),learningPass:this.coursePass(),modelId:52,cpage:this.selectionPage(),psize:100,uid:student?.userId}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({next:({result,page})=>{
|
|
|
accept(result,page?.itemCount);
|
|
accept(result,page?.itemCount);
|
|
|
|
|
+ if (rebaseFromPage !== undefined) this.courseSelectionNeedsRebase = false;
|
|
|
const merged=new Map(this.courseSelectionCache().map(word=>[word.id,word]));
|
|
const merged=new Map(this.courseSelectionCache().map(word=>[word.id,word]));
|
|
|
for(const word of this.words()) merged.set(word.id,word);
|
|
for(const word of this.words()) merged.set(word.id,word);
|
|
|
this.courseSelectionCache.set([...merged.values()]);
|
|
this.courseSelectionCache.set([...merged.values()]);
|
|
@@ -2236,6 +2315,10 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.legacyNewWords = null;
|
|
this.legacyNewWords = null;
|
|
|
this.selectionRevealed.set(new Set());
|
|
this.selectionRevealed.set(new Set());
|
|
|
this.selectionAssessment.set({});
|
|
this.selectionAssessment.set({});
|
|
|
|
|
+ this.bulkSelection.set(null);
|
|
|
|
|
+ this.knownSaveRetry.set(false);
|
|
|
|
|
+ this.knownSaveContinuation = null;
|
|
|
|
|
+ this.courseSelectionNeedsRebase = false;
|
|
|
this.activeSelectionBucket.set('pending');
|
|
this.activeSelectionBucket.set('pending');
|
|
|
this.coursePass.set(0);
|
|
this.coursePass.set(0);
|
|
|
this.coursePassCounts.set(null);
|
|
this.coursePassCounts.set(null);
|
|
@@ -2341,18 +2424,21 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.selectionAssessment.update((current) => Object.fromEntries(Object.entries(current).filter(([id]) => id !== word.id)) as Record<string, 'known' | 'unknown'>);
|
|
this.selectionAssessment.update((current) => Object.fromEntries(Object.entries(current).filter(([id]) => id !== word.id)) as Record<string, 'known' | 'unknown'>);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private persistWordsAsLearned(words: Word[], owner: AppUser | null | undefined): Observable<unknown> {
|
|
|
|
|
|
|
+ private persistWordsAsLearned(words: Word[], owner: AppUser | null | undefined, onBatchSaved?: (batch: Word[]) => void): Observable<unknown> {
|
|
|
if (!words.length) return of(null);
|
|
if (!words.length) return of(null);
|
|
|
- return this.api.post('e_add_words', {
|
|
|
|
|
|
|
+ const batches: Word[][] = [];
|
|
|
|
|
+ for (let index = 0; index < words.length; index += 100) batches.push(words.slice(index, index + 100));
|
|
|
|
|
+ return from(batches).pipe(concatMap(batch => this.api.post('e_add_words', {
|
|
|
uid: owner?.userId,
|
|
uid: owner?.userId,
|
|
|
- wordsId: words.map((word) => word.wordId).join(','),
|
|
|
|
|
|
|
+ wordsId: batch.map((word) => word.wordId).join(','),
|
|
|
kcid: (this.meta.path === 'pages/stu/pl_end' ? this.orderInfo()['learningCourseId'] : '') || this.query('pnid') || this.query('kcid') || this.orderInfo()['kcid'],
|
|
kcid: (this.meta.path === 'pages/stu/pl_end' ? this.orderInfo()['learningCourseId'] : '') || this.query('pnid') || this.query('kcid') || this.orderInfo()['kcid'],
|
|
|
save: 1,
|
|
save: 1,
|
|
|
- requestKey:this.draft().command('learn:'+words.map(w=>w.wordId).sort().join(',')),
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ requestKey:this.draft().command('learn:'+batch.map(w=>w.wordId).sort().join(',')),
|
|
|
|
|
+ }).pipe(tap(() => onBatchSaved?.(batch)))), toArray());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private commitKnownWords(words: Word[]): void {
|
|
private commitKnownWords(words: Word[]): void {
|
|
|
|
|
+ if (this.isRefineSelectionPage && this.coursePass() === 0) this.courseSelectionNeedsRebase = true;
|
|
|
const ids = new Set(words.map(word => word.id));
|
|
const ids = new Set(words.map(word => word.id));
|
|
|
const update = (current: Word[]) => current.map(word => ids.has(word.id) ? {...word, w_learned: 1} : word);
|
|
const update = (current: Word[]) => current.map(word => ids.has(word.id) ? {...word, w_learned: 1} : word);
|
|
|
this.words.update(update);
|
|
this.words.update(update);
|