Compare commits

..

No commits in common. "26088f58c9b130f3ac9ac7c4a5e91a5231a03bf6" and "ac69a11db5e5c6cd80dcd831a0c1d9a3294fe5d0" have entirely different histories.

5 changed files with 105 additions and 259 deletions

View File

@ -16,18 +16,18 @@
<h2 class="text-xl font-semibold">{{ deck.name }}</h2>
<span class="text-gray-600">({{ deck.images.length }} Bilder)</span>
</div>
<button (click)="toggleDeckExpansion(deck.name)" class="text-gray-500 hover:text-gray-700 focus:outline-none" title="Deck ein-/ausklappen">
<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">
<button (click)="toggleDeckExpansion(deck.id)" 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<!-- Bildliste und Action-Buttons nur anzeigen, wenn das Deck erweitert ist und kein Training aktiv ist -->
<ng-container *ngIf="isDeckExpanded(deck.name) && !selectedDeck">
<ng-container *ngIf="isDeckExpanded(deck.id) && !selectedDeck">
<!-- Liste der Bilder mit Anzahl der Boxen und Icons -->
<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">

View File

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

View File

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

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"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Nochmal ({{ getNextInterval(currentBox, 'again') }})
Nochmal
</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"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Gut ({{ getNextInterval(currentBox, 'good') }})
Gut
</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"
[disabled]="!isShowingBox || currentBoxIndex >= boxesToReview.length"
>
Einfach ({{ getNextInterval(currentBox, 'easy') }})
Einfach
</button>
<!-- Nächstes Bild Button -->
@ -52,6 +52,7 @@
</div>
<p class="mt-2">{{ progress }}</p>
<!-- <p class="mt-2">Gewusst: {{ knownCount }} | Nicht gewusst: {{ unknownCount }}</p> -->
<button
(click)="closeTraining()"

View File

@ -2,21 +2,9 @@
import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Deck, DeckImage, DeckService, Box } from '../deck.service';
import { CommonModule } from '@angular/common';
import { lastValueFrom } from 'rxjs';
import { switchMap } from 'rxjs/operators';
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({
selector: 'app-training',
templateUrl: './training.component.html',
@ -41,11 +29,11 @@ export class TrainingComponent implements OnInit {
constructor(private deckService: DeckService) { }
ngOnInit(): void {
// Initialisierung wurde in ngAfterViewInit durchgeführt
// Initialisiere die boxesToReview basierend auf SRS
this.initializeBoxesToReview();
}
ngAfterViewInit(){
// Initialisiere das erste Bild und die dazugehörigen Boxen
if (this.deck && this.deck.images.length > 0) {
this.loadImage(this.currentImageIndex);
} else {
@ -54,10 +42,27 @@ 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
*/
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));
}
loadImage(imageIndex: number): void {
if (imageIndex >= this.deck.images.length) {
this.endTraining();
@ -65,57 +70,13 @@ export class TrainingComponent implements OnInit {
}
this.currentImageData = this.deck.images[imageIndex];
// Initialisiere die Boxen für die aktuelle Runde
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
// Initialisiere boxesToReview mit allen Boxen, die fällig sind, gemischt
this.boxesToReview = this.shuffleArray([...this.currentImageData.boxes].filter(box => box.due! <= this.getTodayInDays()));
this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
// Setze den aktuellen Box-Index zurück
this.currentBoxIndex = 0;
this.isShowingBox = false;
// Zeichne das Canvas neu
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 {
const canvas = this.canvasRef.nativeElement;
const ctx = canvas.getContext('2d');
@ -124,33 +85,30 @@ export class TrainingComponent implements OnInit {
const img = new Image();
img.src = `/api/debug_image/${this.currentImageData.id}/original_compressed.jpg`;
img.onload = () => {
// Setze die Größe des Canvas auf die Größe des Bildes
// Set canvas size to image size
canvas.width = img.width;
canvas.height = img.height;
// Zeichne das Bild
// Draw image
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// Zeichne die Boxen
// Draw boxes
this.boxesToReview.forEach((box, index) => {
if (this.boxRevealed[index]) {
// Box ist bereits enthüllt, nichts zeichnen
return;
}
ctx.beginPath();
ctx.rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1);
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.fillStyle = index === this.currentBoxIndex ? 'rgba(0, 255, 0, 0.99)' : 'rgba(255, 0, 0, 0.99)';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = 'black';
ctx.stroke();
});
};
img.onerror = () => {
@ -160,21 +118,18 @@ export class TrainingComponent implements OnInit {
};
}
/**
* Mischt ein Array zufällig.
* @param array Das zu mischende Array
* @returns Das gemischte Array
*/
// Utility-Funktion zum Mischen eines Arrays
shuffleArray<T>(array: T[]): T[] {
let currentIndex = array.length, randomIndex;
// Solange noch Elemente zum Mischen vorhanden sind
// While there remain elements to shuffle.
while (currentIndex !== 0) {
// Wähle ein verbleibendes Element
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// Tausche es mit dem aktuellen Element
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
@ -182,9 +137,6 @@ export class TrainingComponent implements OnInit {
return array;
}
/**
* Gibt die aktuelle Box zurück, die überprüft wird.
*/
get currentBox(): Box | null {
if (this.currentBoxIndex < this.boxesToReview.length) {
return this.boxesToReview[this.currentBoxIndex];
@ -192,9 +144,6 @@ export class TrainingComponent implements OnInit {
return null;
}
/**
* Zeigt den Inhalt der aktuellen Box an.
*/
showText(): void {
if (this.currentBoxIndex >= this.boxesToReview.length) return;
this.boxRevealed[this.currentBoxIndex] = true;
@ -202,48 +151,61 @@ export class TrainingComponent implements OnInit {
this.drawCanvas();
}
/**
* Markiert die aktuelle Box als "Nochmal" und fährt mit der nächsten Box fort.
*/
// Neue Methoden für Anki-Optionen
async markAgain(): Promise<void> {
await this.updateCard('again');
this.coverCurrentBox();
this.nextBox();
}
/**
* Markiert die aktuelle Box als "Gut" und fährt mit der nächsten Box fort.
*/
async markGood(): Promise<void> {
await this.updateCard('good');
this.coverCurrentBox();
this.nextBox();
}
/**
* Markiert die aktuelle Box als "Einfach" und fährt mit der nächsten Box fort.
*/
async markEasy(): Promise<void> {
await this.updateCard('easy');
this.coverCurrentBox();
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> {
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);
let newIvl = box.ivl || 0;
let newFactor = box.factor || 2.5;
let newReps = box.reps || 0;
let newLapses = box.lapses || 0;
// Aktualisiere das Fälligkeitsdatum
const nextDue = today + Math.floor(newIvl/1440);
switch(action) {
case 'again':
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
box.ivl = newIvl;
@ -251,141 +213,34 @@ export class TrainingComponent implements OnInit {
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.');
}
await this.deckService.updateBox(box).toPromise();
}
/**
* 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
nextBox(): void {
this.isShowingBox = 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 (this.boxesToReview.length === 0) {
// Alle Boxen für dieses Bild sind bearbeitet
this.nextImage();
return;
}
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;
}
if (this.currentBoxIndex >= this.boxesToReview.length - 1) {
this.currentBoxIndex = 0;
} else {
// Card is in review phase, apply space repetition
newIvl = Math.round(newIvl * newFactor);
this.currentBoxIndex++;
}
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();
}
/**
* 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 {
this.currentImageIndex++;
this.loadImage(this.currentImageIndex);
}
/**
* Überspringt zum nächsten Bild im Deck.
*/
skipToNextImage(): void {
if (this.currentImageIndex < this.deck.images.length - 1) {
this.currentImageIndex++;
@ -395,27 +250,19 @@ export class TrainingComponent implements OnInit {
}
}
/**
* Beendet das Training und gibt eine Abschlussmeldung aus.
*/
endTraining(): void {
this.isTrainingFinished = true;
//alert(`Training beendet!\nGewusst: ${this.knownCount}\nNicht gewusst: ${this.unknownCount}`);
alert(`Training beendet!`);
this.close.emit();
}
/**
* Fragt den Benutzer, ob das Training beendet werden soll, und schließt es gegebenenfalls.
*/
closeTraining(): void {
if (confirm('Möchtest du das Training wirklich beenden?')) {
this.close.emit();
}
}
/**
* Gibt den Fortschritt des Trainings an, z.B. "Bild 2 von 5".
*/
get progress(): string {
return `Bild ${this.currentImageIndex + 1} von ${this.deck.images.length}`;
}