SRS 2. Teil

This commit is contained in:
aknuth 2024-12-12 17:20:43 +01:00
parent ac69a11db5
commit d5ac4d6f26
3 changed files with 148 additions and 67 deletions

View File

@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { map, Observable, switchMap } from 'rxjs'; import { map, Observable, switchMap } from 'rxjs';
export interface Deck { export interface Deck {
id?:number;
name: string; name: string;
images: DeckImage[]; images: DeckImage[];
} }

View File

@ -52,7 +52,6 @@
</div> </div>
<p class="mt-2">{{ progress }}</p> <p class="mt-2">{{ progress }}</p>
<!-- <p class="mt-2">Gewusst: {{ knownCount }} | Nicht gewusst: {{ unknownCount }}</p> -->
<button <button
(click)="closeTraining()" (click)="closeTraining()"

View File

@ -4,6 +4,7 @@ import { Deck, DeckImage, DeckService, Box } from '../deck.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { forkJoin } from 'rxjs'; import { forkJoin } from 'rxjs';
import { lastValueFrom } from 'rxjs'; // Import für toPromise-Alternative
@Component({ @Component({
selector: 'app-training', selector: 'app-training',
@ -29,40 +30,24 @@ export class TrainingComponent implements OnInit {
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService) { }
ngOnInit(): void { ngOnInit(): void {
// Initialisiere die boxesToReview basierend auf SRS
this.initializeBoxesToReview();
} }
ngAfterViewInit(){ ngAfterViewInit(){
if (this.deck && this.deck.images.length > 0) { // Die Initialisierung wurde bereits in ngOnInit durchgeführt
this.loadImage(this.currentImageIndex); // Initialisiere die erste Bild und die dazugehörigen Boxen
} else { if (this.deck && this.deck.images.length > 0) {
alert('Kein Deck oder keine Bilder vorhanden.'); this.loadImage(this.currentImageIndex);
this.close.emit(); } else {
} alert('Kein Deck oder keine Bilder vorhanden.');
} this.close.emit();
initializeBoxesToReview(): void {
// Filtere alle Boxen, die fällig sind (due <= heute)
const today = this.getTodayInDays();
this.deck.images.forEach(image => {
image.boxes.forEach(box => {
if (box.due === undefined || box.due <= today) {
this.boxesToReview.push(box);
this.boxRevealed.push(false);
} }
});
});
// Mische die Boxen
this.boxesToReview = this.shuffleArray(this.boxesToReview);
}
getTodayInDays(): number {
const epoch = new Date(1970, 0, 1); // Anki verwendet UNIX-Epoch
const today = new Date();
return Math.floor((today.getTime() - epoch.getTime()) / (1000 * 60 * 60 * 24));
} }
/**
* Lädt das Bild basierend auf dem gegebenen Index und initialisiert die zu überprüfenden Boxen.
* @param imageIndex Index des zu ladenden Bildes im Deck
*/
loadImage(imageIndex: number): void { loadImage(imageIndex: number): void {
if (imageIndex >= this.deck.images.length) { if (imageIndex >= this.deck.images.length) {
this.endTraining(); this.endTraining();
@ -70,13 +55,57 @@ export class TrainingComponent implements OnInit {
} }
this.currentImageData = this.deck.images[imageIndex]; this.currentImageData = this.deck.images[imageIndex];
// Initialisiere boxesToReview mit allen Boxen, die fällig sind, gemischt // Initialisiere die Boxen für die aktuelle Runde
this.boxesToReview = this.shuffleArray([...this.currentImageData.boxes].filter(box => box.due! <= this.getTodayInDays())); this.initializeBoxesToReview();
}
/**
* Ermittelt alle fälligen Boxen für die aktuelle Runde, mischt sie und setzt den aktuellen Box-Index zurück.
* Wenn keine Boxen mehr zu überprüfen sind, wird zum nächsten Bild gewechselt.
*/
initializeBoxesToReview(): void {
if (!this.currentImageData) {
this.nextImage();
return;
}
// Filtere alle Boxen, die fällig sind (due <= heute)
const today = this.getTodayInDays();
this.boxesToReview = this.currentImageData.boxes.filter(box => box.due === undefined || box.due <= today);
if (this.boxesToReview.length === 0) {
// Keine Boxen mehr für dieses Bild, wechsle zum nächsten Bild
this.nextImage();
return;
}
// Mische die Boxen zufällig
this.boxesToReview = this.shuffleArray(this.boxesToReview);
// Initialisiere den Array zur Verfolgung der enthüllten Boxen
this.boxRevealed = new Array(this.boxesToReview.length).fill(false); this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
// Setze den aktuellen Box-Index zurück
this.currentBoxIndex = 0;
this.isShowingBox = false; this.isShowingBox = false;
// Zeichne das Canvas neu
this.drawCanvas(); this.drawCanvas();
} }
/**
* Gibt das heutige Datum in Tagen seit der UNIX-Epoche zurück.
*/
getTodayInDays(): number {
const epoch = new Date(1970, 0, 1); // Anki verwendet UNIX-Epoche
const today = new Date();
return Math.floor((today.getTime() - epoch.getTime()) / (1000 * 60 * 60 * 24));
}
/**
* Zeichnet das aktuelle Bild und die Boxen auf das Canvas.
* Boxen werden rot dargestellt, wenn sie verdeckt sind, und grün, wenn sie enthüllt sind.
*/
drawCanvas(): void { drawCanvas(): void {
const canvas = this.canvasRef.nativeElement; const canvas = this.canvasRef.nativeElement;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -85,30 +114,33 @@ export class TrainingComponent implements OnInit {
const img = new Image(); const img = new Image();
img.src = `/api/debug_image/${this.currentImageData.id}/original_compressed.jpg`; img.src = `/api/debug_image/${this.currentImageData.id}/original_compressed.jpg`;
img.onload = () => { img.onload = () => {
// Set canvas size to image size // Setze die Größe des Canvas auf die Größe des Bildes
canvas.width = img.width; canvas.width = img.width;
canvas.height = img.height; canvas.height = img.height;
// Draw image // Zeichne das Bild
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Draw boxes // Zeichne die Boxen
this.boxesToReview.forEach((box, index) => { this.boxesToReview.forEach((box, index) => {
if (this.boxRevealed[index]) {
// Box ist bereits enthüllt, nichts zeichnen
return;
}
ctx.beginPath(); ctx.beginPath();
ctx.rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1); ctx.rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1);
ctx.fillStyle = index === this.currentBoxIndex ? 'rgba(0, 255, 0, 0.99)' : 'rgba(255, 0, 0, 0.99)'; if (this.currentBoxIndex === index && this.isShowingBox){
return;
// } else if (this.boxRevealed[index]) {
} else if (this.currentBoxIndex === index && !this.isShowingBox) {
// Box ist enthüllt
ctx.fillStyle = 'rgba(0, 255, 0, 1)'; // Transparente grüne Überlagerung
} else {
// Box ist verdeckt
ctx.fillStyle = 'rgba(255, 0, 0, 1)'; // Transparente rote Überlagerung
}
ctx.fill(); ctx.fill();
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.strokeStyle = 'black'; ctx.strokeStyle = 'black';
ctx.stroke(); ctx.stroke();
}); });
}; };
img.onerror = () => { img.onerror = () => {
@ -118,18 +150,21 @@ export class TrainingComponent implements OnInit {
}; };
} }
// Utility-Funktion zum Mischen eines Arrays /**
* Mischt ein Array zufällig.
* @param array Das zu mischende Array
* @returns Das gemischte Array
*/
shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
let currentIndex = array.length, randomIndex; let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle. // Solange noch Elemente zum Mischen vorhanden sind
while (currentIndex !== 0) { while (currentIndex !== 0) {
// Wähle ein verbleibendes Element
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex); randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--; currentIndex--;
// And swap it with the current element. // Tausche es mit dem aktuellen Element
[array[currentIndex], array[randomIndex]] = [ [array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]]; array[randomIndex], array[currentIndex]];
} }
@ -137,6 +172,9 @@ export class TrainingComponent implements OnInit {
return array; return array;
} }
/**
* Gibt die aktuelle Box zurück, die überprüft wird.
*/
get currentBox(): Box | null { get currentBox(): Box | null {
if (this.currentBoxIndex < this.boxesToReview.length) { if (this.currentBoxIndex < this.boxesToReview.length) {
return this.boxesToReview[this.currentBoxIndex]; return this.boxesToReview[this.currentBoxIndex];
@ -144,6 +182,9 @@ export class TrainingComponent implements OnInit {
return null; return null;
} }
/**
* Zeigt den Inhalt der aktuellen Box an.
*/
showText(): void { showText(): void {
if (this.currentBoxIndex >= this.boxesToReview.length) return; if (this.currentBoxIndex >= this.boxesToReview.length) return;
this.boxRevealed[this.currentBoxIndex] = true; this.boxRevealed[this.currentBoxIndex] = true;
@ -151,23 +192,37 @@ export class TrainingComponent implements OnInit {
this.drawCanvas(); this.drawCanvas();
} }
// Neue Methoden für Anki-Optionen /**
* Markiert die aktuelle Box als "Nochmal" und fährt mit der nächsten Box fort.
*/
async markAgain(): Promise<void> { async markAgain(): Promise<void> {
await this.updateCard('again'); await this.updateCard('again');
this.coverCurrentBox();
this.nextBox(); this.nextBox();
} }
/**
* Markiert die aktuelle Box als "Gut" und fährt mit der nächsten Box fort.
*/
async markGood(): Promise<void> { async markGood(): Promise<void> {
await this.updateCard('good'); await this.updateCard('good');
this.coverCurrentBox();
this.nextBox(); this.nextBox();
} }
/**
* Markiert die aktuelle Box als "Einfach" und fährt mit der nächsten Box fort.
*/
async markEasy(): Promise<void> { async markEasy(): Promise<void> {
await this.updateCard('easy'); await this.updateCard('easy');
this.coverCurrentBox();
this.nextBox(); this.nextBox();
} }
/**
* Aktualisiert die SRS-Daten der aktuellen Box basierend auf der gegebenen Aktion.
* @param action Die durchgeführte Aktion ('again', 'good', 'easy')
*/
async updateCard(action: 'again' | 'good' | 'easy'): Promise<void> { async updateCard(action: 'again' | 'good' | 'easy'): Promise<void> {
if (this.currentBoxIndex >= this.boxesToReview.length) return; if (this.currentBoxIndex >= this.boxesToReview.length) return;
@ -197,14 +252,14 @@ export class TrainingComponent implements OnInit {
if (newReps === 0) { if (newReps === 0) {
newIvl = 4; // nächste Wiederholung in 4 Tagen newIvl = 4; // nächste Wiederholung in 4 Tagen
} else { } else {
newIvl = newIvl * newFactor * 1.3; // Anki's "easy" kann zu einem leicht erhöhten Intervall führen newIvl = newIvl * newFactor * 1.3; // leicht erhöhtes Intervall
} }
newReps += 1; newReps += 1;
newFactor = newFactor * 1.15; // optional: Anki erhöht den Faktor leicht newFactor = newFactor * 1.15; // optional: Faktor leicht erhöhen
break; break;
} }
// Update due // Aktualisiere das Fälligkeitsdatum
const nextDue = today + Math.floor(newIvl); const nextDue = today + Math.floor(newIvl);
// Aktualisiere das Box-Objekt // Aktualisiere das Box-Objekt
@ -215,32 +270,50 @@ export class TrainingComponent implements OnInit {
box.due = nextDue; box.due = nextDue;
// Sende das Update an das Backend // Sende das Update an das Backend
await this.deckService.updateBox(box).toPromise(); try {
await lastValueFrom(this.deckService.updateBox(box));
} catch (error) {
console.error('Fehler beim Aktualisieren der Box:', error);
alert('Fehler beim Aktualisieren der Box.');
}
} }
nextBox(): void { /**
this.isShowingBox = false; * Deckt die aktuelle Box wieder auf (verdeckt sie erneut).
*/
if (this.boxesToReview.length === 0) { coverCurrentBox(): void {
// Alle Boxen für dieses Bild sind bearbeitet
this.nextImage();
return;
}
if (this.currentBoxIndex >= this.boxesToReview.length - 1) {
this.currentBoxIndex = 0;
} else {
this.currentBoxIndex++;
}
this.boxRevealed[this.currentBoxIndex] = false; this.boxRevealed[this.currentBoxIndex] = false;
this.drawCanvas(); this.drawCanvas();
} }
/**
* Geht zur nächsten Box. Wenn alle Boxen in der aktuellen Runde bearbeitet wurden,
* wird eine neue Runde gestartet.
*/
nextBox(): void {
this.isShowingBox = false;
if (this.currentBoxIndex >= this.boxesToReview.length - 1) {
// Runde abgeschlossen, starte eine neue Runde
this.initializeBoxesToReview();
} else {
// Gehe zur nächsten Box
this.currentBoxIndex++;
this.drawCanvas();
}
}
/**
* Wechselt zum nächsten Bild im Deck.
*/
nextImage(): void { nextImage(): void {
this.currentImageIndex++; this.currentImageIndex++;
this.loadImage(this.currentImageIndex); this.loadImage(this.currentImageIndex);
} }
/**
* Überspringt zum nächsten Bild im Deck.
*/
skipToNextImage(): void { skipToNextImage(): void {
if (this.currentImageIndex < this.deck.images.length - 1) { if (this.currentImageIndex < this.deck.images.length - 1) {
this.currentImageIndex++; this.currentImageIndex++;
@ -250,19 +323,27 @@ export class TrainingComponent implements OnInit {
} }
} }
/**
* Beendet das Training und gibt eine Abschlussmeldung aus.
*/
endTraining(): void { endTraining(): void {
this.isTrainingFinished = true; this.isTrainingFinished = true;
//alert(`Training beendet!\nGewusst: ${this.knownCount}\nNicht gewusst: ${this.unknownCount}`);
alert(`Training beendet!`); alert(`Training beendet!`);
this.close.emit(); this.close.emit();
} }
/**
* Fragt den Benutzer, ob das Training beendet werden soll, und schließt es gegebenenfalls.
*/
closeTraining(): void { closeTraining(): void {
if (confirm('Möchtest du das Training wirklich beenden?')) { if (confirm('Möchtest du das Training wirklich beenden?')) {
this.close.emit(); this.close.emit();
} }
} }
/**
* Gibt den Fortschritt des Trainings an, z.B. "Bild 2 von 5".
*/
get progress(): string { get progress(): string {
return `Bild ${this.currentImageIndex + 1} von ${this.deck.images.length}`; return `Bild ${this.currentImageIndex + 1} von ${this.deck.images.length}`;
} }