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" />
</svg>
</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>
</li>
</ul>
@ -83,4 +89,13 @@
<app-edit-image-modal *ngIf="imageData" [deckName]="currentUploadDeckName" [imageData]="imageData" (imageSaved)="onImageSaved()" (closed)="onClosed()"></app-edit-image-modal>
<!-- TrainingComponent -->
<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>

View File

@ -7,6 +7,7 @@ import { TrainingComponent } from './training/training.component';
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component';
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
import { firstValueFrom } from 'rxjs';
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
@Component({
selector: 'app-deck-list',
@ -18,6 +19,7 @@ import { firstValueFrom } from 'rxjs';
UploadImageModalComponent,
TrainingComponent,
EditImageModalComponent,
MoveImageModalComponent, // Hinzufügen der neuen Komponente
UploadImageModalComponent
]
})
@ -30,13 +32,16 @@ export class DeckListComponent implements OnInit {
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
@ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent;
imageData: { imageSrc: string | ArrayBuffer | null, deckImage:DeckImage} | null = null;
imageData: { imageSrc: string | ArrayBuffer | null, deckImage: DeckImage } | null = null;
currentUploadDeckName: string = '';
// Set zur Verfolgung erweiterter Decks
expandedDecks: Set<number> = new Set<number>();
// State für das Verschieben von Bildern
imageToMove: { image: DeckImage, sourceDeck: Deck } | null = null;
constructor(private deckService: DeckService) { }
ngOnInit(): void {
@ -73,8 +78,10 @@ export class DeckListComponent implements OnInit {
error: (err) => console.error('Fehler beim Löschen des Bildes', err)
});
}
editImage(deck: Deck, image: DeckImage): void {
let imageSrc = null
this.currentUploadDeckName = deck.name;
fetch(`/api/debug_image/${image.id}/original_compressed.jpg`)
.then(response => {
if (!response.ok) {
@ -86,7 +93,7 @@ export class DeckListComponent implements OnInit {
const reader = new FileReader();
reader.onloadend = () => {
imageSrc = reader.result; // Base64-String
this.imageData = {imageSrc,deckImage:image}
this.imageData = { imageSrc, deckImage: image }
};
reader.readAsDataURL(blob);
})
@ -94,6 +101,7 @@ export class DeckListComponent implements OnInit {
console.error('Fehler beim Laden des Bildes:', error);
});
}
openTraining(deck: Deck): void {
this.selectedDeck = deck;
}
@ -139,18 +147,8 @@ export class DeckListComponent implements OnInit {
console.error('Fehler beim Parsen der erweiterten Decks aus sessionStorage', e);
}
} 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>();
// 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 {
this.imageData = imageData;
}
onClosed(){
this.imageData = null;
}
async onImageSaved() {
// Handle das Speichern der Bilddaten, z.B. aktualisiere die Liste der Bilder
this.imageData = null;
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> {
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> {
return this.http.post(this.apiUrl, { deckname });
}
@ -130,4 +105,9 @@ export class DeckService {
deleteImage(imageName: string): Observable<any> {
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>
</button>
<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">
<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>
<!-- Erkannten Text anzeigen -->
<!-- <div *ngIf="detectedText" class="mt-4">
<h4 class="text-lg font-medium text-gray-700">Erkannter Text:</h4>
<pre class="whitespace-pre-wrap text-sm text-gray-600">{{ detectedText }}</pre>
</div> -->
<!-- Buttons unter dem Canvas -->
<div class="mt-4 flex justify-between">
<button (click)="save()" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
Save
</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>

View File

@ -12,6 +12,9 @@ import { DeckImage, DeckService, OcrResult } from '../deck.service';
imports: [CommonModule]
})
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() imageData : {imageSrc:string|ArrayBuffer|null, deckImage:DeckImage|null} = {imageSrc:null,deckImage:null};
@Output() imageSaved = new EventEmitter<void>();
@ -87,7 +90,7 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
async processImage(): Promise<void> {
try {
if (!this.imageData){
return
return;
}
this.canvas = new fabric.Canvas(this.canvasElement.nativeElement);
@ -123,9 +126,9 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
const rect = new fabric.Rect({
left: box.x1 * scaleFactor,
top: box.y1 * scaleFactor,
width: (box.x2-box.x1) * scaleFactor,
height: (box.y2-box.y1) * scaleFactor,
fill: 'rgba(255, 0, 0, 0.3)',
width: (box.x2 - box.x1) * scaleFactor,
height: (box.y2 - box.y1) * scaleFactor,
fill: this.BOX_COLOR, // Verwendung der Konstante
selectable: true,
hasControls: true,
hasBorders: true,
@ -202,12 +205,58 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
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 {
// Hier implementierst du die Logik zum Speichern der Bilddaten
// Zum Beispiel über einen Service oder direkt hier
const data = {
deckname: this.deckName,
bildname: this.imageData.deckImage?.name,//this.imageFile?.name,
bildname: this.imageData.deckImage?.name, // this.imageFile?.name,
bildid: this.imageData.deckImage?.id,
boxes: this.boxes,
};
@ -222,6 +271,5 @@ export class EditImageModalComponent implements AfterViewInit, OnDestroy {
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 -->
<div class="mb-4">
<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>
<!-- Statusanzeige -->

View File

@ -16,6 +16,7 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
@Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage:DeckImage }>();
@ViewChild('uploadImageModal') modalElement!: ElementRef;
@ViewChild('imageFile') imageFileElement!: ElementRef;
imageFile: File | null = null;
processingStatus: string = '';
@ -90,7 +91,7 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
});
const deckImage:DeckImage={name:bildname,id:bildid,boxes}
this.imageUploaded.emit({ imageSrc, deckImage });
this.resetFileInput();
// Schließe das Upload-Modal
this.closeModal();
} catch (error) {
@ -101,4 +102,12 @@ export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
};
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 = '';
}
}
}