Compare commits

...

3 Commits

Author SHA1 Message Date
aknuth 26088f58c9 isGraduated eingebaut SRS Berechnung umgebaut 2024-12-13 11:59:33 +01:00
aknuth 05bfd4f3eb Anzeige des nächsten Intervals 2024-12-12 18:26:01 +01:00
aknuth d5ac4d6f26 SRS 2. Teil 2024-12-12 17:20:43 +01:00
5 changed files with 259 additions and 105 deletions

View File

@ -16,18 +16,18 @@
<h2 class="text-xl font-semibold">{{ deck.name }}</h2> <h2 class="text-xl font-semibold">{{ deck.name }}</h2>
<span class="text-gray-600">({{ deck.images.length }} Bilder)</span> <span class="text-gray-600">({{ deck.images.length }} Bilder)</span>
</div> </div>
<button (click)="toggleDeckExpansion(deck.id)" class="text-gray-500 hover:text-gray-700 focus:outline-none" title="Deck ein-/ausklappen"> <button (click)="toggleDeckExpansion(deck.name)" class="text-gray-500 hover:text-gray-700 focus:outline-none" title="Deck ein-/ausklappen">
<svg *ngIf="!isDeckExpanded(deck.id)" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transform rotate-0 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg *ngIf="!isDeckExpanded(deck.name)" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transform rotate-0 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
<svg *ngIf="isDeckExpanded(deck.id)" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transform rotate-180 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg *ngIf="isDeckExpanded(deck.name)" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 transform rotate-180 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
</div> </div>
<!-- Bildliste und Action-Buttons nur anzeigen, wenn das Deck erweitert ist und kein Training aktiv ist --> <!-- Bildliste und Action-Buttons nur anzeigen, wenn das Deck erweitert ist und kein Training aktiv ist -->
<ng-container *ngIf="isDeckExpanded(deck.id) && !selectedDeck"> <ng-container *ngIf="isDeckExpanded(deck.name) && !selectedDeck">
<!-- Liste der Bilder mit Anzahl der Boxen und Icons --> <!-- Liste der Bilder mit Anzahl der Boxen und Icons -->
<ul class="mb-4"> <ul class="mb-4">
<li *ngFor="let image of deck.images" class="flex justify-between items-center py-2 border-b last:border-b-0"> <li *ngFor="let image of deck.images" class="flex justify-between items-center py-2 border-b last:border-b-0">

View File

@ -37,7 +37,7 @@ export class DeckListComponent implements OnInit {
currentUploadDeckName: string = ''; currentUploadDeckName: string = '';
// Set zur Verfolgung erweiterter Decks // Set zur Verfolgung erweiterter Decks
expandedDecks: Set<number> = new Set<number>(); expandedDecks: Set<string> = new Set<string>();
// State für das Verschieben von Bildern // State für das Verschieben von Bildern
imageToMove: { image: DeckImage, sourceDeck: Deck } | null = null; imageToMove: { image: DeckImage, sourceDeck: Deck } | null = null;
@ -122,18 +122,18 @@ export class DeckListComponent implements OnInit {
} }
// Methode zum Umschalten der Deck-Erweiterung // Methode zum Umschalten der Deck-Erweiterung
toggleDeckExpansion(deckId: number): void { toggleDeckExpansion(deckName: string): void {
if (this.expandedDecks.has(deckId)) { if (this.expandedDecks.has(deckName)) {
this.expandedDecks.delete(deckId); this.expandedDecks.delete(deckName);
} else { } else {
this.expandedDecks.add(deckId); this.expandedDecks.add(deckName);
} }
this.saveExpandedDecks(); this.saveExpandedDecks();
} }
// Methode zur Überprüfung, ob ein Deck erweitert ist // Methode zur Überprüfung, ob ein Deck erweitert ist
isDeckExpanded(deckId: number): boolean { isDeckExpanded(deckName: string): boolean {
return this.expandedDecks.has(deckId); return this.expandedDecks.has(deckName);
} }
// Laden der erweiterten Decks aus dem sessionStorage // Laden der erweiterten Decks aus dem sessionStorage
@ -141,14 +141,14 @@ export class DeckListComponent implements OnInit {
const stored = sessionStorage.getItem('expandedDecks'); const stored = sessionStorage.getItem('expandedDecks');
if (stored) { if (stored) {
try { try {
const parsed: number[] = JSON.parse(stored); const parsed: string[] = JSON.parse(stored);
this.expandedDecks = new Set<number>(parsed); this.expandedDecks = new Set<string>(parsed);
} catch (e) { } catch (e) {
console.error('Fehler beim Parsen der erweiterten Decks aus sessionStorage', e); console.error('Fehler beim Parsen der erweiterten Decks aus sessionStorage', e);
} }
} else { } else {
// Wenn keine Daten gespeichert sind, alle Decks standardmäßig nicht erweitern // Wenn keine Daten gespeichert sind, alle Decks standardmäßig nicht erweitern
this.expandedDecks = new Set<number>(); this.expandedDecks = new Set<string>();
} }
} }

View File

@ -25,6 +25,7 @@ export interface Box {
factor?: number; factor?: number;
reps?: number; reps?: number;
lapses?: number; lapses?: number;
isGraduated?:boolean;
} }
export interface BackendBox { export interface BackendBox {
@ -89,7 +90,8 @@ export class DeckService {
ivl:image.ivl, ivl:image.ivl,
factor:image.factor, factor:image.factor,
reps:image.reps, reps:image.reps,
lapses:image.lapses lapses:image.lapses,
isGraduated:image.isGraduated?true:false
}); });
}); });

View File

@ -20,7 +20,7 @@
class="bg-orange-500 disabled:bg-orange-200 text-white py-2 px-4 rounded hover:bg-orange-600" class="bg-orange-500 disabled:bg-orange-200 text-white py-2 px-4 rounded hover:bg-orange-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length" [disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
> >
Nochmal Nochmal ({{ getNextInterval(currentBox, 'again') }})
</button> </button>
<!-- Gut Button --> <!-- Gut Button -->
@ -29,7 +29,7 @@
class="bg-blue-500 disabled:bg-blue-200 text-white py-2 px-4 rounded hover:bg-blue-600" class="bg-blue-500 disabled:bg-blue-200 text-white py-2 px-4 rounded hover:bg-blue-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length" [disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
> >
Gut Gut ({{ getNextInterval(currentBox, 'good') }})
</button> </button>
<!-- Einfach Button --> <!-- Einfach Button -->
@ -38,7 +38,7 @@
class="bg-green-500 disabled:bg-green-200 text-white py-2 px-4 rounded hover:bg-green-600" class="bg-green-500 disabled:bg-green-200 text-white py-2 px-4 rounded hover:bg-green-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length" [disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
> >
Einfach Einfach ({{ getNextInterval(currentBox, 'easy') }})
</button> </button>
<!-- Nächstes Bild Button --> <!-- Nächstes Bild Button -->
@ -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

@ -2,9 +2,21 @@
import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Deck, DeckImage, DeckService, Box } from '../deck.service'; import { Deck, DeckImage, DeckService, Box } from '../deck.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { switchMap } from 'rxjs/operators'; import { lastValueFrom } from 'rxjs';
import { forkJoin } from 'rxjs';
const LEARNING_STEPS = {
AGAIN: 1, // 1 minute
GOOD: 10, // 10 minutes
GRADUATION: 1440 // 1 day (in minutes)
};
const FACTOR_CHANGES = {
AGAIN: 0.85, // Reduce factor by 15%
EASY: 1.15, // Increase factor by 15%
MIN: 1.3, // Minimum factor allowed
MAX: 2.9 // Maximum factor allowed
};
const EASY_INTERVAL = 4 * 1440; // 4 days in minutes
@Component({ @Component({
selector: 'app-training', selector: 'app-training',
templateUrl: './training.component.html', templateUrl: './training.component.html',
@ -29,11 +41,11 @@ export class TrainingComponent implements OnInit {
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService) { }
ngOnInit(): void { ngOnInit(): void {
// Initialisiere die boxesToReview basierend auf SRS // Initialisierung wurde in ngAfterViewInit durchgeführt
this.initializeBoxesToReview();
} }
ngAfterViewInit(){ ngAfterViewInit(){
// Initialisiere das erste Bild und die dazugehörigen Boxen
if (this.deck && this.deck.images.length > 0) { if (this.deck && this.deck.images.length > 0) {
this.loadImage(this.currentImageIndex); this.loadImage(this.currentImageIndex);
} else { } else {
@ -42,27 +54,10 @@ export class TrainingComponent implements OnInit {
} }
} }
initializeBoxesToReview(): void { /**
// Filtere alle Boxen, die fällig sind (due <= heute) * Lädt das Bild basierend auf dem gegebenen Index und initialisiert die zu überprüfenden Boxen.
const today = this.getTodayInDays(); * @param imageIndex Index des zu ladenden Bildes im Deck
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));
}
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 +65,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 +124,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 || (box.due && box.due-this.getTodayInDays()>0)) {
// Box ist aktuell enthüllt, keine Überlagerung
return;
} else if (this.currentBoxIndex === index && !this.isShowingBox) {
// Box ist enthüllt
ctx.fillStyle = 'rgba(0, 255, 0, 1)'; // Undurchsichtige grüne Überlagerung
} else {
// Box ist verdeckt
ctx.fillStyle = 'rgba(255, 0, 0, 1)'; // Undurchsichtige 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 +160,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 +182,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 +192,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,61 +202,48 @@ 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;
const box = this.boxesToReview[this.currentBoxIndex]; const box = this.boxesToReview[this.currentBoxIndex];
const today = this.getTodayInDays(); const today = this.getTodayInDays();
let newIvl = box.ivl || 0; // Berechne das neue Intervall und eventuell den neuen Faktor
let newFactor = box.factor || 2.5; const { newIvl, newFactor, newReps, newLapses, newIsGraduated } = this.calculateNewInterval(box, action);
let newReps = box.reps || 0;
let newLapses = box.lapses || 0;
switch(action) { // Aktualisiere das Fälligkeitsdatum
case 'again': const nextDue = today + Math.floor(newIvl/1440);
newIvl = 1 / 1440; // weniger als ein Tag, z.B., 1 Minute in Tagen
newReps = 0;
newLapses += 1;
break;
case 'good':
if (newReps === 0) {
newIvl = 1; // nächste Wiederholung am nächsten Tag
} else {
newIvl = newIvl * newFactor;
}
newReps += 1;
break;
case 'easy':
if (newReps === 0) {
newIvl = 4; // nächste Wiederholung in 4 Tagen
} else {
newIvl = newIvl * newFactor * 1.3; // Anki's "easy" kann zu einem leicht erhöhten Intervall führen
}
newReps += 1;
newFactor = newFactor * 1.15; // optional: Anki erhöht den Faktor leicht
break;
}
// Update due
const nextDue = today + Math.floor(newIvl);
// Aktualisiere das Box-Objekt // Aktualisiere das Box-Objekt
box.ivl = newIvl; box.ivl = newIvl;
@ -213,34 +251,141 @@ export class TrainingComponent implements OnInit {
box.reps = newReps; box.reps = newReps;
box.lapses = newLapses; box.lapses = newLapses;
box.due = nextDue; box.due = nextDue;
box.isGraduated = newIsGraduated
// 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; * Berechnet das neue Intervall, den Faktor, die Wiederholungen und die Lapses basierend auf der Aktion.
* @param box Die aktuelle Box
* @param action Die Aktion ('again', 'good', 'easy')
* @returns Ein Objekt mit den neuen Werten für ivl, factor, reps und lapses
*/
calculateNewInterval(box: Box, action: 'again' | 'good' | 'easy'): { newIvl: number, newFactor: number, newReps: number, newLapses: number, newIsGraduated: boolean } {
let newIvl = box.ivl || 0;
let newFactor = box.factor || 2.5;
let newReps = box.reps || 0;
let newLapses = box.lapses || 0;
let newIsGraduated = box.isGraduated || false
if (this.boxesToReview.length === 0) { if (action === 'again') {
// Alle Boxen für dieses Bild sind bearbeitet newLapses++;
this.nextImage(); newReps = 0;
return; newIvl = LEARNING_STEPS.AGAIN;
newIsGraduated = false;
// Reduce factor but not below minimum
newFactor = Math.max(
FACTOR_CHANGES.MIN,
newFactor * FACTOR_CHANGES.AGAIN
);
return { newIvl, newFactor, newReps, newLapses, newIsGraduated };
} }
if (this.currentBoxIndex >= this.boxesToReview.length - 1) { if (action === 'easy') {
this.currentBoxIndex = 0; newReps++;
newIsGraduated = true;
newIvl = EASY_INTERVAL;
// Increase factor but not above maximum
newFactor = Math.min(
FACTOR_CHANGES.MAX,
newFactor * FACTOR_CHANGES.EASY
);
return { newIvl, newFactor, newReps, newLapses, newIsGraduated };;
}
// Handle 'good' action
newReps++;
if (!newIsGraduated) {
// Card is in learning phase
if (newReps === 1) {
newIvl = LEARNING_STEPS.GOOD;
} else {
// Graduate the card
newIsGraduated = true;
newIvl = LEARNING_STEPS.GRADUATION;
}
} else { } else {
this.currentBoxIndex++; // Card is in review phase, apply space repetition
newIvl = Math.round(newIvl * newFactor);
} }
return { newIvl, newFactor, newReps, newLapses, newIsGraduated };;
}
/**
* Berechnet das nächste Intervall basierend auf der Aktion und gibt es als String zurück.
* @param box Die aktuelle Box
* @param action Die Aktion ('again', 'good', 'easy')
* @returns Das nächste Intervall als String (z.B. "10 min", "2 d")
*/
getNextInterval(box: Box | null, action: 'again' | 'good' | 'easy'): string {
if (!box)
return '';
const { newIvl } = this.calculateNewInterval(box, action);
return this.formatInterval(newIvl);
}
/**
* Formatiert das Intervall als String, entweder in Minuten oder Tagen.
* @param ivl Das Intervall in Tagen
* @returns Das formatierte Intervall als String
*/
formatInterval(minutes: number): string {
if (minutes < 60) return `${minutes}min`;
if (minutes < 1440) return `${Math.round(minutes/60)}h`;
return `${Math.round(minutes/1440)}d`;
}
/**
* Deckt die aktuelle Box wieder auf (verdeckt sie erneut).
*/
coverCurrentBox(): void {
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 +395,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}`;
} }