@page "/images" @attribute [Authorize] @rendermode InteractiveServer @inject GhostConfigService ConfigService @inject GhostApiService GhostApi @inject ImageAnalyzerService ImageAnalyzer @inject ImageReplacementService ReplacementService @inject ImageDeletionService DeletionService @inject ImageExportService ExportService @inject ImageDatabaseService DbService @inject IJSRuntime JsRuntime @inject ILogger Logger Ghost Bilder

Ghost Bilderverwaltung

@if (_instances.Count == 0) {
Keine Ghost-Instanzen konfiguriert. Bitte zuerst unter Einstellungen eine Instanz anlegen.
} else {
@if (FilteredImages.Any()) {
@if (SelectedOrphanedImages.Any()) { } }
@if (!string.IsNullOrEmpty(_errorMessage)) {
@_errorMessage
} @if (!string.IsNullOrEmpty(_successMessage)) {
@_successMessage
} @if (_allImages.Count > 0) {
Gefiltert: @_filteredImages.Count von @_allImages.Count @if (!string.IsNullOrWhiteSpace(_authorFilter) || !string.IsNullOrWhiteSpace(_themeFilter) || !string.IsNullOrWhiteSpace(_categoryFilter)) { @if (!string.IsNullOrWhiteSpace(_authorFilter)): @:Autor @if (!string.IsNullOrWhiteSpace(_themeFilter)): @:Thema @if (!string.IsNullOrWhiteSpace(_categoryFilter)): @:Kategorie }
@foreach (var image in FilteredImages) {
@if (image.IsOrphaned) {
}
@image.FileName
@image.FileName

Pfad: @image.RelativePath Größe: @image.FormattedFileSize
Dimensionen: @image.Dimensions
Datei-Datum: @image.LastModified.ToString("dd.MM.yyyy HH:mm")
@if (!string.IsNullOrEmpty(image.AuthorName)) { Autor: @image.AuthorName
} @if (!string.IsNullOrEmpty(image.Theme)) { Thema: @image.Theme
} @if (image.CaptureDate.HasValue) { Aufnahmedatum: @image.CaptureDate.Value.ToString("dd.MM.yyyy HH:mm")
} @if (!string.IsNullOrEmpty(image.CameraModel)) { Kamera: @image.CameraDisplay
@image.ExifSummary }

@if (image.IsOrphaned) {
⚠️ Verwaist (nicht in Posts verwendet)
} else {
Verwendet in:
    @foreach (var post in image.UsedInPosts.Take(3)) {
  • @(post.IsFeatureImage ? "Feature" : "Content") @post.Title
  • } @if (image.UsedInPosts.Count > 3) {
  • ... und @(image.UsedInPosts.Count - 3) weitere
  • }
}
@if (image.IsOrphaned) { }
}
} else if (!_isLoading && _hasLoaded) {
Keine Bilder gefunden.
} } @if (_showReplaceDialog && _selectedImage != null) { } @if (_showDeleteDialog && _selectedImage != null) { } @if (_showBulkDeleteDialog) { } @if (_showEditDialog && _editingImage != null) { } @code { private List _instances = new(); private string _selectedInstanceId = ""; private string _searchTerm = ""; private string _authorFilter = ""; private string _themeFilter = ""; private List _allImages = new(); private List _filteredImages = new(); 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 string _categoryFilter = ""; 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 _selectedImageUrls = new(); private bool _selectAllOrphaned; private List _ghostUsers = new(); private GhostImage? _editingImage; private bool _showEditDialog; private string? _editAuthorId; private string? _editTheme; private DateTime? _editCaptureDate; private bool _isSavingEdit; protected override void OnInitialized() { var config = ConfigService.Load(); _instances = config.Instances; _selectedInstanceId = _instances.FirstOrDefault()?.Id ?? ""; } private async Task OnInstanceChanged() { _allImages.Clear(); _filteredImages.Clear(); _hasLoaded = false; _selectedImageUrls.Clear(); _authorFilter = ""; _themeFilter = ""; 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); // Ghost-Benutzer laden _ghostUsers = await GhostApi.GetUsersAsync(instance); var filter = string.IsNullOrWhiteSpace(_searchTerm) ? "" : $"title:~'{EscapeFilter(_searchTerm)}'"; _allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance, filter); CalculateCounts(); ApplyFilters(); _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)); _categoryCount = _allImages.Count(i => !string.IsNullOrEmpty(i.Category)); } private List FilteredImages => _filteredImages; private List SelectedOrphanedImages => _filteredImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.IsOrphaned).ToList(); private List> AvailableAuthors => _allImages .Where(i => !string.IsNullOrEmpty(i.AuthorId)) .GroupBy(i => i.AuthorId!) .Select(g => new KeyValuePair(g.Key, g.First().AuthorName ?? "Unbekannt")) .OrderBy(kvp => kvp.Value) .ToList(); private List AvailableThemes => _allImages .Where(i => !string.IsNullOrEmpty(i.Theme)) .Select(i => i.Theme!) .Distinct() .OrderBy(t => t) .ToList(); private List> AvailableCategories => _allImages .Where(i => !string.IsNullOrEmpty(i.Category)) .GroupBy(i => i.Category!) .Select(g => new KeyValuePair(g.Key, g.Key)) .OrderBy(kvp => kvp.Value) .ToList(); private int GetAuthorCount(string authorId) => _allImages.Count(i => i.AuthorId == authorId); private int GetThemeCount(string theme) => _allImages.Count(i => i.Theme == theme); private int GetCategoryCount(string category) => _allImages.Count(i => i.Category == category); private void ApplyFilters() { 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)); if (!string.IsNullOrWhiteSpace(_authorFilter)) query = query.Where(i => i.AuthorId == _authorFilter); if (!string.IsNullOrWhiteSpace(_themeFilter)) query = query.Where(i => i.Theme == _themeFilter); if (!string.IsNullOrWhiteSpace(_categoryFilter)) query = query.Where(i => i.Category == _categoryFilter); _filteredImages = query.ToList(); StateHasChanged(); } private void ResetFilters() { _authorFilter = ""; _themeFilter = ""; _categoryFilter = ""; _searchTerm = ""; _currentFilter = ImageFilter.All; ApplyFilters(); } private void SetFilter(ImageFilter filter) { _currentFilter = filter; ApplyFilters(); } 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); if (_selectedImage.ManagedImageId.HasValue) await DbService.DeleteAsync(_selectedImage.ManagedImageId.Value); _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}"; } } 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(); } } 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, Category } }