625 lines
24 KiB
Text
625 lines
24 KiB
Text
@page "/images"
|
|
@attribute [Authorize]
|
|
@rendermode InteractiveServer
|
|
@inject GhostConfigService ConfigService
|
|
@inject ImageAnalyzerService ImageAnalyzer
|
|
@inject ImageReplacementService ReplacementService
|
|
@inject ImageDeletionService DeletionService
|
|
@inject ImageExportService ExportService
|
|
@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">
|
|
<div class="col-md-3">
|
|
<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>
|
|
<div class="col-md-3">
|
|
<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>
|
|
<div class="col-md-6 d-flex align-items-end">
|
|
<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>
|
|
<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 />
|
|
<strong>Datum:</strong> @image.LastModified.ToString("dd.MM.yyyy HH:mm")
|
|
</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>
|
|
}
|
|
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-primary" @onclick="() => ShowReplaceDialog(image)">
|
|
Tauschen
|
|
</button>
|
|
@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>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private List<GhostInstance> _instances = new();
|
|
private string _selectedInstanceId = "";
|
|
private string _searchTerm = "";
|
|
private List<GhostImage> _allImages = 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 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;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
var config = ConfigService.Load();
|
|
_instances = config.Instances;
|
|
_selectedInstanceId = _instances.FirstOrDefault()?.Id ?? "";
|
|
}
|
|
|
|
private async Task OnInstanceChanged()
|
|
{
|
|
_allImages.Clear();
|
|
_hasLoaded = false;
|
|
_selectedImageUrls.Clear();
|
|
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);
|
|
var filter = string.IsNullOrWhiteSpace(_searchTerm) ? "" : $"title:~'{EscapeFilter(_searchTerm)}'";
|
|
_allImages = await ImageAnalyzer.AnalyzeImagesAsync(instance, filter);
|
|
CalculateCounts();
|
|
_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));
|
|
}
|
|
|
|
private List<GhostImage> FilteredImages => _currentFilter switch
|
|
{
|
|
ImageFilter.Orphaned => _allImages.Where(i => i.IsOrphaned).ToList(),
|
|
ImageFilter.Feature => _allImages.Where(i => i.UsedInPosts.Any(r => r.IsFeatureImage)).ToList(),
|
|
ImageFilter.Content => _allImages.Where(i => i.UsedInPosts.Any(r => r.IsInContent)).ToList(),
|
|
_ => _allImages
|
|
};
|
|
|
|
private List<GhostImage> SelectedOrphanedImages => _allImages.Where(i => _selectedImageUrls.Contains(i.Url) && i.IsOrphaned).ToList();
|
|
|
|
private void SetFilter(ImageFilter filter)
|
|
{
|
|
_currentFilter = filter;
|
|
}
|
|
|
|
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);
|
|
_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 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
|
|
}
|
|
}
|