@page "/" @implements IDisposable @inject ImporterSession Session @inject PostCatalog Catalog @inject LinkSearchService Search @inject IJSRuntime JS

WP2Ghost

WordPress: @Short(Importer.WpBaseUrl) @* Ziel für Senden, Prüfen und Abgleich *@ Ghost: @for (int i = 0; i < Importer.GhostTargets.Length; i++) { int index = i; var target = Importer.GhostTargets[index]; } Daten: @Session.DataDir
@if (_link != null && Session.Prompt != null) { @* Toter Link: Ersatz suchen, in der Vorschau ansehen und direkt übernehmen *@

Toter Link – Ersatz suchen

Linktext @(_link.Text.Length > 0 ? _link.Text : "(ohne Text)")
Alte Adresse @_link.Href
Problem @_link.Reason
@if (_searching) {

Suche läuft …

} @if (_searchError != null) {

@_searchError

} @if (!_searching && _searchError == null && _suggestions.Count == 0) {

Keine Treffer. Suchbegriff ändern oder eine Adresse rechts eintragen.

} @foreach (var suggestion in _suggestions) {
@if (suggestion.Source == "Archiv") { Archiv } @suggestion.Title
@suggestion.Url
@if (suggestion.Snippet.Length > 0) {
@suggestion.Snippet
}
}
@if (_previewUrl.Length > 0) {

Bleibt die Vorschau leer, verbietet die Seite das Einbetten – in neuem Tab öffnen ↗. Klickst du in der Vorschau weiter, bitte die gewünschte Adresse oben eintragen: die Oberfläche kann sie aus der Vorschau nicht auslesen.

} else {
„Vorschau“ bei einem Treffer zeigt die Seite hier an.
}

Beibehalten oder Löschen wählst du rechts bei der Rückfrage.

} @if (_tab == "posts") {

Abläufe

Rückfragen (tote Links, primäre Kategorie, Untertag, „?“-Fälle) erscheinen rechts im Ablauf. Gesendet, geprüft und abgeglichen wird mit der oben gewählten Ghost-Instanz. @if (!GhostApi.IsConfigured()) { Für das Senden bitte zuerst unter „Einstellungen“ Ghost-URL und Admin-API-Schlüssel von @ActiveLabel eintragen. }

Alle Beiträge neu abrufen

Der Abruf holt nur Beiträge, die noch nicht abgefragt wurden. Zurücksetzen leert diese Liste, sodass der nächste Abruf alle Beiträge neu holt. Bereits abgerufene Dateien werden dabei überschrieben.

Abgerufene Posts @Visible.Count von @_rows.Count

Bereich: @_selected.Count ausgewählt
@if (_rows.Count == 0) {

Noch keine Posts abgerufen. „Von WordPress abrufen“ holt sie in den Datenordner.

} else {
@foreach (var row in Visible) { }
ID Titel Datum Kategorien Bilder Videos Komm. Status
@row.WpId @row.Title @(row.Date?.ToString("dd.MM.yyyy") ?? "—") @string.Join(", ", row.Categories) @row.Images @(row.Videos > 0 ? row.Videos.ToString() : "") @(row.Comments > 0 ? row.Comments.ToString() : "") @if (row.Archived) { archiviert } @switch (row.Ghost) { case GhostState.Present: in @ActiveLabel break; case GhostState.WrongDate: in @ActiveLabel, Datum weicht ab break; case GhostState.Missing: nicht in @ActiveLabel break; }
@if (Catalog.GhostCheckedAt is DateTime checkedAt) {

Stand des Abgleichs mit @ActiveLabel: @checkedAt.ToString("dd.MM.yyyy HH:mm")

} else {

Ob ein Post schon in @ActiveLabel ist, zeigt die Liste nach „Mit Ghost abgleichen“.

} }
} else {

Einstellungen

@for (int i = 0; i < _ghostForms.Length; i++) { int index = i; var form = _ghostForms[index];
Ghost-Instanz @(index + 1)@(index == Importer.ActiveGhost ? " (aktiv)" : "")
}
@if (_settingsMessage != null) { @_settingsMessage }

Gespeichert wird in config.txt im Datenordner – dieselbe Datei, die auch das Konsolenprogramm liest. Das Archiv gilt für beide Ghost-Instanzen gemeinsam. Bitte Konsole und Oberfläche nicht gleichzeitig auf demselben Datenordner arbeiten lassen.

}
@code { private string _tab = "posts"; private List _rows = new(); private readonly HashSet _selected = new(); private string _filter = "active"; private string _search = ""; private int? _fromId, _toId; private bool _confirmReset; private string _answer = ""; private int _seenRuns; private string? _lastPrompt; // Einstellungen private string _wpUrl = "", _zipPath = ""; private int? _maxPosts; private string? _settingsMessage; // Eingabefelder je Ghost-Instanz; der Schlüssel wird nie angezeigt private class GhostForm { public string Name = "", Url = "", AdminKey = ""; } private readonly GhostForm[] _ghostForms = Importer.GhostTargets.Select(_ => new GhostForm()).ToArray(); private static string ActiveLabel => Importer.GhostLabel(Importer.ActiveGhost); private List Visible => _rows .Where(r => _filter == "all" || (_filter == "archived") == r.Archived) .Where(r => _search.Length == 0 || r.Title.Contains(_search, StringComparison.OrdinalIgnoreCase) || r.WpId.ToString().Contains(_search)) .ToList(); private List SelectedActive => _rows.Where(r => _selected.Contains(r.WpId) && !r.Archived).ToList(); private List SelectedArchived => _rows.Where(r => _selected.Contains(r.WpId) && r.Archived).ToList(); protected override void OnInitialized() { Session.Changed += OnSessionChanged; _seenRuns = Session.CompletedRuns; LoadSettings(); Reload(); } public void Dispose() => Session.Changed -= OnSessionChanged; // ---- Toter Link: Ersatz suchen ---- private LinkQuestion? _link; private List _suggestions = new(); private string _searchQuery = "", _previewUrl = "", _addressInput = ""; private bool _searching; private string? _searchError; private static bool IsWebAddress(string text) => Uri.TryCreate(text.Trim(), UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); // Fragt der Import zu einem (anderen) toten Link, sofort passend dazu suchen private void TrackLinkQuestion() { var current = Session.Prompt != null ? LinkQuestion.Current : null; if (current == _link) return; _link = current; _suggestions = new(); _previewUrl = _addressInput = ""; _searchError = null; if (current == null) return; _searchQuery = current.Text; _ = SearchAsync(includeArchive: true); } private async Task SearchAsync(bool includeArchive = false) { var link = _link; if (link == null) return; _searching = true; _searchError = null; StateHasChanged(); var found = new List(); try { // Archivfassung der alten Adresse nur beim ersten Suchen holen und danach behalten var archived = includeArchive ? await Search.FindArchivedAsync(link.Href) : _suggestions.FirstOrDefault(s => s.Source == "Archiv"); if (archived != null) found.Add(archived); found.AddRange(await Search.SearchAsync(_searchQuery)); } catch (Exception ex) { _searchError = $"Die automatische Suche geht gerade nicht: {ex.Message}. „Bei Google suchen“ öffnet die Suche in einem neuen Tab; die gefundene Adresse kannst du rechts eintragen und übernehmen."; } // Inzwischen kann schon der nächste Link an der Reihe sein if (link == _link) { _suggestions = found; _searching = false; await InvokeAsync(StateHasChanged); } } private void Preview(string url) { url = url.Trim(); if (!IsWebAddress(url)) return; _previewUrl = _addressInput = url; } // Übernehmen = im Import "bearbeiten" wählen und die neue Adresse eintragen private void Adopt(string url) { url = url.Trim(); if (!IsWebAddress(url) || Session.Prompt is not string prompt) return; if (prompt.Contains("Neue URL eingeben")) Session.Answer(url); else Session.AnswerMany("e", url); } private void OnSessionChanged() => InvokeAsync(() => { TrackLinkQuestion(); // Nach einem Ablauf haben sich Dateien geändert (abgerufen, archiviert ...) if (Session.CompletedRuns != _seenRuns) { _seenRuns = Session.CompletedRuns; Reload(); } StateHasChanged(); }); protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("wp2ghost.scrollToEnd", "log"); // Bei einer neuen Rückfrage direkt ins Antwortfeld if (Session.Prompt != _lastPrompt) { _lastPrompt = Session.Prompt; if (_lastPrompt != null) await JS.InvokeVoidAsync("wp2ghost.focus", "answer"); } } private void Reload() { _rows = Catalog.Load(); _selected.IntersectWith(_rows.Select(r => r.WpId)); } // ---- Abläufe ---- private void Fetch() => Session.Run("Abruf von WordPress", () => PostFetcher.FetchFromWordPressAsync(Importer.WpBaseUrl, Importer.MaxPosts)); private void Send() { var posts = SelectedActive.OrderBy(r => r.WpId).Select(r => (r.Path, r.WpId)).ToList(); if (posts.Count == 0) return; Session.Run($"Senden an {ActiveLabel} ({posts.Count} Post(s))", () => Importer.SendPostsAsync(posts)); } private void Check() => Session.Run("Prüfen & korrigieren", () => PostChecker.RunAsync(Importer.BaseDir)); private void Compare() => Session.Run($"Abgleich mit {ActiveLabel}", () => Catalog.CompareWithGhostAsync()); // Andere Ghost-Instanz wählen: der Abgleich der Liste gilt dann nicht mehr private void SwitchGhost(int index) { if (Session.IsBusy) return; Importer.SwitchGhost(index); Catalog.ResetGhostState(); Reload(); } private void ResetProcessed() { _confirmReset = false; Session.Run("Abgefragte Beiträge zurücksetzen", () => { PostFetcher.ClearProcessedPosts(); return Task.CompletedTask; }); } private void Reply(string value) { Session.Answer(value); _answer = ""; } // ---- Auswahl ---- private void Toggle(int wpId, bool on) { if (on) _selected.Add(wpId); else _selected.Remove(wpId); } private void SelectRange() { if (_fromId == null) return; int from = _fromId.Value, to = _toId ?? from; if (to < from) (from, to) = (to, from); foreach (var row in Visible.Where(r => r.WpId >= from && r.WpId <= to)) _selected.Add(row.WpId); } private void SelectVisible() { foreach (var row in Visible) _selected.Add(row.WpId); } private void Archive() { int moved = Catalog.Archive(SelectedActive); Console.WriteLine($"{moved} Post(s) archiviert."); Reload(); } private void Restore() { int moved = Catalog.Restore(SelectedArchived); Console.WriteLine($"{moved} Post(s) aus dem Archiv zurückgeholt."); Reload(); } // ---- Einstellungen ---- private void LoadSettings() { _wpUrl = Importer.WpBaseUrl; _maxPosts = Importer.MaxPosts > 0 ? Importer.MaxPosts : null; _zipPath = Importer.OutputZipPath; for (int i = 0; i < _ghostForms.Length; i++) { _ghostForms[i].Name = Importer.GhostTargets[i].Name; _ghostForms[i].Url = Importer.GhostTargets[i].Url; _ghostForms[i].AdminKey = ""; } } private void SaveSettings() { Importer.WpBaseUrl = _wpUrl.Trim(); Importer.MaxPosts = _maxPosts is > 0 ? _maxPosts.Value : -1; if (!string.IsNullOrWhiteSpace(_zipPath)) Importer.OutputZipPath = _zipPath.Trim(); for (int i = 0; i < _ghostForms.Length; i++) { var target = Importer.GhostTargets[i]; target.Name = _ghostForms[i].Name.Trim(); target.Url = _ghostForms[i].Url.Trim(); // Der Schlüssel wird nie angezeigt; ein leeres Feld lässt den gespeicherten unverändert if (!string.IsNullOrWhiteSpace(_ghostForms[i].AdminKey)) target.AdminApiKey = _ghostForms[i].AdminKey.Trim(); } Importer.ApplySettings(); // Neue Adresse oder neuer Schlüssel: der letzte Abgleich passt womöglich nicht mehr Catalog.ResetGhostState(); Reload(); LoadSettings(); _settingsMessage = $"Gespeichert um {DateTime.Now:HH:mm:ss}."; } // ---- Darstellung ---- private static string Short(string url) => url.Replace("https://", "").Replace("http://", "").TrimEnd('/'); private static string LineClass(string line) { if (line.StartsWith("▶") || line.StartsWith("■") || line.StartsWith("===")) return "log__head"; if (line.Contains("[Fehler]") || line.Contains("FEHLER") || line.Contains("ABBRUCH") || line.Contains("-Fehler]")) return "log__error"; if (line.Contains("[Warnung]")) return "log__warn"; return ""; } }