@@ -57,7 +56,7 @@
(click)="closeTraining()"
class="mt-4 text-gray-500 hover:text-gray-700 underline"
>
- Training beenden
+ End Training
-
+
\ No newline at end of file
diff --git a/src/app/training/training.component.ts b/src/app/training/training.component.ts
index 341c3c6..8d45c80 100644
--- a/src/app/training/training.component.ts
+++ b/src/app/training/training.component.ts
@@ -1,4 +1,3 @@
-// training.component.ts
import { Component, Input, Output, EventEmitter, OnInit, ViewChild, ElementRef } from '@angular/core';
import { Deck, DeckImage, DeckService, Box } from '../deck.service';
import { CommonModule } from '@angular/common';
@@ -27,7 +26,7 @@ export class TrainingComponent implements OnInit {
@Input() deck!: Deck;
@Output() close = new EventEmitter();
- @ViewChild('canvas',{static : false}) canvasRef!: ElementRef;
+ @ViewChild('canvas', { static: false }) canvasRef!: ElementRef;
currentImageIndex: number = 0;
currentImageData: DeckImage | null = null;
@@ -41,22 +40,22 @@ export class TrainingComponent implements OnInit {
constructor(private deckService: DeckService) { }
ngOnInit(): void {
- // Initialisierung wurde in ngAfterViewInit durchgeführt
+ // Initialization was done in ngAfterViewInit
}
- ngAfterViewInit(){
- // Initialisiere das erste Bild und die dazugehörigen Boxen
+ ngAfterViewInit() {
+ // Initialize the first image and its boxes
if (this.deck && this.deck.images.length > 0) {
this.loadImage(this.currentImageIndex);
} else {
- alert('Kein Deck oder keine Bilder vorhanden.');
+ alert('No deck or images available.');
this.close.emit();
}
}
/**
- * 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
+ * Loads the image based on the given index and initializes the boxes to review.
+ * @param imageIndex Index of the image to load in the deck
*/
loadImage(imageIndex: number): void {
if (imageIndex >= this.deck.images.length) {
@@ -65,13 +64,13 @@ export class TrainingComponent implements OnInit {
}
this.currentImageData = this.deck.images[imageIndex];
- // Initialisiere die Boxen für die aktuelle Runde
+ // Initialize the boxes for the current round
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.
+ * Determines all due boxes for the current round, shuffles them, and resets the current box index.
+ * If no boxes are left to review, it moves to the next image.
*/
initializeBoxesToReview(): void {
if (!this.currentImageData) {
@@ -79,42 +78,42 @@ export class TrainingComponent implements OnInit {
return;
}
- // Filtere alle Boxen, die fällig sind (due <= heute)
+ // Filter all boxes that are due (due <= today)
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
+ // No more boxes for this image, move to the next image
this.nextImage();
return;
}
- // Mische die Boxen zufällig
+ // Shuffle the boxes randomly
this.boxesToReview = this.shuffleArray(this.boxesToReview);
- // Initialisiere den Array zur Verfolgung der enthüllten Boxen
+ // Initialize the array to track revealed boxes
this.boxRevealed = new Array(this.boxesToReview.length).fill(false);
- // Setze den aktuellen Box-Index zurück
+ // Reset the current box index
this.currentBoxIndex = 0;
this.isShowingBox = false;
- // Zeichne das Canvas neu
+ // Redraw the canvas
this.drawCanvas();
}
/**
- * Gibt das heutige Datum in Tagen seit der UNIX-Epoche zurück.
+ * Returns today's date in days since the UNIX epoch.
*/
getTodayInDays(): number {
- const epoch = new Date(1970, 0, 1); // Anki verwendet UNIX-Epoche
+ const epoch = new Date(1970, 0, 1); // Anki uses UNIX epoch
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.
+ * Draws the current image and boxes on the canvas.
+ * Boxes are displayed in red if hidden and green if revealed.
*/
drawCanvas(): void {
const canvas = this.canvasRef.nativeElement;
@@ -124,27 +123,27 @@ 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 the canvas size to the image size
canvas.width = img.width;
canvas.height = img.height;
- // Zeichne das Bild
+ // Draw the image
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
- // Zeichne die Boxen
+ // Draw the boxes
this.boxesToReview.forEach((box, index) => {
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
+ if (this.currentBoxIndex === index && this.isShowingBox || (box.due && box.due - this.getTodayInDays() > 0)) {
+ // Box is currently revealed, no overlay
return;
} else if (this.currentBoxIndex === index && !this.isShowingBox) {
- // Box ist enthüllt
- ctx.fillStyle = 'rgba(0, 255, 0, 1)'; // Undurchsichtige grüne Überlagerung
+ // Box is revealed
+ ctx.fillStyle = 'rgba(0, 255, 0, 1)'; // Opaque green overlay
} else {
- // Box ist verdeckt
- ctx.fillStyle = 'rgba(255, 0, 0, 1)'; // Undurchsichtige rote Überlagerung
+ // Box is hidden
+ ctx.fillStyle = 'rgba(255, 0, 0, 1)'; // Opaque red overlay
}
ctx.fill();
ctx.lineWidth = 2;
@@ -154,27 +153,27 @@ export class TrainingComponent implements OnInit {
};
img.onerror = () => {
- console.error('Fehler beim Laden des Bildes für Canvas.');
- alert('Fehler beim Laden des Bildes für Canvas.');
+ console.error('Error loading image for canvas.');
+ alert('Error loading image for canvas.');
this.close.emit();
};
}
/**
- * Mischt ein Array zufällig.
- * @param array Das zu mischende Array
- * @returns Das gemischte Array
+ * Shuffles an array randomly.
+ * @param array The array to shuffle
+ * @returns The shuffled array
*/
shuffleArray(array: T[]): T[] {
let currentIndex = array.length, randomIndex;
- // Solange noch Elemente zum Mischen vorhanden sind
+ // While there are 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
+ // Swap it with the current element
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
@@ -183,7 +182,7 @@ export class TrainingComponent implements OnInit {
}
/**
- * Gibt die aktuelle Box zurück, die überprüft wird.
+ * Returns the current box being reviewed.
*/
get currentBox(): Box | null {
if (this.currentBoxIndex < this.boxesToReview.length) {
@@ -193,7 +192,7 @@ export class TrainingComponent implements OnInit {
}
/**
- * Zeigt den Inhalt der aktuellen Box an.
+ * Reveals the content of the current box.
*/
showText(): void {
if (this.currentBoxIndex >= this.boxesToReview.length) return;
@@ -203,7 +202,7 @@ export class TrainingComponent implements OnInit {
}
/**
- * Markiert die aktuelle Box als "Nochmal" und fährt mit der nächsten Box fort.
+ * Marks the current box as "Again" and proceeds to the next box.
*/
async markAgain(): Promise {
await this.updateCard('again');
@@ -212,7 +211,7 @@ export class TrainingComponent implements OnInit {
}
/**
- * Markiert die aktuelle Box als "Gut" und fährt mit der nächsten Box fort.
+ * Marks the current box as "Good" and proceeds to the next box.
*/
async markGood(): Promise {
await this.updateCard('good');
@@ -221,7 +220,7 @@ export class TrainingComponent implements OnInit {
}
/**
- * Markiert die aktuelle Box als "Einfach" und fährt mit der nächsten Box fort.
+ * Marks the current box as "Easy" and proceeds to the next box.
*/
async markEasy(): Promise {
await this.updateCard('easy');
@@ -230,8 +229,8 @@ export class TrainingComponent implements OnInit {
}
/**
- * Aktualisiert die SRS-Daten der aktuellen Box basierend auf der gegebenen Aktion.
- * @param action Die durchgeführte Aktion ('again', 'good', 'easy')
+ * Updates the SRS data of the current box based on the given action.
+ * @param action The action performed ('again', 'good', 'easy')
*/
async updateCard(action: 'again' | 'good' | 'easy'): Promise {
if (this.currentBoxIndex >= this.boxesToReview.length) return;
@@ -239,96 +238,96 @@ export class TrainingComponent implements OnInit {
const box = this.boxesToReview[this.currentBoxIndex];
const today = this.getTodayInDays();
- // Berechne das neue Intervall und eventuell den neuen Faktor
+ // Calculate the new interval and possibly the new factor
const { newIvl, newFactor, newReps, newLapses, newIsGraduated } = this.calculateNewInterval(box, action);
- // Aktualisiere das Fälligkeitsdatum
- const nextDue = today + Math.floor(newIvl/1440);
+ // Update the due date
+ const nextDue = today + Math.floor(newIvl / 1440);
- // Aktualisiere das Box-Objekt
+ // Update the box object
box.ivl = newIvl;
box.factor = newFactor;
box.reps = newReps;
box.lapses = newLapses;
box.due = nextDue;
- box.isGraduated = newIsGraduated
+ box.isGraduated = newIsGraduated;
- // Sende das Update an das Backend
+ // Send the update to the backend
try {
await lastValueFrom(this.deckService.updateBox(box));
} catch (error) {
- console.error('Fehler beim Aktualisieren der Box:', error);
- alert('Fehler beim Aktualisieren der Box.');
+ console.error('Error updating box:', error);
+ alert('Error updating 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
+ * Calculates the new interval, factor, repetitions, and lapses based on the action.
+ * @param box The current box
+ * @param action The action ('again', 'good', 'easy')
+ * @returns An object with the new values for ivl, factor, reps, and 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
+ 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 === '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 };;
+ 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;
- }
+ // 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);
+ // Card is in review phase, apply space repetition
+ newIvl = Math.round(newIvl * newFactor);
}
- return { newIvl, newFactor, newReps, newLapses, newIsGraduated };;
+ 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")
+ * Calculates the next interval based on the action and returns it as a string.
+ * @param box The current box
+ * @param action The action ('again', 'good', 'easy')
+ * @returns The next interval as a string (e.g., "10 min", "2 d")
*/
getNextInterval(box: Box | null, action: 'again' | 'good' | 'easy'): string {
if (!box)
@@ -340,18 +339,18 @@ export class TrainingComponent implements OnInit {
}
/**
- * Formatiert das Intervall als String, entweder in Minuten oder Tagen.
- * @param ivl Das Intervall in Tagen
- * @returns Das formatierte Intervall als String
+ * Formats the interval as a string, either in minutes or days.
+ * @param ivl The interval in days
+ * @returns The formatted interval as a 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`;
+ if (minutes < 1440) return `${Math.round(minutes / 60)}h`;
+ return `${Math.round(minutes / 1440)}d`;
}
/**
- * Deckt die aktuelle Box wieder auf (verdeckt sie erneut).
+ * Covers the current box again (hides it).
*/
coverCurrentBox(): void {
this.boxRevealed[this.currentBoxIndex] = false;
@@ -359,24 +358,24 @@ export class TrainingComponent implements OnInit {
}
/**
- * Geht zur nächsten Box. Wenn alle Boxen in der aktuellen Runde bearbeitet wurden,
- * wird eine neue Runde gestartet.
+ * Moves to the next box. If all boxes in the current round have been processed,
+ * a new round is started.
*/
nextBox(): void {
this.isShowingBox = false;
if (this.currentBoxIndex >= this.boxesToReview.length - 1) {
- // Runde abgeschlossen, starte eine neue Runde
+ // Round completed, start a new round
this.initializeBoxesToReview();
} else {
- // Gehe zur nächsten Box
+ // Move to the next box
this.currentBoxIndex++;
this.drawCanvas();
}
}
/**
- * Wechselt zum nächsten Bild im Deck.
+ * Moves to the next image in the deck.
*/
nextImage(): void {
this.currentImageIndex++;
@@ -384,39 +383,39 @@ export class TrainingComponent implements OnInit {
}
/**
- * Überspringt zum nächsten Bild im Deck.
+ * Skips to the next image in the deck.
*/
skipToNextImage(): void {
if (this.currentImageIndex < this.deck.images.length - 1) {
this.currentImageIndex++;
this.loadImage(this.currentImageIndex);
} else {
- alert('Dies ist das letzte Bild im Deck.');
+ alert('This is the last image in the deck.');
}
}
/**
- * Beendet das Training und gibt eine Abschlussmeldung aus.
+ * Ends the training and displays a completion message.
*/
endTraining(): void {
this.isTrainingFinished = true;
- alert(`Training beendet!`);
+ alert(`Training completed!`);
this.close.emit();
}
/**
- * Fragt den Benutzer, ob das Training beendet werden soll, und schließt es gegebenenfalls.
+ * Asks the user if they want to end the training and closes it if confirmed.
*/
closeTraining(): void {
- if (confirm('Möchtest du das Training wirklich beenden?')) {
+ if (confirm('Do you really want to end the training?')) {
this.close.emit();
}
}
/**
- * Gibt den Fortschritt des Trainings an, z.B. "Bild 2 von 5".
+ * Returns the progress of the training, e.g., "Image 2 of 5".
*/
get progress(): string {
- return `Bild ${this.currentImageIndex + 1} von ${this.deck.images.length}`;
+ return `Image ${this.currentImageIndex + 1} of ${this.deck.images.length}`;
}
-}
+}
\ No newline at end of file
diff --git a/src/app/upload-image-modal/upload-image-modal.component.html b/src/app/upload-image-modal/upload-image-modal.component.html
index f93ac74..834ee84 100644
--- a/src/app/upload-image-modal/upload-image-modal.component.html
+++ b/src/app/upload-image-modal/upload-image-modal.component.html
@@ -1,4 +1,3 @@
-
@@ -6,29 +5,39 @@
- Schließen
+ Close
-
Bild zu Deck hinzufügen
+
Add Image to Deck
-
+
-
-
+
+
+
+
+
+
+
+
+
+
-
+
{{ processingStatus }}
-
+
-
Verarbeitung läuft...
+
Processing in progress...
-
+
\ No newline at end of file
diff --git a/src/app/upload-image-modal/upload-image-modal.component.ts b/src/app/upload-image-modal/upload-image-modal.component.ts
index 285dc3a..f53d7ae 100644
--- a/src/app/upload-image-modal/upload-image-modal.component.ts
+++ b/src/app/upload-image-modal/upload-image-modal.component.ts
@@ -1,4 +1,3 @@
-// src/app/upload-image-modal.component.ts
import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { Box, DeckImage, DeckService, OcrResult } from '../deck.service';
import { CommonModule } from '@angular/common';
@@ -9,11 +8,20 @@ import { Modal } from 'flowbite';
selector: 'app-upload-image-modal',
templateUrl: './upload-image-modal.component.html',
standalone: true,
+ styles:`
+ label {
+ transition: background-color 0.3s ease;
+ }
+
+ label:hover {
+ background-color: #2563eb;
+ }
+ `,
imports: [CommonModule]
})
export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
@Input() deckName: string = '';
- @Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage:DeckImage }>();
+ @Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage: DeckImage }>();
@ViewChild('uploadImageModal') modalElement!: ElementRef;
@ViewChild('imageFile') imageFileElement!: ElementRef;
@@ -30,7 +38,7 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
}
ngOnDestroy(): void {
- // Modal wird automatisch von Flowbite verwaltet
+ // Modal is automatically managed by Flowbite
}
open(): void {
@@ -51,63 +59,68 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
onFileChange(event: any): void {
const file: File = event.target.files[0];
if (!file) return;
+ const fileNameElement = document.getElementById('fileName');
+ if (fileNameElement) {
+ fileNameElement.textContent = file.name;
+ }
this.imageFile = file;
- this.processingStatus = 'Verarbeitung läuft...';
+ this.processingStatus = 'Processing in progress...';
this.loading = true;
const reader = new FileReader();
reader.onload = async (e) => {
const imageSrc = e.target?.result;
- // Bild als Base64-String ohne Präfix (data:image/...)
+ // Image as Base64 string without prefix (data:image/...)
const imageBase64 = (imageSrc as string).split(',')[1];
try {
const response = await this.http.post('/api/ocr', { image: imageBase64 }).toPromise();
if (!response || !response.results) {
- this.processingStatus = 'Ungültige Antwort vom OCR-Service';
+ this.processingStatus = 'Invalid response from OCR service';
this.loading = false;
return;
}
- this.processingStatus = 'Verarbeitung abgeschlossen';
+ this.processingStatus = 'Processing complete';
this.loading = false;
- // Emit Event mit Bilddaten und OCR-Ergebnissen
- const bildname=this.imageFile?.name??'';
- const bildid=response.results.length>0?response.results[0].name:null
- const boxes:Box[] = [];
+ // Emit event with image data and OCR results
+ const imageName = this.imageFile?.name ?? '';
+ const imageId = response.results.length > 0 ? response.results[0].name : null;
+ const boxes: Box[] = [];
response.results.forEach((result: OcrResult) => {
const box = result.box;
-
+
const xs = box.map((point: number[]) => point[0]);
const ys = box.map((point: number[]) => point[1]);
const xMin = Math.min(...xs);
const xMax = Math.max(...xs);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
- boxes.push({x1:xMin,x2:xMax,y1:yMin,y2:yMax})
+ boxes.push({ x1: xMin, x2: xMax, y1: yMin, y2: yMax });
});
- const deckImage:DeckImage={name:bildname,id:bildid,boxes}
+ const deckImage: DeckImage = { name: imageName, id: imageId, boxes };
this.imageUploaded.emit({ imageSrc, deckImage });
this.resetFileInput();
- // Schließe das Upload-Modal
+ // Close the upload modal
this.closeModal();
} catch (error) {
- console.error('Fehler beim OCR-Service:', error);
- this.processingStatus = 'Fehler beim OCR-Service';
+ console.error('Error with OCR service:', error);
+ this.processingStatus = 'Error with OCR service';
this.loading = false;
}
};
reader.readAsDataURL(file);
}
+
/**
- * Setzt das Datei-Input-Feld zurück, sodass dieselbe Datei erneut ausgewählt werden kann.
+ * Resets the file input field so the same file can be selected again.
*/
resetFileInput(): void {
if (this.imageFileElement && this.imageFileElement.nativeElement) {
this.imageFileElement.nativeElement.value = '';
}
}
-}
+}
\ No newline at end of file