move image, edit image

This commit is contained in:
aknuth 2024-12-07 21:29:51 +01:00
parent 92ec07fe75
commit 26518bef56
9 changed files with 193 additions and 57 deletions

View File

@ -57,6 +57,12 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<!-- Move Icon -->
<button (click)="openMoveImageModal(deck, image)" class="text-yellow-500 hover:text-yellow-700" title="Bild verschieben">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
</div> </div>
</li> </li>
</ul> </ul>
@ -83,4 +89,13 @@
<app-edit-image-modal *ngIf="imageData" [deckName]="currentUploadDeckName" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal> <app-edit-image-modal *ngIf="imageData" [deckName]="currentUploadDeckName" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal>
<!-- TrainingComponent --> <!-- TrainingComponent -->
<app-training *ngIf="selectedDeck" [deck]="selectedDeck" (close)="closeTraining()"></app-training> <app-training *ngIf="selectedDeck" [deck]="selectedDeck" (close)="closeTraining()"></app-training>
<!-- MoveImageModalComponent -->
<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>

View File

@ -7,6 +7,7 @@ import { TrainingComponent } from './training/training.component';
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component'; import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component';
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component'; import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
@Component({ @Component({
selector: 'app-deck-list', selector: 'app-deck-list',
@ -18,6 +19,7 @@ import { firstValueFrom } from 'rxjs';
UploadImageModalComponent, UploadImageModalComponent,
TrainingComponent, TrainingComponent,
EditImageModalComponent, EditImageModalComponent,
MoveImageModalComponent, // Hinzufügen der neuen Komponente
UploadImageModalComponent UploadImageModalComponent
] ]
}) })
@ -30,13 +32,16 @@ export class DeckListComponent implements OnInit {
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent; @ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
@ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent; @ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent;
imageData: { imageSrc: string | ArrayBuffer | null, deckImage:DeckImage} | null = null; imageData: { imageSrc: string | ArrayBuffer | null, deckImage: DeckImage } | null = null;
currentUploadDeckName: string = ''; currentUploadDeckName: string = '';
// Set zur Verfolgung erweiterter Decks // Set zur Verfolgung erweiterter Decks
expandedDecks: Set<number> = new Set<number>(); expandedDecks: Set<number> = new Set<number>();
// State für das Verschieben von Bildern
imageToMove: { image: DeckImage, sourceDeck: Deck } | null = null;
constructor(private deckService: DeckService) { } constructor(private deckService: DeckService) { }
ngOnInit(): void { ngOnInit(): void {
@ -73,8 +78,10 @@ export class DeckListComponent implements OnInit {
error: (err) => console.error('Fehler beim Löschen des Bildes', err) error: (err) => console.error('Fehler beim Löschen des Bildes', err)
}); });
} }
editImage(deck: Deck, image: DeckImage): void { editImage(deck: Deck, image: DeckImage): void {
let imageSrc = null let imageSrc = null
this.currentUploadDeckName = deck.name;
fetch(`/api/debug_image/${image.id}/original_compressed.jpg`) fetch(`/api/debug_image/${image.id}/original_compressed.jpg`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
@ -86,7 +93,7 @@ export class DeckListComponent implements OnInit {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
imageSrc = reader.result; // Base64-String imageSrc = reader.result; // Base64-String
this.imageData = {imageSrc,deckImage:image} this.imageData = { imageSrc, deckImage: image }
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}) })
@ -94,6 +101,7 @@ export class DeckListComponent implements OnInit {
console.error('Fehler beim Laden des Bildes:', error); console.error('Fehler beim Laden des Bildes:', error);
}); });
} }
openTraining(deck: Deck): void { openTraining(deck: Deck): void {
this.selectedDeck = deck; this.selectedDeck = deck;
} }
@ -139,18 +147,8 @@ export class DeckListComponent implements OnInit {
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 erweitern // Wenn keine Daten gespeichert sind, alle Decks standardmäßig nicht erweitern
this.expandedDecks = new Set<number>(); this.expandedDecks = new Set<number>();
// Dieser Teil wird nach dem Laden der Decks initialisiert
// Wir erweitern alle Decks, sobald sie geladen sind
this.deckService.getDecks().subscribe({
next: (data) => {
data.forEach(deck => this.expandedDecks.add(deck.id));
this.saveExpandedDecks();
this.decks = data;
},
error: (err) => console.error('Fehler beim Laden der Decks', err)
});
} }
} }
@ -169,12 +167,25 @@ export class DeckListComponent implements OnInit {
onImageUploaded(imageData: any): void { onImageUploaded(imageData: any): void {
this.imageData = imageData; this.imageData = imageData;
} }
onClosed(){ onClosed(){
this.imageData = null; this.imageData = null;
} }
async onImageSaved() { async onImageSaved() {
// Handle das Speichern der Bilddaten, z.B. aktualisiere die Liste der Bilder // Handle das Speichern der Bilddaten, z.B. aktualisiere die Liste der Bilder
this.imageData = null; this.imageData = null;
this.decks = await firstValueFrom(this.deckService.getDecks()) this.decks = await firstValueFrom(this.deckService.getDecks())
} }
// Methode zum Öffnen des MoveImageModal
openMoveImageModal(deck: Deck, image: DeckImage): void {
this.imageToMove = { image, sourceDeck: deck };
}
// Handler für das moveCompleted Event
onImageMoved(): void {
this.imageToMove = null;
this.loadDecks();
}
} }

View File

@ -88,32 +88,7 @@ export class DeckService {
getDeck(deckname:string): Observable<Deck> { getDeck(deckname:string): Observable<Deck> {
return this.http.get<Deck>(`${this.apiUrl}/${deckname}/images`); return this.http.get<Deck>(`${this.apiUrl}/${deckname}/images`);
} }
// getImage(name: string): Observable<DeckImage> {
// return this.http.get<BackendBox[]>(`${this.apiUrl}/image/${name}`).pipe(
// map(response => {
// if (response.length === 0) {
// // Falls keine Daten zurückgegeben werden, ein leeres DeckImage zurückgeben
// return { name: name, boxes: [] };
// }
// // Extrahiere den Bildnamen aus dem ersten Element des Arrays
// const imageName = response[0].bildname;
// // Mape die Backend-Daten auf das Box-Interface
// const boxes: Box[] = response.map(item => ({
// x1: item.x1,
// x2: item.x2,
// y1: item.y1,
// y2: item.y2
// }));
// return {
// name: imageName,
// boxes: boxes
// };
// })
// );
// }
createDeck(deckname: string): Observable<any> { createDeck(deckname: string): Observable<any> {
return this.http.post(this.apiUrl, { deckname }); return this.http.post(this.apiUrl, { deckname });
} }
@ -130,4 +105,9 @@ export class DeckService {
deleteImage(imageName: string): Observable<any> { deleteImage(imageName: string): Observable<any> {
return this.http.delete(`${this.apiUrl}/image/${imageName}`); return this.http.delete(`${this.apiUrl}/image/${imageName}`);
} }
// Neue Methode zum Verschieben eines Bildes
moveImage(imageId: string, targetDeckId: number): Observable<any> {
return this.http.post(`${this.apiUrl}/images/${imageId}/move`, { targetDeckId });
}
} }

View File

@ -9,21 +9,25 @@
<span class="sr-only">Schließen</span> <span class="sr-only">Schließen</span>
</button> </button>
<div class="p-6 relative"> <div class="p-6 relative">
<h3 class="mb-4 text-xl font-medium text-gray-900">Bild bearbeiten</h3> <!-- Überschrift mit Boxanzahl -->
<h3 class="mb-4 text-xl font-medium text-gray-900">
Bild bearbeiten <span *ngIf="boxes.length > 0">({{ boxes.length }} Box{{ boxes.length > 1 ? 'en' : '' }})</span>
</h3>
<!-- Canvas und Save Button --> <!-- Canvas -->
<div class="mt-4"> <div class="mt-4">
<canvas #canvas class="border border-gray-300 rounded w-full h-auto"></canvas> <canvas #canvas class="border border-gray-300 rounded w-full h-auto"></canvas>
<button (click)="save()" class="mt-4 bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
Save
</button>
</div> </div>
<!-- Erkannten Text anzeigen --> <!-- Buttons unter dem Canvas -->
<!-- <div *ngIf="detectedText" class="mt-4"> <div class="mt-4 flex justify-between">
<h4 class="text-lg font-medium text-gray-700">Erkannter Text:</h4> <button (click)="save()" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
<pre class="whitespace-pre-wrap text-sm text-gray-600">{{ detectedText }}</pre> Save
</div> --> </button>
<button (click)="addNewBox()" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">
Neue Box
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,6 +12,9 @@ import { DeckImage, DeckService, OcrResult } from '../deck.service';
imports: [CommonModule] imports: [CommonModule]
}) })
export class EditImageModalComponent implements AfterViewInit, OnDestroy { export class EditImageModalComponent implements AfterViewInit, OnDestroy {
// Konstante für die Boxfarbe
private readonly BOX_COLOR = 'rgba(255, 0, 0, 0.3)'; // Rot mit Transparenz
@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>();
@ -35,7 +38,7 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
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;
@ -87,7 +90,7 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
async processImage(): Promise<void> { async processImage(): Promise<void> {
try { try {
if (!this.imageData){ if (!this.imageData){
return return;
} }
this.canvas = new fabric.Canvas(this.canvasElement.nativeElement); this.canvas = new fabric.Canvas(this.canvasElement.nativeElement);
@ -123,9 +126,9 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
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,
width: (box.x2-box.x1) * scaleFactor, width: (box.x2 - box.x1) * scaleFactor,
height: (box.y2-box.y1) * scaleFactor, height: (box.y2 - box.y1) * scaleFactor,
fill: 'rgba(255, 0, 0, 0.3)', fill: this.BOX_COLOR, // Verwendung der Konstante
selectable: true, selectable: true,
hasControls: true, hasControls: true,
hasBorders: true, hasBorders: true,
@ -202,12 +205,58 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
this.canvas.requestRenderAll(); this.canvas.requestRenderAll();
} }
addNewBox(): void {
if (!this.canvas) {
return;
}
const boxWidth = 100;
const boxHeight = 50;
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
const rect = new fabric.Rect({
left: (canvasWidth - boxWidth) / 2,
top: (canvasHeight - boxHeight) / 2,
width: boxWidth,
height: boxHeight,
fill: this.BOX_COLOR, // Verwendung der Konstante
selectable: true,
hasControls: true,
hasBorders: true,
objectCaching: false,
});
rect.on('modified', () => {
this.updateBoxCoordinates();
});
rect.on('moved', () => {
this.updateBoxCoordinates();
});
rect.on('scaled', () => {
this.updateBoxCoordinates();
});
rect.on('rotated', () => {
this.updateBoxCoordinates();
});
rect.on('removed', () => {
this.updateBoxCoordinates();
});
this.canvas.add(rect);
this.canvas.setActiveObject(rect);
this.canvas.requestRenderAll();
this.updateBoxCoordinates();
}
save(): void { save(): void {
// Hier implementierst du die Logik zum Speichern der Bilddaten // Hier implementierst du die Logik zum Speichern der Bilddaten
// Zum Beispiel über einen Service oder direkt hier // Zum Beispiel über einen Service oder direkt hier
const data = { const data = {
deckname: this.deckName, deckname: this.deckName,
bildname: this.imageData.deckImage?.name,//this.imageFile?.name, bildname: this.imageData.deckImage?.name, // this.imageFile?.name,
bildid: this.imageData.deckImage?.id, bildid: this.imageData.deckImage?.id,
boxes: this.boxes, boxes: this.boxes,
}; };
@ -222,6 +271,5 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
this.closeModal(); this.closeModal();
} }
}); });
} }
} }

View File

@ -0,0 +1,24 @@
<!-- src/app/move-image-modal.component.html -->
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<div class="bg-white rounded-lg shadow-lg w-96 p-6">
<h2 class="text-xl font-semibold mb-4">Bild verschieben</h2>
<p class="mb-4">Wähle das Zieldeck für das Bild <strong>{{ image.name }}</strong> aus.</p>
<select [(ngModel)]="selectedDeckId" class="w-full p-2 border border-gray-300 rounded mb-4">
<option *ngFor="let deck of decks" [value]="deck.name" [disabled]="deck.name === sourceDeck.name">
{{ deck.name }}
</option>
</select>
<div class="flex justify-end space-x-2">
<button (click)="close()" class="bg-gray-500 text-white py-2 px-4 rounded hover:bg-gray-600">
Abbrechen
</button>
<button
(click)="moveImage()"
[disabled]="!selectedDeckId"
class="bg-yellow-500 text-white py-2 px-4 rounded hover:bg-yellow-600">
Verschieben
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,45 @@
// src/app/move-image-modal.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DeckImage, Deck, DeckService } from '../deck.service';
@Component({
selector: 'app-move-image-modal',
templateUrl: './move-image-modal.component.html',
standalone: true,
imports: [CommonModule, FormsModule]
})
export class MoveImageModalComponent {
@Input() image!: DeckImage;
@Input() sourceDeck!: Deck;
@Input() decks: Deck[] = [];
@Output() moveCompleted = new EventEmitter<void>();
@Output() closed = new EventEmitter<void>();
selectedDeckId: number | null = null;
constructor(private deckService: DeckService) { }
moveImage(): void {
if (this.selectedDeckId === null) {
return;
}
this.deckService.moveImage(this.image.id, this.selectedDeckId).subscribe({
next: () => {
this.moveCompleted.emit();
this.close();
},
error: (err) => {
console.error('Fehler beim Verschieben des Bildes:', err);
alert('Fehler beim Verschieben des Bildes.');
}
});
}
close(): void {
this.closed.emit();
}
}

View File

@ -14,7 +14,7 @@
<!-- Formular zum Hochladen --> <!-- Formular zum Hochladen -->
<div class="mb-4"> <div class="mb-4">
<label for="imageFile" class="block text-sm font-medium text-gray-700">Bild hochladen</label> <label for="imageFile" class="block text-sm font-medium text-gray-700">Bild hochladen</label>
<input type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> <input #imageFile type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
</div> </div>
<!-- Statusanzeige --> <!-- Statusanzeige -->

View File

@ -16,6 +16,7 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
@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('uploadImageModal') modalElement!: ElementRef;
@ViewChild('imageFile') imageFileElement!: ElementRef;
imageFile: File | null = null; imageFile: File | null = null;
processingStatus: string = ''; processingStatus: string = '';
@ -90,7 +91,7 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
}); });
const deckImage:DeckImage={name:bildname,id:bildid,boxes} const deckImage:DeckImage={name:bildname,id:bildid,boxes}
this.imageUploaded.emit({ imageSrc, deckImage }); this.imageUploaded.emit({ imageSrc, deckImage });
this.resetFileInput();
// Schließe das Upload-Modal // Schließe das Upload-Modal
this.closeModal(); this.closeModal();
} catch (error) { } catch (error) {
@ -101,4 +102,12 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
/**
* Setzt das Datei-Input-Feld zurück, sodass dieselbe Datei erneut ausgewählt werden kann.
*/
resetFileInput(): void {
if (this.imageFileElement && this.imageFileElement.nativeElement) {
this.imageFileElement.nativeElement.value = '';
}
}
} }