Wp2ghost/GhostImageManager/Components/Pages/Images.razor

864 lines
33 KiB
Text
Raw Permalink Normal View History

2026-09-24 19:23:54 +02:00
@page "/images"
@attribute [Authorize]
@rendermode InteractiveServer
@inject GhostConfigService ConfigService
2026-09-28 19:09:09 +02:00
@inject GhostApiService GhostApi
2026-09-24 19:23:54 +02:00
@inject ImageAnalyzerService ImageAnalyzer
@inject ImageReplacementService ReplacementService
@inject ImageDeletionService DeletionService
@inject ImageExportService ExportService
2026-09-28 19:09:09 +02:00
@inject ImageDatabaseService DbService
2026-09-24 19:23:54 +02:00
@inject IJSRuntime JsRuntime
@inject ILogger<Images> Logger
<PageTitle>Ghost Bilder</PageTitle>
<h1>Ghost Bilderverwaltung</h1>
@if (_instances.Count == 0)
{
<div class="alert alert-warning">
Keine Ghost-Instanzen konfiguriert. Bitte zuerst unter <a href="settings">Einstellungen</a> eine Instanz anlegen.
</div>
}
else
{
<div class="row mb-3">
2026-09-28 19:09:09 +02:00
<div class="col-md-2">
2026-09-24 19:23:54 +02:00
<label class="form-label">Ghost-Instanz</label>
<select class="form-select" @bind="_selectedInstanceId" @bind:after="OnInstanceChanged">
@foreach (var instance in _instances)
{
<option value="@instance.Id">@instance.Name</option>
}
</select>
</div>
2026-09-28 19:09:09 +02:00
<div class="col-md-2">
2026-09-24 19:23:54 +02:00
<label class="form-label">Suche nach Post</label>
<div class="input-group">
<input type="text" class="form-control" @bind="_searchTerm" @bind:event="oninput" placeholder="Post-Titel..." />
<button class="btn btn-outline-secondary" @onclick="LoadImages" disabled="@_isLoading">Suchen</button>
</div>
</div>
2026-09-28 19:09:09 +02:00
<div class="col-md-2">
<label class="form-label">Autor</label>
<select class="form-select" @bind="_authorFilter" @bind:after="ApplyFilters">
<option value="">Alle Autoren</option>
@foreach (var author in AvailableAuthors)
{
<option value="@author.Key">@author.Value (@GetAuthorCount(author.Key))</option>
}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Thema</label>
<select class="form-select" @bind="_themeFilter" @bind:after="ApplyFilters">
<option value="">Alle Themen</option>
@foreach (var theme in AvailableThemes)
{
<option value="@theme">@theme (@GetThemeCount(theme))</option>
}
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button class="btn btn-outline-secondary me-2" @onclick="ResetFilters" title="Filter zurücksetzen">
<i class="bi bi-x-circle"></i>
</button>
2026-09-24 19:23:54 +02:00
<button class="btn btn-primary me-2" @onclick="LoadImages" disabled="@_isLoading">
@if (_isLoading)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Lade...</span>
}
else
{
<span>Alle Bilder laden</span>
}
</button>
@if (FilteredImages.Any())
{
<div class="btn-group me-2">
<button class="btn btn-outline-success dropdown-toggle" data-bs-toggle="dropdown">
Exportieren
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" @onclick="ExportCsv">CSV exportieren</button></li>
<li><button class="dropdown-item" @onclick="ExportTxt">TXT exportieren</button></li>
<li><button class="dropdown-item" @onclick="ExportSelectedZip">Ausgewählte als ZIP</button></li>
</ul>
</div>
@if (SelectedOrphanedImages.Any())
{
<button class="btn btn-danger me-2" @onclick="DeleteSelectedOrphaned">
Auswahl löschen (@SelectedOrphanedImages.Count)
</button>
}
}
</div>
</div>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger">@_errorMessage</div>
}
@if (!string.IsNullOrEmpty(_successMessage))
{
<div class="alert alert-success">@_successMessage</div>
}
@if (_allImages.Count > 0)
{
<div class="row mb-3 align-items-center">
<div class="col">
<div class="btn-group" role="group">
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.All)" @onclick="() => SetFilter(ImageFilter.All)">Alle (@_allImages.Count)</button>
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Feature)" @onclick="() => SetFilter(ImageFilter.Feature)">Feature (@_featureCount)</button>
<button class="btn btn-outline-secondary @GetFilterClass(ImageFilter.Content)" @onclick="() => SetFilter(ImageFilter.Content)">Content (@_contentCount)</button>
<button class="btn btn-outline-danger @GetFilterClass(ImageFilter.Orphaned)" @onclick="() => SetFilter(ImageFilter.Orphaned)">Verwaist (@_orphanedCount)</button>
</div>
</div>
2026-09-28 19:09:09 +02:00
<div class="col-auto">
<span class="text-muted small">
Gefiltert: <strong>@_filteredImages.Count</strong> von @_allImages.Count
@if (!string.IsNullOrWhiteSpace(_authorFilter) || !string.IsNullOrWhiteSpace(_themeFilter))
{
<span class="ms-2">
(@(!string.IsNullOrWhiteSpace(_authorFilter) ? "Autor" : "")
@(!string.IsNullOrWhiteSpace(_authorFilter) && !string.IsNullOrWhiteSpace(_themeFilter) ? " + " : "")
@(!string.IsNullOrWhiteSpace(_themeFilter) ? "Thema" : ""))
</span>
}
</span>
</div>
2026-09-24 19:23:54 +02:00
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllOrphaned" @onchange="ToggleSelectAllOrphaned" checked="@_selectAllOrphaned" />
<label class="form-check-label" for="selectAllOrphaned">Alle verwaisten auswählen</label>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
@foreach (var image in FilteredImages)
{
<div class="col">
<div class="card h-100 @GetCardClass(image)">
@if (image.IsOrphaned)
{
<div class="card-header py-1">
<div class="form-check">
<input class="form-check-input" type="checkbox" checked="@_selectedImageUrls.Contains(image.Url)" @onchange="e => ToggleSelection(image, e)" id="chk-@image.GetHashCode()" />
<label class="form-check-label small" for="chk-@image.GetHashCode()">Auswählen</label>
</div>
</div>
}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 200px;">
<img src="@image.Url" alt="@image.FileName" class="img-fluid" style="max-height: 100%; max-width: 100%; object-fit: contain;" loading="lazy" />
</div>
<div class="card-body">
<h5 class="card-title text-truncate" title="@image.FileName">@image.FileName</h5>
<p class="card-text small text-muted">
<strong>Pfad:</strong> <span class="text-truncate d-block" title="@image.RelativePath">@image.RelativePath</span>
<strong>Größe:</strong> @image.FormattedFileSize<br />
<strong>Dimensionen:</strong> @image.Dimensions<br />
2026-09-28 19:09:09 +02:00
<strong>Datei-Datum:</strong> @image.LastModified.ToString("dd.MM.yyyy HH:mm")<br />
@if (!string.IsNullOrEmpty(image.AuthorName))
{
<strong>Autor:</strong> @image.AuthorName<br />
}
@if (!string.IsNullOrEmpty(image.Theme))
{
<strong>Thema:</strong> @image.Theme<br />
}
@if (image.CaptureDate.HasValue)
{
<strong>Aufnahmedatum:</strong> @image.CaptureDate.Value.ToString("dd.MM.yyyy HH:mm")<br />
}
@if (!string.IsNullOrEmpty(image.CameraModel))
{
<strong>Kamera:</strong> @image.CameraDisplay<br />
<span class="d-block">@image.ExifSummary</span>
}
2026-09-24 19:23:54 +02:00
</p>
@if (image.IsOrphaned)
{
<div class="alert alert-warning py-1 px-2 mb-2 small">
⚠️ Verwaist (nicht in Posts verwendet)
</div>
}
else
{
<div class="mb-2">
<small class="text-muted">Verwendet in:</small>
<ul class="list-unstyled small mb-0">
@foreach (var post in image.UsedInPosts.Take(3))
{
<li>
<span class="badge @(post.IsFeatureImage ? "bg-primary" : "bg-secondary") me-1">
@(post.IsFeatureImage ? "Feature" : "Content")
</span>
<span title="@post.Title">@post.Title</span>
</li>
}
@if (image.UsedInPosts.Count > 3)
{
<li class="text-muted">... und @(image.UsedInPosts.Count - 3) weitere</li>
}
</ul>
</div>
}
2026-09-28 19:09:09 +02:00
<div class="d-flex gap-2 flex-wrap">
2026-09-24 19:23:54 +02:00
<button class="btn btn-sm btn-outline-primary" @onclick="() => ShowReplaceDialog(image)">
Tauschen
</button>
2026-09-28 19:09:09 +02:00
<button class="btn btn-sm btn-outline-secondary" @onclick="() => ShowEditDialog(image)">
Bearbeiten
</button>
2026-09-24 19:23:54 +02:00
@if (image.IsOrphaned)
{
<button class="btn btn-sm btn-outline-danger" @onclick="() => ShowDeleteDialog(image)">
Löschen
</button>
}
</div>
</div>
</div>
</div>
}
</div>
}
else if (!_isLoading && _hasLoaded)
{
<div class="alert alert-info">Keine Bilder gefunden.</div>
}
}
@if (_showReplaceDialog && _selectedImage != null)
{
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bild tauschen: @_selectedImage.FileName</h5>
<button type="button" class="btn-close" @onclick="CloseReplaceDialog"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h6>Aktuelles Bild</h6>
<img src="@_selectedImage.Url" class="img-fluid border" alt="Aktuell" />
<p class="small text-muted mt-2">
@_selectedImage.FormattedFileSize | @_selectedImage.Dimensions
</p>
</div>
<div class="col-md-6">
<h6>Neues Bild</h6>
<InputFile OnChange="LoadNewImage" class="form-control" accept="image/*" />
@if (_newImagePreview != null)
{
<img src="@_newImagePreview" class="img-fluid border mt-2" alt="Vorschau" />
}
</div>
</div>
@if (!string.IsNullOrEmpty(_replaceError))
{
<div class="alert alert-danger mt-3">@_replaceError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseReplaceDialog">Abbrechen</button>
<button type="button" class="btn btn-primary" @onclick="ReplaceImage" disabled="@(_newImageFile == null || _isReplacing)">
@if (_isReplacing)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Tausche...</span>
}
else
{
<span>Hochladen & Ersetzen</span>
}
</button>
</div>
</div>
</div>
</div>
}
@if (_showDeleteDialog && _selectedImage != null)
{
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bild löschen</h5>
<button type="button" class="btn-close" @onclick="CloseDeleteDialog"></button>
</div>
<div class="modal-body">
<p>Möchtest du wirklich <strong>@_selectedImage.FileName</strong> löschen?</p>
<div class="alert alert-warning">
⚠️ Dieses Bild ist verwaist und wird in keinem Post verwendet.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseDeleteDialog">Abbrechen</button>
<button type="button" class="btn btn-danger" @onclick="DeleteImage">
Löschen
</button>
</div>
</div>
</div>
</div>
}
@if (_showBulkDeleteDialog)
{
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ausgewählte Bilder löschen</h5>
<button type="button" class="btn-close" @onclick="CloseBulkDeleteDialog"></button>
</div>
<div class="modal-body">
<p>Möchtest du wirklich <strong>@SelectedOrphanedImages.Count</strong> verwaiste Bilder löschen?</p>
<ul class="list-group small">
@foreach (var img in SelectedOrphanedImages.Take(10))
{
<li class="list-group-item">@img.FileName</li>
}
@if (SelectedOrphanedImages.Count > 10)
{
<li class="list-group-item text-muted">... und @(SelectedOrphanedImages.Count - 10) weitere</li>
}
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseBulkDeleteDialog">Abbrechen</button>
<button type="button" class="btn btn-danger" @onclick="DeleteSelectedOrphanedConfirmed" disabled="@_isDeleting">
@if (_isDeleting)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Lösche...</span>
}
else
{
<span>Alle löschen</span>
}
</button>
</div>
</div>
2026-09-28 19:09:09 +02:00
</div>
</div>
}
@if (_showEditDialog && _editingImage != null)
{
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Bild bearbeiten: @_editingImage.FileName</h5>
<button type="button" class="btn-close" @onclick="CloseEditDialog"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<img src="@_editingImage.Url" class="img-fluid border" alt="@_editingImage.FileName" />
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Autor</label>
<select class="form-select" @bind="_editAuthorId">
<option value="">(keiner)</option>
@foreach (var user in _ghostUsers)
{
<option value="@user.Id">@user.Name</option>
}
</select>
</div>
<div class="mb-3">
<label class="form-label">Thema</label>
<input type="text" class="form-control" @bind="_editTheme" placeholder="z.B. Landschaft, Portrait..." />
</div>
<div class="mb-3">
<label class="form-label">Aufnahmedatum</label>
<InputDate class="form-control" @bind-Value="_editCaptureDate" />
</div>
<div class="alert alert-info small">
<strong>Kamera:</strong> @_editingImage.CameraDisplay<br />
<strong>EXIF:</strong> @_editingImage.ExifSummary<br />
<strong>Dateigröße:</strong> @_editingImage.FormattedFileSize | @_editingImage.Dimensions
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CloseEditDialog">Abbrechen</button>
<button type="button" class="btn btn-primary" @onclick="SaveEdit" disabled="@_isSavingEdit">
@if (_isSavingEdit)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Speichere...</span>
}
else
{
<span>Speichern</span>
}
</button>
</div>
</div>
2026-09-24 19:23:54 +02:00
</div>
</div>
}
@code {
private List<GhostInstance> _instances = new();
private string _selectedInstanceId = "";
private string _searchTerm = "";
2026-09-28 19:09:09 +02:00
private string _authorFilter = "";
private string _themeFilter = "";
2026-09-24 19:23:54 +02:00
private List<GhostImage> _allImages = new();
2026-09-28 19:09:09 +02:00
private List<GhostImage> _filteredImages = new();
2026-09-24 19:23:54 +02:00
private bool _isLoading;
private bool _hasLoaded;
private string? _errorMessage;
private string? _successMessage;
private ImageFilter _currentFilter = ImageFilter.All;
private int _orphanedCount;
private int _featureCount;
private int _contentCount;
private GhostImage? _selectedImage;
private bool _showReplaceDialog;
private bool _showDeleteDialog;
private bool _showBulkDeleteDialog;
private IBrowserFile? _newImageFile;
private string? _newImagePreview;
private string? _replaceError;
private bool _isReplacing;
private bool _isDeleting;
private HashSet<string> _selectedImageUrls = new();
private bool _selectAllOrphaned;
2026-09-28 19:09:09 +02:00
private List<GhostUser> _ghostUsers = new();
private GhostImage? _editingImage;
private bool _showEditDialog;
private string? _editAuthorId;
private string? _editTheme;
private DateTime? _editCaptureDate;
private bool _isSavingEdit;
2026-09-24 19:23:54 +02:00
protected override void OnInitialized()
{
var config = ConfigService.Load();
_instances = config.Instances;
_selectedInstanceId = _instances.FirstOrDefault()?.Id ?? "";
}
private async Task OnInstanceChanged()
{
_allImages.Clear();
2026-09-28 19:09:09 +02:00
_filteredImages.Clear();
2026-09-24 19:23:54 +02:00
_hasLoaded = false;
_selectedImageUrls.Clear();
2026-09-28 19:09:09 +02:00
_authorFilter = "";
_themeFilter = "";
2026-09-24 19:23:54 +02:00
await LoadImages();
}
private async Task LoadImages()
{
if (string.IsNullOrEmpty(_selectedInstanceId))
return;
_isLoading = true;
_errorMessage = null;
_successMessage = null;
StateHasChanged();
try
{
var instance = _instances.First(i => i.Id == _selectedInstanceId);
2026-09-28 19:09:09 +02:00
// Ghost-Benutzer laden
_ghostUsers = await GhostApi.GetUsersAsync(instance);
2026-09-24 19:23:54 +02:00
var filter = string.IsNullOrWhiteSpace(_searchTerm) ? "" : $"title:~'{EscapeFilter(_searchTerm)}'";
_allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance, filter);
CalculateCounts();
2026-09-28 19:09:09 +02:00
ApplyFilters();
2026-09-24 19:23:54 +02:00
_hasLoaded = true;
}
catch (Exception ex)
{
Logger.LogError(ex, "Fehler beim Laden der Bilder");
_errorMessage = $"Fehler: {ex.Message}";
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private void CalculateCounts()
{
_orphanedCount = _allImages.Count(i => i.IsOrphaned);
_featureCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsFeatureImage));
_contentCount = _allImages.Count(i => i.UsedInPosts.Any(r => r.IsInContent));
}
2026-09-28 19:09:09 +02:00
private List<GhostImage> FilteredImages => _filteredImages;
private List<GhostImage> SelectedOrphanedImages => _filteredImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.IsOrphaned).ToList();
private List<KeyValuePair<string, string>> AvailableAuthors => _allImages
.Where(i => !string.IsNullOrEmpty(i.AuthorId))
.GroupBy(i => i.AuthorId!)
.Select(g => new KeyValuePair<string, string>(g.Key, g.First().AuthorName ?? "Unbekannt"))
.OrderBy(kvp => kvp.Value)
.ToList();
private List<string> AvailableThemes => _allImages
.Where(i => !string.IsNullOrEmpty(i.Theme))
.Select(i => i.Theme!)
.Distinct()
.OrderBy(t => t)
.ToList();
private int GetAuthorCount(string authorId) => _allImages.Count(i => i.AuthorId == authorId);
private int GetThemeCount(string theme) => _allImages.Count(i => i.Theme == theme);
private void ApplyFilters()
2026-09-24 19:23:54 +02:00
{
2026-09-28 19:09:09 +02:00
var query = _allImages.AsEnumerable();
if (_currentFilter == ImageFilter.Orphaned)
query = query.Where(i => i.IsOrphaned);
else if (_currentFilter == ImageFilter.Feature)
query = query.Where(i => i.UsedInPosts.Any(r => r.IsFeatureImage));
else if (_currentFilter == ImageFilter.Content)
query = query.Where(i => i.UsedInPosts.Any(r => r.IsInContent));
2026-09-24 19:23:54 +02:00
2026-09-28 19:09:09 +02:00
if (!string.IsNullOrWhiteSpace(_authorFilter))
query = query.Where(i => i.AuthorId == _authorFilter);
if (!string.IsNullOrWhiteSpace(_themeFilter))
query = query.Where(i => i.Theme == _themeFilter);
_filteredImages = query.ToList();
StateHasChanged();
}
private void ResetFilters()
{
_authorFilter = "";
_themeFilter = "";
_searchTerm = "";
_currentFilter = ImageFilter.All;
ApplyFilters();
}
2026-09-24 19:23:54 +02:00
private void SetFilter(ImageFilter filter)
{
_currentFilter = filter;
2026-09-28 19:09:09 +02:00
ApplyFilters();
2026-09-24 19:23:54 +02:00
}
private string GetFilterClass(ImageFilter filter) => _currentFilter == filter ? "active" : "";
private string GetCardClass(GhostImage image) => image.IsOrphaned ? "border-danger" : "";
private void ToggleSelection(GhostImage image, ChangeEventArgs e)
{
var isSelected = e.Value is bool b && b;
if (isSelected)
_selectedImageUrls.Add(image.Url);
else
_selectedImageUrls.Remove(image.Url);
_selectAllOrphaned = false;
StateHasChanged();
}
private void ToggleSelectAllOrphaned(ChangeEventArgs e)
{
_selectAllOrphaned = e.Value is bool b && b;
if (_selectAllOrphaned)
{
foreach (var image in _allImages.Where(i => i.IsOrphaned))
_selectedImageUrls.Add(image.Url);
}
else
{
foreach (var image in _allImages.Where(i => i.IsOrphaned))
_selectedImageUrls.Remove(image.Url);
}
StateHasChanged();
}
private void ShowReplaceDialog(GhostImage image)
{
_selectedImage = image;
_showReplaceDialog = true;
_newImageFile = null;
_newImagePreview = null;
_replaceError = null;
}
private void CloseReplaceDialog()
{
_showReplaceDialog = false;
_selectedImage = null;
_newImageFile = null;
_newImagePreview = null;
_replaceError = null;
}
private void ShowDeleteDialog(GhostImage image)
{
_selectedImage = image;
_showDeleteDialog = true;
}
private void CloseDeleteDialog()
{
_showDeleteDialog = false;
_selectedImage = null;
}
private void ShowBulkDeleteDialog()
{
_showBulkDeleteDialog = true;
}
private void CloseBulkDeleteDialog()
{
_showBulkDeleteDialog = false;
}
private void DeleteSelectedOrphaned()
{
if (SelectedOrphanedImages.Any())
ShowBulkDeleteDialog();
}
private async Task DeleteSelectedOrphanedConfirmed()
{
_isDeleting = true;
StateHasChanged();
try
{
var result = await DeletionService.DeleteImagesAsync(SelectedOrphanedImages);
_selectedImageUrls.Clear();
_selectAllOrphaned = false;
_successMessage = $"{result.Success} Bilder gelöscht, {result.Failed} fehlgeschlagen.";
await LoadImages();
}
catch (Exception ex)
{
_errorMessage = $"Fehler beim Löschen: {ex.Message}";
}
finally
{
_isDeleting = false;
_showBulkDeleteDialog = false;
StateHasChanged();
}
}
private async Task DeleteImage()
{
if (_selectedImage == null) return;
try
{
var success = await DeletionService.DeleteImageAsync(_selectedImage);
if (success)
{
_selectedImageUrls.Remove(_selectedImage.Url);
2026-09-28 19:09:09 +02:00
if (_selectedImage.ManagedImageId.HasValue)
await DbService.DeleteAsync(_selectedImage.ManagedImageId.Value);
2026-09-24 19:23:54 +02:00
_successMessage = $"{_selectedImage.FileName} gelöscht.";
CloseDeleteDialog();
await LoadImages();
}
else
{
_errorMessage = "Bild konnte nicht gelöscht werden.";
}
}
catch (Exception ex)
{
_errorMessage = $"Fehler beim Löschen: {ex.Message}";
}
}
2026-09-28 19:09:09 +02:00
private void ShowEditDialog(GhostImage image)
{
_editingImage = image;
_editAuthorId = image.AuthorId;
_editTheme = image.Theme;
_editCaptureDate = image.CaptureDate;
_showEditDialog = true;
}
private void CloseEditDialog()
{
_showEditDialog = false;
_editingImage = null;
_editAuthorId = null;
_editTheme = null;
_editCaptureDate = null;
}
private async Task SaveEdit()
{
if (_editingImage?.ManagedImageId == null) return;
_isSavingEdit = true;
StateHasChanged();
try
{
var selectedAuthor = _ghostUsers.FirstOrDefault(u => u.Id == _editAuthorId);
await DbService.UpdateAuthorAsync(_editingImage.ManagedImageId.Value, _editAuthorId, selectedAuthor?.Name);
await DbService.UpdateThemeAsync(_editingImage.ManagedImageId.Value, _editTheme);
await DbService.UpdateCaptureDateAsync(_editingImage.ManagedImageId.Value, _editCaptureDate);
// Lokale Anzeige aktualisieren
_editingImage.AuthorId = _editAuthorId;
_editingImage.AuthorName = selectedAuthor?.Name;
_editingImage.Theme = _editTheme;
_editingImage.CaptureDate = _editCaptureDate;
_successMessage = $"{_editingImage.FileName} aktualisiert.";
CloseEditDialog();
}
catch (Exception ex)
{
Logger.LogError(ex, "Fehler beim Speichern der Bild-Metadaten");
_errorMessage = $"Fehler beim Speichern: {ex.Message}";
}
finally
{
_isSavingEdit = false;
StateHasChanged();
}
}
2026-09-24 19:23:54 +02:00
private async Task LoadNewImage(InputFileChangeEventArgs e)
{
_newImageFile = e.File;
_replaceError = null;
try
{
using var stream = _newImageFile.OpenReadStream(maxAllowedSize: 104857600);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var bytes = ms.ToArray();
_newImagePreview = $"data:{_newImageFile.ContentType};base64,{Convert.ToBase64String(bytes)}";
}
catch (Exception ex)
{
_replaceError = $"Fehler beim Lesen der Datei: {ex.Message}";
}
StateHasChanged();
}
private async Task ReplaceImage()
{
if (_selectedImage == null || _newImageFile == null)
return;
_isReplacing = true;
_replaceError = null;
StateHasChanged();
try
{
using var stream = _newImageFile.OpenReadStream(maxAllowedSize: 104857600);
var success = await ReplacementService.ReplaceImageAsync(_selectedImage, stream, _newImageFile.Name);
if (success)
{
await LoadImages();
CloseReplaceDialog();
}
else
{
_replaceError = "Bild konnte nicht ersetzt werden.";
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Fehler beim Tauschen des Bildes");
_replaceError = $"Fehler: {ex.Message}";
}
finally
{
_isReplacing = false;
StateHasChanged();
}
}
private async Task ExportCsv()
{
var data = ExportService.ExportToCsv(FilteredImages);
await DownloadFileAsync($"ghost-images-{DateTime.Now:yyyyMMdd-HHmm}.csv", data, "text/csv");
}
private async Task ExportTxt()
{
var data = ExportService.ExportToTxt(FilteredImages);
await DownloadFileAsync($"ghost-images-{DateTime.Now:yyyyMMdd-HHmm}.txt", data, "text/plain");
}
private async Task ExportSelectedZip()
{
var selectedImages = _allImages.Where(i => _selectedImageUrls.Contains(i.Url)).ToList();
if (!selectedImages.Any())
{
_errorMessage = "Keine Bilder ausgewählt.";
return;
}
var data = await ExportService.ExportToZipAsync(selectedImages);
await DownloadFileAsync($"ghost-images-{DateTime.Now:yyyyMMdd-HHmm}.zip", data, "application/zip");
}
private async Task DownloadFileAsync(string fileName, byte[] data, string contentType)
{
var base64 = Convert.ToBase64String(data);
await JsRuntime.InvokeVoidAsync("downloadFileFromBase64", fileName, base64, contentType);
}
private static string EscapeFilter(string term)
{
return term.Replace("'", "\\'");
}
private enum ImageFilter
{
All,
Feature,
Content,
Orphaned
}
}