Compare commits

...

5 Commits

Author SHA1 Message Date
156d017730 3column design, borders 2025-01-12 16:34:48 +00:00
26088f58c9 isGraduated eingebaut SRS Berechnung umgebaut 2024-12-13 11:59:33 +01:00
05bfd4f3eb Anzeige des nächsten Intervals 2024-12-12 18:26:01 +01:00
d5ac4d6f26 SRS 2. Teil 2024-12-12 17:20:43 +01:00
ac69a11db5 move to SRS Algo 2024-12-11 21:33:52 +01:00
5 changed files with 320 additions and 72 deletions

View File

@@ -8,26 +8,26 @@
</div> </div>
<!-- Decks anzeigen --> <!-- Decks anzeigen -->
<div class="flex flex-wrap gap-6"> <div class="flex flex-wrap">
<div *ngFor="let deck of decks" class="bg-white shadow rounded-lg p-6 w-full md:w-1/2 lg:w-1/3 flex flex-col"> <div *ngFor="let deck of decks" class="bg-white shadow rounded-lg p-6 w-full md:w-1/2 lg:w-1/3 flex flex-col border-dashed border-2 border-indigo-600">
<!-- Deck-Header mit Toggle-Button --> <!-- Deck-Header mit Toggle-Button -->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<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

@@ -4,7 +4,6 @@ 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; // Hinzugefügt
name: string; name: string;
images: DeckImage[]; images: DeckImage[];
} }
@@ -16,10 +15,17 @@ export interface DeckImage {
} }
export interface Box { export interface Box {
id?:number;
x1:number; x1:number;
x2:number; x2:number;
y1:number; y1:number;
y2:number; y2:number;
due?: number;
ivl?: number;
factor?: number;
reps?: number;
lapses?: number;
isGraduated?:boolean;
} }
export interface BackendBox { export interface BackendBox {
@@ -58,7 +64,6 @@ export class DeckService {
getDecks(): Observable<Deck[]> { getDecks(): Observable<Deck[]> {
return this.http.get<any[]>(this.apiUrl).pipe( return this.http.get<any[]>(this.apiUrl).pipe(
map(decks => decks.map(deck => ({ map(decks => decks.map(deck => ({
id: deck.id, // Annahme: Jeder Deck hat eine eindeutige ID
name: deck.name, name: deck.name,
images: this.groupImagesByName(deck.images) images: this.groupImagesByName(deck.images)
}))) })))
@@ -76,10 +81,17 @@ export class DeckService {
}; };
} }
imageMap[image.id].boxes.push({ imageMap[image.id].boxes.push({
id: image.boxid,
x1: image.x1, x1: image.x1,
x2: image.x2, x2: image.x2,
y1: image.y1, y1: image.y1,
y2: image.y2 y2: image.y2,
due: image.due,
ivl:image.ivl,
factor:image.factor,
reps:image.reps,
lapses:image.lapses,
isGraduated:image.isGraduated?true:false
}); });
}); });
@@ -110,4 +122,8 @@ export class DeckService {
moveImage(imageId: string, targetDeckId: number): Observable<any> { moveImage(imageId: string, targetDeckId: number): Observable<any> {
return this.http.post(`${this.apiUrl}/images/${imageId}/move`, { targetDeckId }); return this.http.post(`${this.apiUrl}/images/${imageId}/move`, { targetDeckId });
} }
updateBox(box: Box): Observable<any> {
return this.http.put(`${this.apiUrl}/boxes/${box.id}`, box);
}
} }

View File

@@ -14,22 +14,31 @@
Anzeigen Anzeigen
</button> </button>
<!-- Gewusst Button --> <!-- Nochmal Button -->
<button <button
(click)="markKnown()" (click)="markAgain()"
class="bg-orange-500 disabled:bg-orange-200 text-white py-2 px-4 rounded hover:bg-orange-600"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Nochmal ({{ getNextInterval(currentBox, 'again') }})
</button>
<!-- Gut Button -->
<button
(click)="markGood()"
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"
> >
Gewusst Gut ({{ getNextInterval(currentBox, 'good') }})
</button> </button>
<!-- Nicht gewusst Button --> <!-- Einfach Button -->
<button <button
(click)="markUnknown()" (click)="markEasy()"
class="bg-red-500 disabled:bg-red-200 text-white py-2 px-4 rounded hover:bg-red-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"
> >
Nicht gewusst Einfach ({{ getNextInterval(currentBox, 'easy') }})
</button> </button>
<!-- Nächstes Bild Button --> <!-- Nächstes Bild Button -->
@@ -43,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

@@ -1,10 +1,22 @@
// src/app/training.component.ts // training.component.ts
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',
@@ -19,23 +31,21 @@ export class TrainingComponent implements OnInit {
currentImageIndex: number = 0; currentImageIndex: number = 0;
currentImageData: DeckImage | null = null; currentImageData: DeckImage | null = null;
// Ändere currentBoxIndex zu boxesToReview als Array
currentBoxIndex: number = 0; currentBoxIndex: number = 0;
boxesToReview: Box[] = []; boxesToReview: Box[] = [];
boxRevealed: boolean[] = []; boxRevealed: boolean[] = [];
knownCount: number = 0;
unknownCount: number = 0;
isShowingBox: boolean = false; isShowingBox: boolean = false;
isTrainingFinished: boolean = false; isTrainingFinished: boolean = false;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService) { }
ngOnInit(): void { ngOnInit(): void {
// Initialisierung wurde in ngAfterViewInit durchgeführt
} }
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 {
@@ -44,6 +54,10 @@ export class TrainingComponent implements OnInit {
} }
} }
/**
* 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();
@@ -51,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, gemischt // Initialisiere die Boxen für die aktuelle Runde
this.boxesToReview = this.shuffleArray([...this.currentImageData.boxes]); 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');
@@ -66,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 = () => {
@@ -99,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]];
} }
@@ -118,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];
@@ -125,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;
@@ -132,44 +202,190 @@ export class TrainingComponent implements OnInit {
this.drawCanvas(); this.drawCanvas();
} }
markKnown(): void { /**
this.knownCount++; * Markiert die aktuelle Box als "Nochmal" und fährt mit der nächsten Box fort.
// Entferne die aktuelle Box aus boxesToReview, da sie bekannt ist */
this.boxesToReview.splice(this.currentBoxIndex, 1); async markAgain(): Promise<void> {
this.boxRevealed.splice(this.currentBoxIndex, 1); await this.updateCard('again');
this.coverCurrentBox();
this.nextBox(); this.nextBox();
} }
markUnknown(): void { /**
this.unknownCount++; * Markiert die aktuelle Box als "Gut" und fährt mit der nächsten Box fort.
// Behalte die aktuelle Box in der Liste und verschiebe sie an eine zufällige Position am Ende */
const box = this.boxesToReview.splice(this.currentBoxIndex, 1)[0]; async markGood(): Promise<void> {
this.boxesToReview.push(box); await this.updateCard('good');
this.boxRevealed.splice(this.currentBoxIndex, 1); this.coverCurrentBox();
this.nextBox(); this.nextBox();
} }
nextBox(): void { /**
this.isShowingBox = false; * Markiert die aktuelle Box als "Einfach" und fährt mit der nächsten Box fort.
*/
if (this.boxesToReview.length === 0) { async markEasy(): Promise<void> {
// Alle Boxen für dieses Bild sind bearbeitet await this.updateCard('easy');
this.nextImage(); this.coverCurrentBox();
return; this.nextBox();
} }
if (this.currentBoxIndex >= this.boxesToReview.length) { /**
this.currentBoxIndex = 0; * 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> {
if (this.currentBoxIndex >= this.boxesToReview.length) return;
const box = this.boxesToReview[this.currentBoxIndex];
const today = this.getTodayInDays();
// Berechne das neue Intervall und eventuell den neuen Faktor
const { newIvl, newFactor, newReps, newLapses, newIsGraduated } = this.calculateNewInterval(box, action);
// Aktualisiere das Fälligkeitsdatum
const nextDue = today + Math.floor(newIvl/1440);
// Aktualisiere das Box-Objekt
box.ivl = newIvl;
box.factor = newFactor;
box.reps = newReps;
box.lapses = newLapses;
box.due = nextDue;
box.isGraduated = newIsGraduated
// Sende das Update an das Backend
try {
await lastValueFrom(this.deckService.updateBox(box));
} catch (error) {
console.error('Fehler beim Aktualisieren der Box:', error);
alert('Fehler beim Aktualisieren der Box.');
}
} }
/**
* 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 (action === 'again') {
newLapses++;
newReps = 0;
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 (action === 'easy') {
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 {
// 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.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++;
@@ -179,20 +395,28 @@ 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!`);
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}`;
} }
} }