replace alerts und confirms with modals #15

This commit is contained in:
Your Name 2025-01-28 16:33:34 +01:00
parent 47b393e134
commit 2f35648264
8 changed files with 274 additions and 188 deletions

View File

@ -0,0 +1,53 @@
// popover.component.ts
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-popover',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="fixed inset-0 bg-gray-500 bg-opacity-50 flex items-center justify-center z-50" *ngIf="visible">
<div class="bg-white rounded-lg p-6 max-w-xs w-full shadow-lg border border-gray-200">
<h3 class="text-lg font-semibold mb-4">{{ title }}</h3>
<p *ngIf="message" class="mb-4">{{ message }}</p>
<input *ngIf="showInput" type="text" class="w-full p-2 border rounded mb-4 focus:ring-2 focus:ring-blue-500 focus:border-transparent" [(ngModel)]="inputValue" (keyup.enter)="onConfirm()" autofocus />
<div class="flex justify-end space-x-2">
<button (click)="onCancel()" class="px-4 py-2 rounded bg-gray-100 text-gray-700 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400">Cancel</button>
<button (click)="onConfirm()" class="px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">
{{ confirmText }}
</button>
</div>
</div>
</div>
`,
})
export class PopoverComponent {
@Input() title: string = '';
@Input() message: string = '';
@Input() showInput: boolean = false;
@Input() confirmText: string = 'Confirm';
@Input() inputValue: string = '';
@Input() visible: boolean = false;
@Output() confirmed = new EventEmitter<string>();
@Output() canceled = new EventEmitter<void>();
onConfirm() {
this.confirmed.emit(this.inputValue);
//this.reset();
}
onCancel() {
this.canceled.emit();
//this.reset();
}
private reset() {
this.visible = false;
this.inputValue = '';
}
}

View File

@ -1,14 +1,15 @@
import { Component, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
import { DeckService } from '../deck.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Modal } from 'flowbite'; import { AfterViewInit, Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Modal } from 'flowbite';
import { DeckService } from '../deck.service';
import { PopoverService } from '../services/popover.service';
@Component({ @Component({
selector: 'app-create-deck-modal', selector: 'app-create-deck-modal',
templateUrl: './create-deck-modal.component.html', templateUrl: './create-deck-modal.component.html',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule] imports: [CommonModule, FormsModule],
}) })
export class CreateDeckModalComponent implements AfterViewInit { export class CreateDeckModalComponent implements AfterViewInit {
@Output() deckCreated = new EventEmitter<void>(); @Output() deckCreated = new EventEmitter<void>();
@ -16,7 +17,7 @@ export class CreateDeckModalComponent implements AfterViewInit {
deckName: string = ''; deckName: string = '';
modal!: Modal; modal!: Modal;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService, private popoverService: PopoverService) {}
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.modal = new Modal(this.modalElement.nativeElement); this.modal = new Modal(this.modalElement.nativeElement);
@ -29,7 +30,10 @@ export class CreateDeckModalComponent implements AfterViewInit {
createDeck(event: Event): void { createDeck(event: Event): void {
event.preventDefault(); event.preventDefault();
if (this.deckName.trim() === '') { if (this.deckName.trim() === '') {
alert('Please enter a deck name.'); this.popoverService.show({
title: 'Information',
message: 'Please enter a deck name !',
});
return; return;
} }
@ -39,10 +43,13 @@ export class CreateDeckModalComponent implements AfterViewInit {
this.deckCreated.emit(); this.deckCreated.emit();
this.modal.hide(); this.modal.hide();
}, },
error: (err) => { error: err => {
console.error('Error creating deck', err); console.error('Error creating deck', err);
alert('Error creating deck.'); this.popoverService.show({
} title: 'Error',
message: 'Error creating deck.',
});
},
}); });
} }

View File

@ -18,7 +18,7 @@
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
<div class="flex items-center space-x-2 whitespace-nowrap"> <div class="flex items-center space-x-2 whitespace-nowrap">
<h3 class="font-medium">{{ deck.name }}</h3> <h3 class="font-medium">{{ deck.name }}</h3>
<span class="text-gray-600">({{ deck.images.length }})</span> <span class="text-gray-600">({{ deck.images.length }} Pics)</span>
</div> </div>
<div class="text-sm text-gray-600"> <div class="text-sm text-gray-600">
<div>Next training: {{ getNextTrainingString(deck) }}</div> <div>Next training: {{ getNextTrainingString(deck) }}</div>
@ -123,20 +123,40 @@
<app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal> <app-move-image-modal *ngIf="imageToMove" [image]="imageToMove.image" [sourceDeck]="imageToMove.sourceDeck" [decks]="decks" (moveCompleted)="onImageMoved()" (closed)="imageToMove = null"> </app-move-image-modal>
</div> </div>
<!-- Popover für die Löschbestätigung --> <!-- Popover für die Löschbestätigung -->
<div id="deletePopover" popover="manual" class="popover"> <!-- <div id="deletePopover" popover="manual" class="popover">
<p>Are you sure you want to delete this deck?</p> <p>Are you sure you want to delete this deck?</p>
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<button (click)="confirmDelete()" class="bg-red-500 text-white px-4 py-2 rounded">Delete</button> <button (click)="confirmDelete()" class="bg-red-500 text-white px-4 py-2 rounded">Delete</button>
<button (click)="cancelDelete()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button> <button (click)="cancelDelete()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button>
</div> </div>
</div> </div> -->
<!-- Popover für die Eingabe des neuen Decknamens --> <!-- Popover für die Eingabe des neuen Decknamens -->
<div id="renamePopover" popover="manual" class="popover"> <!-- <div id="renamePopover" popover="manual" class="popover">
<p>Enter the new name for the deck:</p> <p>Enter the new name for the deck:</p>
<input type="text" id="newDeckNameInput" class="w-full p-2 border rounded mb-2" [value]="deckToRename?.name" /> <input type="text" id="newDeckNameInput" class="w-full p-2 border rounded mb-2" [value]="deckToRename?.name" />
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<button (click)="confirmRename()" class="bg-blue-500 text-white px-4 py-2 rounded">Rename</button> <button (click)="confirmRename()" class="bg-blue-500 text-white px-4 py-2 rounded">Rename</button>
<button (click)="cancelRename()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button> <button (click)="cancelRename()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancel</button>
</div> </div>
</div> </div> -->
<!-- Statt der alten Popover-Divs -->
<!-- <app-popover [visible]="showDeletePopover" title="Delete Deck" message="Are you sure you want to delete this deck?" confirmText="Delete" (confirmed)="confirmDelete()" (canceled)="cancelDelete()"></app-popover>
<app-popover
[visible]="showRenamePopover"
title="Rename Deck"
[showInput]="true"
[inputValue]="deckToRename?.name || ''"
confirmText="Rename"
(confirmed)="confirmRename($event)"
(canceled)="cancelRename()"
></app-popover>
<app-popover
[visible]="showDeleteImagePopover"
title="Delete Image"
message="Are you sure you want to delete this image?"
confirmText="Delete"
(confirmed)="confirmImageDelete()"
(canceled)="cancelImageDelete()"
></app-popover> -->

View File

@ -1,11 +1,13 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component'; import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component';
import { Box, Deck, DeckImage, DeckService, OcrResult } from './deck.service'; import { Box, Deck, DeckImage, DeckService, OcrResult } from './deck.service';
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component'; import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component'; import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
import { PopoverService } from './services/popover.service';
import { TrainingComponent } from './training/training.component'; import { TrainingComponent } from './training/training.component';
@Component({ @Component({
@ -28,14 +30,13 @@ import { TrainingComponent } from './training/training.component';
TrainingComponent, TrainingComponent,
EditImageModalComponent, EditImageModalComponent,
MoveImageModalComponent, // Adding the new component MoveImageModalComponent, // Adding the new component
FormsModule,
], ],
}) })
export class DeckListComponent implements OnInit { export class DeckListComponent implements OnInit {
decks: Deck[] = []; decks: Deck[] = [];
trainingsDeck: Deck | null = null; trainingsDeck: Deck | null = null;
activeDeck: Deck | null = null; activeDeck: Deck | null = null;
private deckToDelete: string | null = null;
deckToRename: Deck | null = null;
loading: boolean = false; loading: boolean = false;
@ViewChild(CreateDeckModalComponent) @ViewChild(CreateDeckModalComponent)
@ -54,7 +55,7 @@ export class DeckListComponent implements OnInit {
// State for moving images // State for moving images
imageToMove: { image: DeckImage; sourceDeck: Deck } | null = null; imageToMove: { image: DeckImage; sourceDeck: Deck } | null = null;
constructor(private deckService: DeckService, private cdr: ChangeDetectorRef, private http: HttpClient) {} constructor(private deckService: DeckService, private cdr: ChangeDetectorRef, private http: HttpClient, private popoverService: PopoverService) {}
ngOnInit(): void { ngOnInit(): void {
this.loadExpandedDecks(); this.loadExpandedDecks();
@ -84,11 +85,12 @@ export class DeckListComponent implements OnInit {
// Method to open the delete confirmation popover // Method to open the delete confirmation popover
openDeletePopover(deckName: string): void { openDeletePopover(deckName: string): void {
this.deckToDelete = deckName; this.popoverService.show({
const deletePopover = document.getElementById('deletePopover'); title: 'Delete Deck',
if (deletePopover) { message: 'Are you sure you want to delete this deck?',
deletePopover.showPopover(); confirmText: 'Delete',
} onConfirm: () => this.confirmDelete(deckName),
});
} }
// Method to check if a deck is active // Method to check if a deck is active
@ -97,113 +99,56 @@ export class DeckListComponent implements OnInit {
} }
// Method to confirm the deletion of a deck // Method to confirm the deletion of a deck
confirmDelete(): void { confirmDelete(deckName: string): void {
if (this.deckToDelete) { this.deckService.deleteDeck(deckName).subscribe({
this.deckService.deleteDeck(this.deckToDelete).subscribe({
next: () => { next: () => {
this.loadDecks(); this.loadDecks();
this.closeDeletePopover();
this.activeDeck = this.decks.length > 0 ? this.decks[0] : null; this.activeDeck = this.decks.length > 0 ? this.decks[0] : null;
}, },
error: err => console.error('Error deleting deck', err), error: err => console.error('Error deleting deck', err),
}); });
} }
}
// Method to cancel the deletion of a deck
cancelDelete(): void {
this.closeDeletePopover();
}
// Method to close the delete confirmation popover
closeDeletePopover(): void {
const deletePopover = document.getElementById('deletePopover');
if (deletePopover) {
deletePopover.hidePopover();
}
this.deckToDelete = null;
}
// Method to open the rename popover // Method to open the rename popover
openRenamePopover(deck: Deck): void { openRenamePopover(deck: Deck): void {
this.deckToRename = deck; this.popoverService.showWithInput({
const renamePopover = document.getElementById('renamePopover'); title: 'Rename Deck',
if (renamePopover) { message: 'Enter the new name for the deck:',
renamePopover.showPopover(); confirmText: 'Rename',
// Set the input value to the current deck name and select it inputValue: deck.name,
setTimeout(() => { onConfirm: (inputValue?: string) => this.confirmRename(deck, inputValue),
const newDeckNameInput = document.getElementById('newDeckNameInput') as HTMLInputElement; });
if (newDeckNameInput) {
newDeckNameInput.value = deck.name;
newDeckNameInput.select();
}
}, 0);
}
} }
// Method to confirm the renaming of a deck // Method to confirm the renaming of a deck
confirmRename(): void { confirmRename(deck: Deck, newName?: string): void {
const newDeckNameInput = document.getElementById('newDeckNameInput') as HTMLInputElement; if (newName && newName.trim() !== '' && newName !== deck.name) {
if (newDeckNameInput && this.deckToRename) { this.deckService.renameDeck(deck.name, newName).subscribe({
const newDeckName = newDeckNameInput.value.trim();
if (newDeckName && newDeckName !== this.deckToRename.name) {
this.deckService.renameDeck(this.deckToRename.name, newDeckName).subscribe({
next: () => { next: () => {
// Aktualisiere den Namen im activeDeck, falls dieser der umbenannte Deck ist if (this.activeDeck?.name === deck.name) {
if (this.activeDeck && this.activeDeck.name === this.deckToRename?.name) { this.activeDeck.name = newName;
this.activeDeck.name = newDeckName;
} }
this.loadDecks(); // Lade die Decks neu, um die Liste zu aktualisieren this.loadDecks();
this.closeRenamePopover();
}, },
error: err => console.error('Error renaming deck', err), error: err => console.error('Error renaming deck', err),
}); });
} } else {
} // Optional: Handle ungültigen neuen Namen
} console.warn('Invalid new deck name.');
// Method to cancel the renaming of a deck
cancelRename(): void {
this.closeRenamePopover();
}
// Method to close the rename popover
closeRenamePopover(): void {
const renamePopover = document.getElementById('renamePopover');
if (renamePopover) {
renamePopover.hidePopover();
}
this.deckToRename = null;
}
// Method to delete a deck
deleteDeck(deckName: string): void {
if (!confirm(`Are you sure you want to delete the deck "${deckName}"?`)) {
return;
}
this.deckService.deleteDeck(deckName).subscribe({
next: () => this.loadDecks(),
error: err => console.error('Error deleting deck', err),
});
}
// Method to rename a deck
renameDeck(deck: Deck): void {
const newDeckName = prompt('Enter the new name for the deck:', deck.name);
if (newDeckName && newDeckName !== deck.name) {
this.deckService.renameDeck(deck.name, newDeckName).subscribe({
next: () => this.loadDecks(),
error: err => console.error('Error renaming deck', err),
});
} }
} }
// Method to delete an image from a deck // Delete-Image Methoden ersetzen
deleteImage(deck: Deck, image: DeckImage): void { deleteImage(deck: Deck, image: DeckImage): void {
if (!confirm(`Are you sure you want to delete the image "${image.name}"?`)) { this.popoverService.show({
return; title: 'Delete Image',
message: `Are you sure you want to delete the image ${image.name}?`,
confirmText: 'Delete',
onConfirm: () => this.confirmImageDelete(deck, image),
});
} }
confirmImageDelete(deck: Deck, image: DeckImage): void {
const imageId = image.id; const imageId = image.id;
this.deckService.deleteImage(imageId).subscribe({ this.deckService.deleteImage(imageId).subscribe({
@ -211,7 +156,7 @@ export class DeckListComponent implements OnInit {
this.loadDecks(); this.loadDecks();
if (this.activeDeck) { if (this.activeDeck) {
this.activeDeck.images = this.activeDeck.images.filter(img => img.id !== imageId); this.activeDeck.images = this.activeDeck.images.filter(img => img.id !== imageId);
this.cdr.detectChanges(); // Erzwingt einen UI-Update this.cdr.detectChanges();
} }
}, },
error: err => console.error('Error deleting image', err), error: err => console.error('Error deleting image', err),
@ -416,6 +361,6 @@ export class DeckListComponent implements OnInit {
// Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter // Methode zur Berechnung der Anzahl der zu bearbeitenden Wörter
getWordsToReview(deck: Deck): number { getWordsToReview(deck: Deck): number {
const nextTraining = this.getNextTrainingDate(deck); const nextTraining = this.getNextTrainingDate(deck);
return deck.images.flatMap(image => image.boxes.filter(box => box.due && new Date(box.due * 86400000) <= new Date(nextTraining))).length; return deck.images.flatMap(image => image.boxes.filter(box => (box.due && new Date(box.due * 86400000) <= new Date(nextTraining)) || !box.due)).length;
} }
} }

View File

@ -1,22 +1,23 @@
// src/app/edit-image-modal.component.ts // src/app/edit-image-modal.component.ts
import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { fabric } from 'fabric'; import { fabric } from 'fabric';
import { Modal } from 'flowbite'; import { Modal } from 'flowbite';
import { DeckImage, DeckService, OcrResult } from '../deck.service'; import { DeckImage, DeckService } from '../deck.service';
import { PopoverService } from '../services/popover.service';
@Component({ @Component({
selector: 'app-edit-image-modal', selector: 'app-edit-image-modal',
templateUrl: './edit-image-modal.component.html', templateUrl: './edit-image-modal.component.html',
standalone: true, standalone: true,
imports: [CommonModule] imports: [CommonModule],
}) })
export class EditImageModalComponent implements AfterViewInit, OnDestroy { export class EditImageModalComponent implements AfterViewInit, OnDestroy {
// Constant for box color // Constant for box color
private readonly BOX_COLOR = 'rgba(255, 0, 0, 0.3)'; // Red with transparency private readonly BOX_COLOR = 'rgba(255, 0, 0, 0.3)'; // Red with transparency
@Input() deckName: string = ''; @Input() deckName: string = '';
@Input() imageData: { imageSrc: string | ArrayBuffer | null, deckImage: DeckImage | null } = { imageSrc: null, deckImage: null }; @Input() imageData: { imageSrc: string | ArrayBuffer | null; deckImage: DeckImage | null } = { imageSrc: null, deckImage: null };
@Output() imageSaved = new EventEmitter<void>(); @Output() imageSaved = new EventEmitter<void>();
@Output() closed = new EventEmitter<void>(); @Output() closed = new EventEmitter<void>();
@ViewChild('editImageModal') modalElement!: ElementRef; @ViewChild('editImageModal') modalElement!: ElementRef;
@ -32,13 +33,13 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
private keyDownHandler!: (e: KeyboardEvent) => void; private keyDownHandler!: (e: KeyboardEvent) => void;
modal: any; modal: any;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService, private popoverService: PopoverService) {}
async ngAfterViewInit() { async ngAfterViewInit() {
this.modal = new Modal(this.modalElement.nativeElement, { this.modal = new Modal(this.modalElement.nativeElement, {
onHide: () => { onHide: () => {
this.closed.emit(); this.closed.emit();
} },
}); });
this.maxCanvasWidth = window.innerWidth * 0.6; this.maxCanvasWidth = window.innerWidth * 0.6;
@ -75,14 +76,14 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fabric.Image.fromURL( fabric.Image.fromURL(
url, url,
(img) => { img => {
resolve(img); resolve(img);
}, },
{ {
crossOrigin: 'anonymous', crossOrigin: 'anonymous',
originX: 'left', originX: 'left',
originY: 'top', originY: 'top',
} },
); );
}); });
} }
@ -122,7 +123,6 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
// Add boxes // Add boxes
this.imageData.deckImage?.boxes.forEach(box => { this.imageData.deckImage?.boxes.forEach(box => {
const rect = new fabric.Rect({ const rect = new fabric.Rect({
left: box.x1 * scaleFactor, left: box.x1 * scaleFactor,
top: box.y1 * scaleFactor, top: box.y1 * scaleFactor,
@ -157,7 +157,6 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
this.updateBoxCoordinates(); this.updateBoxCoordinates();
// this.detectedText = ocrResults.map(result => result.text).join('\n'); // this.detectedText = ocrResults.map(result => result.text).join('\n');
} catch (error) { } catch (error) {
console.error('Error processing image:', error); console.error('Error processing image:', error);
} }
@ -198,7 +197,7 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
x1: Math.round(x1), x1: Math.round(x1),
x2: Math.round(x2), x2: Math.round(x2),
y1: Math.round(y1), y1: Math.round(y1),
y2: Math.round(y2) y2: Math.round(y2),
}); });
}); });
@ -265,11 +264,14 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
this.imageSaved.emit(); this.imageSaved.emit();
this.closeModal(); this.closeModal();
}, },
error: (err) => { error: err => {
console.error('Error saving image:', err); console.error('Error saving image:', err);
alert('Error saving image.'); this.popoverService.show({
title: 'Error',
message: 'Error saving image.',
});
this.closeModal(); this.closeModal();
} },
}); });
} }
} }

View File

@ -1,13 +1,14 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { DeckImage, Deck, DeckService } from '../deck.service'; import { Deck, DeckImage, DeckService } from '../deck.service';
import { PopoverService } from '../services/popover.service';
@Component({ @Component({
selector: 'app-move-image-modal', selector: 'app-move-image-modal',
templateUrl: './move-image-modal.component.html', templateUrl: './move-image-modal.component.html',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule] imports: [CommonModule, FormsModule],
}) })
export class MoveImageModalComponent { export class MoveImageModalComponent {
@Input() image!: DeckImage; @Input() image!: DeckImage;
@ -18,7 +19,7 @@ export class MoveImageModalComponent {
selectedDeckId: number | null = null; selectedDeckId: number | null = null;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService, private popoverService: PopoverService) {}
moveImage(): void { moveImage(): void {
if (this.selectedDeckId === null) { if (this.selectedDeckId === null) {
@ -30,10 +31,13 @@ export class MoveImageModalComponent {
this.moveCompleted.emit(); this.moveCompleted.emit();
this.close(); this.close();
}, },
error: (err) => { error: err => {
console.error('Error moving image:', err); console.error('Error moving image:', err);
alert('Error moving image.'); this.popoverService.show({
} title: 'Error',
message: 'Error moving image.',
});
},
}); });
} }

View File

@ -0,0 +1,34 @@
// popover.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class PopoverService {
private showPopoverSource = new Subject<{
title: string;
message: string;
showInput: boolean;
inputValue: string;
confirmText: string;
onConfirm: () => void;
onCancel?: () => void;
}>();
popoverState$ = this.showPopoverSource.asObservable();
show(options: { title: string; message: string; confirmText?: string; onConfirm?: (inputValue?: string) => void; onCancel?: () => void }) {
this.showPopoverSource.next({
showInput: false,
inputValue: null,
confirmText: 'Ok',
onConfirm: (inputValue?: string) => {},
...options,
});
}
showWithInput(options: { title: string; message: string; confirmText: string; inputValue: string; onConfirm: (inputValue?: string) => void; onCancel?: () => void }) {
this.showPopoverSource.next({
showInput: true,
...options,
});
}
}

View File

@ -2,25 +2,26 @@ import { CommonModule } from '@angular/common';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { Box, Deck, DeckImage, DeckService } from '../deck.service'; import { Box, Deck, DeckImage, DeckService } from '../deck.service';
import { PopoverService } from '../services/popover.service';
const LEARNING_STEPS = { const LEARNING_STEPS = {
AGAIN: 1, // 1 minute AGAIN: 1, // 1 minute
GOOD: 10, // 10 minutes GOOD: 10, // 10 minutes
GRADUATION: 1440 // 1 day (in minutes) GRADUATION: 1440, // 1 day (in minutes)
}; };
const FACTOR_CHANGES = { const FACTOR_CHANGES = {
AGAIN: 0.85, // Reduce factor by 15% AGAIN: 0.85, // Reduce factor by 15%
EASY: 1.15, // Increase factor by 15% EASY: 1.15, // Increase factor by 15%
MIN: 1.3, // Minimum factor allowed MIN: 1.3, // Minimum factor allowed
MAX: 2.9 // Maximum factor allowed MAX: 2.9, // Maximum factor allowed
}; };
const EASY_INTERVAL = 4 * 1440; // 4 days in minutes 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',
standalone: true, standalone: true,
imports: [CommonModule] imports: [CommonModule],
}) })
export class TrainingComponent implements OnInit { export class TrainingComponent implements OnInit {
@Input() deck!: Deck; @Input() deck!: Deck;
@ -37,7 +38,7 @@ export class TrainingComponent implements OnInit {
isShowingBox: boolean = false; isShowingBox: boolean = false;
isTrainingFinished: boolean = false; isTrainingFinished: boolean = false;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService, private popoverService: PopoverService) {}
ngOnInit(): void { ngOnInit(): void {
// Initialization was done in ngAfterViewInit // Initialization was done in ngAfterViewInit
@ -48,7 +49,10 @@ export class TrainingComponent implements OnInit {
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 {
alert('No deck or images available.'); this.popoverService.show({
title: 'Information',
message: 'No deck or images available.',
});
this.close.emit(); this.close.emit();
} }
} }
@ -135,7 +139,7 @@ export class TrainingComponent implements OnInit {
this.boxesToReview.forEach((box, index) => { this.boxesToReview.forEach((box, index) => {
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);
if (this.currentBoxIndex === index && this.isShowingBox || (box.due && box.due - this.getTodayInDays() > 0)) { if ((this.currentBoxIndex === index && this.isShowingBox) || (box.due && box.due - this.getTodayInDays() > 0)) {
// Box is currently revealed, no overlay // Box is currently revealed, no overlay
return; return;
} else if (this.currentBoxIndex === index && !this.isShowingBox) { } else if (this.currentBoxIndex === index && !this.isShowingBox) {
@ -154,7 +158,10 @@ export class TrainingComponent implements OnInit {
img.onerror = () => { img.onerror = () => {
console.error('Error loading image for canvas.'); console.error('Error loading image for canvas.');
alert('Error loading image for canvas.'); this.popoverService.show({
title: 'Error',
message: 'Error loading image for canvas.',
});
this.close.emit(); this.close.emit();
}; };
} }
@ -165,7 +172,8 @@ export class TrainingComponent implements OnInit {
* @returns The shuffled array * @returns The shuffled array
*/ */
shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
let currentIndex = array.length, randomIndex; let currentIndex = array.length,
randomIndex;
// While there are elements to shuffle // While there are elements to shuffle
while (currentIndex !== 0) { while (currentIndex !== 0) {
@ -174,8 +182,7 @@ export class TrainingComponent implements OnInit {
currentIndex--; currentIndex--;
// Swap it with the current element // Swap it with the current element
[array[currentIndex], array[randomIndex]] = [ [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
array[randomIndex], array[currentIndex]];
} }
return array; return array;
@ -257,7 +264,10 @@ export class TrainingComponent implements OnInit {
await lastValueFrom(this.deckService.updateBox(box)); await lastValueFrom(this.deckService.updateBox(box));
} catch (error) { } catch (error) {
console.error('Error updating box:', error); console.error('Error updating box:', error);
alert('Error updating box.'); this.popoverService.show({
title: 'Error',
message: 'Error updating box.',
});
} }
} }
@ -267,21 +277,24 @@ export class TrainingComponent implements OnInit {
* @param action The action ('again', 'good', 'easy') * @param action The action ('again', 'good', 'easy')
* @returns An object with the new values for ivl, factor, reps, and lapses * @returns An object with the new values for ivl, factor, reps, and lapses
*/ */
calculateNewInterval(box: Box, action: 'again' | 'good' | 'easy'): { calculateNewInterval(
newIvl: number, box: Box,
newFactor: number, action: 'again' | 'good' | 'easy',
newReps: number, ): {
newLapses: number, newIvl: number;
newIsGraduated: boolean newFactor: number;
newReps: number;
newLapses: number;
newIsGraduated: boolean;
} { } {
const LEARNING_STEPS = { const LEARNING_STEPS = {
AGAIN: 1, // 1 minute AGAIN: 1, // 1 minute
GOOD: 10, // 10 minutes GOOD: 10, // 10 minutes
GRADUATION: 1440 // 1 day (1440 minutes) GRADUATION: 1440, // 1 day (1440 minutes)
}; };
const EASY_BONUS = 1.3; // Zusätzlicher Easy-Multiplikator const EASY_BONUS = 1.3; // Zusätzlicher Easy-Multiplikator
/* const FACTOR_CHANGES = { /* const FACTOR_CHANGES = {
MIN: 1.3, MIN: 1.3,
MAX: 2.5, MAX: 2.5,
AGAIN: 0.85, AGAIN: 0.85,
@ -347,8 +360,7 @@ export class TrainingComponent implements OnInit {
* @returns The next interval as a string (e.g., "10 min", "2 d") * @returns The next interval as a string (e.g., "10 min", "2 d")
*/ */
getNextInterval(box: Box | null, action: 'again' | 'good' | 'easy'): string { getNextInterval(box: Box | null, action: 'again' | 'good' | 'easy'): string {
if (!box) if (!box) return '';
return '';
const { newIvl } = this.calculateNewInterval(box, action); const { newIvl } = this.calculateNewInterval(box, action);
@ -407,7 +419,10 @@ export class TrainingComponent implements OnInit {
this.currentImageIndex++; this.currentImageIndex++;
this.loadImage(this.currentImageIndex); this.loadImage(this.currentImageIndex);
} else { } else {
alert('This is the last image in the deck.'); this.popoverService.show({
title: 'Information',
message: 'This is the last image in the deck.',
});
} }
} }
@ -416,7 +431,10 @@ export class TrainingComponent implements OnInit {
*/ */
endTraining(): void { endTraining(): void {
this.isTrainingFinished = true; this.isTrainingFinished = true;
alert(`Training completed!`); this.popoverService.show({
title: 'Information',
message: 'Training completed!',
});
this.close.emit(); this.close.emit();
} }
@ -424,9 +442,12 @@ export class TrainingComponent implements OnInit {
* Asks the user if they want to end the training and closes it if confirmed. * Asks the user if they want to end the training and closes it if confirmed.
*/ */
closeTraining(): void { closeTraining(): void {
if (confirm('Do you really want to end the training?')) { this.popoverService.show({
this.close.emit(); title: 'End Training',
} message: 'Do you really want to end the training?',
confirmText: 'End Training',
onConfirm: (inputValue?: string) => this.close.emit(),
});
} }
/** /**