@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 (SelectedDeletableImages.Any()) { } }
@if (!string.IsNullOrEmpty(_errorMessage)) {
@_errorMessage
} @if (!string.IsNullOrEmpty(_warningMessage)) {
@_warningMessage
} @if (!string.IsNullOrEmpty(_successMessage)) {
@_successMessage
} @if (_allImages.Count > 0) {
Gefiltert: @_filteredImages.Count von @_allImages.Count
@foreach (var image in FilteredImages) {
@if (image.CanDelete) {
}
@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.IsOriginal) {
Originaldatei von @image.OriginalOf @(image.IsOrphaned ? "(Bild wird nicht verwendet)" : "(Bild wird verwendet)")
} else if (image.IsOrphaned) {
⚠️ Verwaist (nicht in Posts verwendet)
} else {
Verwendet in:
    @foreach (var post in image.UsedInPosts.Take(3)) {
  • @(post.IsFeatureImage ? "Feature" : post.IsInContent ? "Content" : "Website") @post.Title
  • } @if (image.UsedInPosts.Count > 3) {
  • ... und @(image.UsedInPosts.Count - 3) weitere
  • }
}
@if (image.CanDelete) { }
}
} 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 _nameSearch = ""; private string? _warningMessage; private int _originalCount; private int _categoryCount; private bool _selectAllOriginals; 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); _allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance); _warningMessage = ImageAnalyzer.LastWarning; 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)); _originalCount = _allImages.Count(i => i.IsOriginal); } private List FilteredImages => _filteredImages; private List SelectedDeletableImages => _filteredImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.CanDelete).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)); else if (_currentFilter == ImageFilter.Original) query = query.Where(i => i.IsOriginal); else if (_currentFilter == ImageFilter.Category) query = query.Where(i => !string.IsNullOrEmpty(i.Category)); if (!string.IsNullOrWhiteSpace(_nameSearch)) { var name = _nameSearch.Trim(); query = query.Where(i => i.FileName.Contains(name, StringComparison.OrdinalIgnoreCase) || i.RelativePath.Contains(name, StringComparison.OrdinalIgnoreCase)); } if (!string.IsNullOrWhiteSpace(_searchTerm)) { var title = _searchTerm.Trim(); query = query.Where(i => i.UsedInPosts.Any(r => r.Title.Contains(title, StringComparison.OrdinalIgnoreCase))); } 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 = ""; _nameSearch = ""; _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; _selectAllOriginals = false; StateHasChanged(); } // Wirkt auf die aktuell angezeigten (gefilterten) Bilder private void ToggleSelectAllOrphaned(ChangeEventArgs e) { _selectAllOrphaned = e.Value is bool b && b; SetSelection(_filteredImages.Where(i => i.IsOrphaned), _selectAllOrphaned); } private void ToggleSelectAllOriginals(ChangeEventArgs e) { _selectAllOriginals = e.Value is bool b && b; SetSelection(_filteredImages.Where(i => i.IsOriginal), _selectAllOriginals); } private void SetSelection(IEnumerable images, bool selected) { foreach (var image in images) { if (selected) _selectedImageUrls.Add(image.Url); else _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 DeleteSelected() { if (SelectedDeletableImages.Any()) ShowBulkDeleteDialog(); } private async Task DeleteSelectedConfirmed() { _isDeleting = true; StateHasChanged(); try { int success = 0, failed = 0; foreach (var image in SelectedDeletableImages) { if (await DeletionService.DeleteImageAsync(image)) { success++; if (image.ManagedImageId.HasValue) await DbService.DeleteAsync(image.ManagedImageId.Value); } else { failed++; } } _selectedImageUrls.Clear(); _selectAllOrphaned = false; _selectAllOriginals = false; await LoadImages(); _successMessage = $"{success} Dateien gelöscht, {failed} fehlgeschlagen."; } 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); var deletedName = _selectedImage.FileName; CloseDeleteDialog(); await LoadImages(); _successMessage = $"{deletedName} gelöscht."; } 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 enum ImageFilter { All, Feature, Content, Orphaned, Original, Category } }