2026-05-14 12:31:35 +02:00
|
|
|
@page "/editor"
|
|
|
|
|
@page "/editor/{action}"
|
|
|
|
|
@page "/editor/{action}/{projectId:int}"
|
2026-05-25 06:38:16 +02:00
|
|
|
@using System.Threading
|
2026-05-14 12:31:35 +02:00
|
|
|
@inject IOllamaService OllamaService
|
|
|
|
|
@inject IProjectService ProjectService
|
2026-05-20 06:12:54 +02:00
|
|
|
@inject IRagService RagService
|
|
|
|
|
@inject ISettingsService SettingsService
|
|
|
|
|
@inject IWikipediaService WikipediaService
|
2026-05-14 12:31:35 +02:00
|
|
|
@inject NavigationManager Navigation
|
2026-05-25 06:38:16 +02:00
|
|
|
@inject ILocalizationService Loc
|
2026-05-31 19:31:27 +02:00
|
|
|
@inject IAuthService AuthService
|
|
|
|
|
@inject OllamaSettings OllamaSettings
|
|
|
|
|
@inject OperationState OpState
|
|
|
|
|
@inject IJSRuntime JS
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
@implements IDisposable
|
|
|
|
|
@rendermode InteractiveServer
|
2026-05-21 11:04:42 +02:00
|
|
|
@attribute [Authorize]
|
2026-05-20 06:12:54 +02:00
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
<PageTitle>@Loc["page.title.editor"]</PageTitle>
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="editor-page">
|
2026-05-25 06:38:16 +02:00
|
|
|
<textarea class="editor-area"
|
|
|
|
|
@oninput="OnContentChanged"
|
|
|
|
|
value="@editorContent"
|
|
|
|
|
placeholder="@Loc["editor.placeholder.content"]"></textarea>
|
2026-05-31 19:31:27 +02:00
|
|
|
<div class="editor-format-toolbar">
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertHeading(1)" title="Überschrift 1">H1</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertHeading(2)" title="Überschrift 2">H2</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertHeading(3)" title="Überschrift 3">H3</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertHeading(4)" title="Überschrift 4">H4</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertHeading(5)" title="Überschrift 5">H5</button>
|
|
|
|
|
<span class="fmt-sep"></span>
|
|
|
|
|
<button class="fmt-btn" @onclick='() => WrapSelection("**", "**")' title="Fett"><b>B</b></button>
|
|
|
|
|
<button class="fmt-btn" @onclick='() => WrapSelection("*", "*")' title="Kursiv"><i>I</i></button>
|
|
|
|
|
<button class="fmt-btn" @onclick='() => WrapSelection("<u>", "</u>")' title="Unterstrichen"><u>U</u></button>
|
|
|
|
|
<span class="fmt-sep"></span>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertList(false)" title="Aufzählung">UL</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => InsertList(true)" title="Nummerierte Liste">OL</button>
|
|
|
|
|
<span class="fmt-sep"></span>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => IndentLines()" title="Einrücken">→</button>
|
|
|
|
|
<button class="fmt-btn" @onclick="() => OutdentLines()" title="Ausrücken">←</button>
|
|
|
|
|
</div>
|
|
|
|
|
<CopyCreateToolbar Text="@editorContent" DefaultFileName="editor_text" />
|
2026-05-20 06:12:54 +02:00
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
<div class="editor-toolbar">
|
2026-05-25 06:38:16 +02:00
|
|
|
<span style="flex: 1; font-size: 12px; color: var(--border-color); display:flex;align-items:center;gap:8px;">
|
|
|
|
|
@if (_contentModified)
|
|
|
|
|
{
|
|
|
|
|
<span class="editor-modified-badge">●</span>
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
@statusText
|
|
|
|
|
</span>
|
|
|
|
|
@if (isChecking)
|
|
|
|
|
{
|
|
|
|
|
<progress value="@checkProgress" max="100"></progress>
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="CheckStyle" disabled="@isChecking">@Loc["editor.button.style_check"]</button>
|
|
|
|
|
<button @onclick="SpellCheck" disabled="@isChecking">@Loc["editor.button.spell_check"]</button>
|
|
|
|
|
<button @onclick="SaveCurrentFile" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.button.save"]</button>
|
|
|
|
|
<button @onclick="ShowVersionDialog" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.version.button.save"]</button>
|
|
|
|
|
<button @onclick="ShowVersionBrowser" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.version.button.browse"]</button>
|
|
|
|
|
<button @onclick="OpenAssistant">@Loc["editor.button.assistant"]</button>
|
|
|
|
|
<button @onclick="IndexProject" disabled="@isChecking">@Loc["editor.button.index_project"]</button>
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
@if (showVersionDialog)
|
|
|
|
|
{
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="modal-title">@Loc["editor.version.title"]</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label>@Loc["editor.version.label.comment"]</label>
|
|
|
|
|
<input @bind="versionComment" placeholder="@Loc["editor.version.placeholder.comment"]" />
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="() => showVersionDialog = false">@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="SaveVersioned" class="primary">@Loc["editor.version.button.save"]</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="search-section">
|
2026-05-25 06:38:16 +02:00
|
|
|
<input value="@searchQuery" @oninput="HandleSearchInput" placeholder="@Loc["editor.search.placeholder"]"
|
2026-05-20 06:12:54 +02:00
|
|
|
disabled="@isChecking" />
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="RagSearch" disabled=@buttonDisabled>@Loc["editor.button.search_rag"]</button>
|
|
|
|
|
<button @onclick="SearchWikipedia" disabled=@buttonDisabled>@Loc["editor.button.search_wikipedia"]</button>
|
2026-05-17 07:27:18 +02:00
|
|
|
</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
@if (showSpellDiff && !string.IsNullOrEmpty(correctedText))
|
|
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
|
|
|
|
<div class="diff-panel">
|
2026-05-31 19:31:27 +02:00
|
|
|
<h4 style="display:inline">@Loc["editor.spellcheck.heading"]</h4>
|
|
|
|
|
<CopyCreateToolbar Text="@correctedText" DefaultFileName="korrigierter_text" />
|
2026-05-20 06:12:54 +02:00
|
|
|
<DiffView Original="@originalText" Corrected="@correctedText" />
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="diff-actions">
|
|
|
|
|
<button class="btn btn-primary" @onclick="ApplySpellCorrections">@Loc["editor.spellcheck.button.apply"]</button>
|
|
|
|
|
</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
else if (showVersionDiff && _selectedVersion != null)
|
|
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
|
|
|
|
<div class="diff-panel">
|
|
|
|
|
<h4>@Loc["editor.version.diff_title"] @_selectedVersion.Timestamp:dd.MM.yyyy HH:mm</h4>
|
|
|
|
|
@if (!string.IsNullOrEmpty(_selectedVersion.Comment))
|
|
|
|
|
{
|
|
|
|
|
<p style="font-size:11px;color:var(--border-color);margin:0 0 8px 0;">@Loc["editor.version.label.comment"]: @_selectedVersion.Comment</p>
|
|
|
|
|
}
|
|
|
|
|
<DiffView Original="@editorContent" Corrected="@_versionDiffContent" />
|
|
|
|
|
<div class="diff-actions">
|
|
|
|
|
<button class="btn btn-primary" @onclick="() => RestoreVersion(_selectedVersion)">@Loc["editor.version.button.restore"]</button>
|
|
|
|
|
<button class="btn btn-secondary" @onclick="CloseVersionBrowser">@Loc["common.cancel"]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else if (showVersionBrowser)
|
|
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
|
|
|
|
<h4>@Loc["editor.version.list_title"]</h4>
|
|
|
|
|
@if (_versionList == null || _versionList.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
<p>@Loc["editor.version.empty"]</p>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<div class="version-list">
|
|
|
|
|
@foreach (var v in _versionList)
|
|
|
|
|
{
|
|
|
|
|
<div class="version-item @(v == _selectedVersion ? "active" : "")">
|
|
|
|
|
<div class="version-item-info">
|
|
|
|
|
<span class="version-date">@v.Timestamp.ToString("dd.MM.yyyy HH:mm")</span>
|
|
|
|
|
@if (!string.IsNullOrEmpty(v.Comment))
|
|
|
|
|
{
|
|
|
|
|
<span class="version-comment">@v.Comment</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="version-item-actions">
|
|
|
|
|
<button class="btn btn-sm" @onclick="() => ShowVersionDiff(v)">@Loc["editor.version.button.diff"]</button>
|
|
|
|
|
<button class="btn btn-sm btn-primary" @onclick="() => RestoreVersion(v)">@Loc["editor.version.button.restore"]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
<button class="btn btn-secondary" @onclick="CloseVersionBrowser">@Loc["common.cancel"]</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
else if (ragResults != null && ragResults.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
2026-05-25 06:38:16 +02:00
|
|
|
<h4>@Loc["editor.rag.heading"]</h4>
|
2026-05-20 06:12:54 +02:00
|
|
|
@foreach (var r in ragResults)
|
|
|
|
|
{
|
|
|
|
|
<div class="fact-card">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="fact-similarity">@Loc["editor.rag.label.similarity"] @r.Similarity.ToString("P1")</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="fact-path">@r.FilePath</div>
|
|
|
|
|
<div class="fact-text">@r.Text</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else if (wikipediaResults != null && wikipediaResults.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
2026-05-25 06:38:16 +02:00
|
|
|
<h4>@Loc["editor.wikipedia.heading"]</h4>
|
2026-05-20 06:12:54 +02:00
|
|
|
@foreach (var r in wikipediaResults)
|
|
|
|
|
{
|
|
|
|
|
<div class="fact-card">
|
|
|
|
|
<div class="fact-similarity">@(r.Language == "de" ? "🇩🇪" : "🇬🇧") @r.Language.ToUpper()</div>
|
|
|
|
|
<div class="fact-path">
|
|
|
|
|
<a href="@r.Url" target="_blank">@r.Title</a>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fact-text">@r.Snippet</div>
|
|
|
|
|
@if (!string.IsNullOrEmpty(r.Extract))
|
|
|
|
|
{
|
|
|
|
|
<div class="fact-extract">@r.Extract</div>
|
|
|
|
|
}
|
|
|
|
|
<div class="fact-actions">
|
|
|
|
|
<button @onclick="() => LoadFullArticle(r)" disabled="@(loadedArticles.ContainsKey(r.Url))">
|
2026-05-20 07:27:41 +02:00
|
|
|
@(loadedArticles.ContainsKey(r.Url)
|
2026-05-25 06:38:16 +02:00
|
|
|
? Loc["editor.wikipedia.status.loaded"]
|
|
|
|
|
: Loc["editor.wikipedia.button.load"])
|
2026-05-20 06:12:54 +02:00
|
|
|
</button>
|
|
|
|
|
<button @onclick="() => SaveAsFact(r)" disabled="@(string.IsNullOrWhiteSpace(r.Extract) || currentProjectId == 0)">
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["editor.wikipedia.button.save_as_fact"]
|
2026-05-20 06:12:54 +02:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
else if (!string.IsNullOrEmpty(analysisResult) && !showVersionBrowser)
|
2026-05-20 06:12:54 +02:00
|
|
|
{
|
|
|
|
|
<div class="results-area">
|
2026-05-31 19:31:27 +02:00
|
|
|
<h4 style="display:inline">@Loc["editor.result.heading"]</h4>
|
|
|
|
|
<CopyCreateToolbar Text="@analysisResult" />
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="result-text">@analysisResult</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (showCreateDialog)
|
2026-05-17 07:27:18 +02:00
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="modal-title">@Loc["editor.create_project.title"]</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label>@Loc["editor.create_project.label.name"]</label>
|
|
|
|
|
<input @bind="newProjectName" placeholder="@Loc["editor.create_project.placeholder.name"]" />
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label>@Loc["editor.create_project.label.path"]</label>
|
|
|
|
|
<input @bind="newProjectPath" placeholder="@Loc["editor.create_project.placeholder.path"]" />
|
2026-05-31 19:31:27 +02:00
|
|
|
<small class="form-hint">Erlaubter Pfad: @ProjectPathHelper.AllowedBasePath</small>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>Vorlage</label>
|
|
|
|
|
<div class="template-radio-group">
|
|
|
|
|
<label class="template-radio">
|
|
|
|
|
<input type="radio" name="template" value="@ProjectTemplate.Novel" checked="@(selectedTemplate == ProjectTemplate.Novel)" @onchange="() => selectedTemplate = ProjectTemplate.Novel" />
|
|
|
|
|
<span>📖 Roman</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="template-radio">
|
|
|
|
|
<input type="radio" name="template" value="@ProjectTemplate.Technical" checked="@(selectedTemplate == ProjectTemplate.Technical)" @onchange="() => selectedTemplate = ProjectTemplate.Technical" />
|
|
|
|
|
<span>⚙️ Technische Vorlage (Requirements)</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="CancelCreateProject">@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="CreateProject" disabled="@(() => string.IsNullOrWhiteSpace(newProjectName) || string.IsNullOrWhiteSpace(newProjectPath))" class="primary">@Loc["editor.button.create"]</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
[Parameter]
|
|
|
|
|
public string? Action { get; set; }
|
|
|
|
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
public int? ProjectId { get; set; }
|
|
|
|
|
|
|
|
|
|
private string editorContent = string.Empty;
|
2026-05-20 06:12:54 +02:00
|
|
|
private string searchQuery = string.Empty;
|
2026-05-20 07:27:41 +02:00
|
|
|
private string statusText = string.Empty;
|
2026-05-14 12:31:35 +02:00
|
|
|
private bool isChecking;
|
2026-05-20 06:12:54 +02:00
|
|
|
private bool buttonDisabled;
|
2026-05-14 12:31:35 +02:00
|
|
|
private double checkProgress;
|
|
|
|
|
private string analysisResult = string.Empty;
|
2026-05-17 07:27:18 +02:00
|
|
|
private string originalText = string.Empty;
|
|
|
|
|
private string correctedText = string.Empty;
|
|
|
|
|
private bool showSpellDiff;
|
2026-05-20 06:12:54 +02:00
|
|
|
private List<RagSearchResult>? ragResults;
|
|
|
|
|
private List<WikipediaResult>? wikipediaResults;
|
|
|
|
|
|
|
|
|
|
private int currentProjectId;
|
|
|
|
|
private string? currentFile;
|
2026-05-31 19:31:27 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private bool showCreateDialog;
|
|
|
|
|
private string newProjectName = string.Empty;
|
|
|
|
|
private string newProjectPath = string.Empty;
|
2026-05-31 19:31:27 +02:00
|
|
|
private ProjectTemplate selectedTemplate = ProjectTemplate.Novel;
|
2026-05-20 06:12:54 +02:00
|
|
|
private Dictionary<string, bool> loadedArticles = new();
|
|
|
|
|
private bool showVersionDialog;
|
|
|
|
|
private string versionComment = string.Empty;
|
|
|
|
|
private VersionEntry? latestVersion;
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private Timer? _autosaveTimer;
|
|
|
|
|
private string _lastSavedContent = string.Empty;
|
|
|
|
|
private bool _contentModified;
|
|
|
|
|
private bool showVersionBrowser;
|
|
|
|
|
private bool showVersionDiff;
|
|
|
|
|
private List<VersionEntry> _versionList = new();
|
|
|
|
|
private VersionEntry? _selectedVersion;
|
|
|
|
|
private string _versionDiffContent = string.Empty;
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private record VersionEntry(string FilePath, DateTime Timestamp, string? Comment);
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-20 07:27:41 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
Navigation.LocationChanged += OnLocationChanged;
|
2026-05-20 07:27:41 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
await LoadProjectContext();
|
|
|
|
|
|
2026-05-20 07:27:41 +02:00
|
|
|
var pendingOutput = SettingsService.PendingAssistantOutput;
|
|
|
|
|
if (!string.IsNullOrEmpty(pendingOutput))
|
|
|
|
|
{
|
|
|
|
|
SettingsService.PendingAssistantOutput = null;
|
|
|
|
|
editorContent = pendingOutput;
|
2026-05-25 06:38:16 +02:00
|
|
|
_lastSavedContent = editorContent;
|
|
|
|
|
statusText = Loc["editor.status.imported_from_assistant"];
|
2026-05-20 07:27:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
if (string.Equals(Action, "newproject", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
newProjectName = string.Empty;
|
|
|
|
|
newProjectPath = ProjectPathHelper.SuggestPath("Projekt");
|
2026-05-20 06:12:54 +02:00
|
|
|
showCreateDialog = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
await LoadProjectContext();
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Navigation.LocationChanged -= OnLocationChanged;
|
2026-05-25 06:38:16 +02:00
|
|
|
_autosaveTimer?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnContentChanged(ChangeEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
editorContent = e.Value?.ToString() ?? string.Empty;
|
|
|
|
|
_contentModified = editorContent != _lastSavedContent;
|
|
|
|
|
ResetAutosaveTimer();
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ResetAutosaveTimer()
|
|
|
|
|
{
|
|
|
|
|
_autosaveTimer?.Dispose();
|
|
|
|
|
_autosaveTimer = new Timer(AutosaveCallback, null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void AutosaveCallback(object? state)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(currentFile) || !_contentModified || isChecking) return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await InvokeAsync(async () =>
|
|
|
|
|
{
|
|
|
|
|
await File.WriteAllTextAsync(currentFile, editorContent);
|
|
|
|
|
_lastSavedContent = editorContent;
|
|
|
|
|
_contentModified = false;
|
|
|
|
|
statusText = Loc["editor.status.autosaved"];
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-20 07:27:41 +02:00
|
|
|
private void OpenAssistant()
|
|
|
|
|
{
|
|
|
|
|
SettingsService.PendingAssistantInput = editorContent;
|
|
|
|
|
Navigation.NavigateTo("/assistant");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private void HandleSearchInput(ChangeEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
searchQuery = e.Value?.ToString() ?? string.Empty;
|
|
|
|
|
buttonDisabled = isChecking || string.IsNullOrWhiteSpace(searchQuery);
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadProjectContext()
|
|
|
|
|
{
|
|
|
|
|
if (ProjectId.HasValue)
|
|
|
|
|
{
|
|
|
|
|
currentProjectId = ProjectId.Value;
|
|
|
|
|
SettingsService.CurrentProjectId = currentProjectId;
|
|
|
|
|
}
|
|
|
|
|
else if (SettingsService.CurrentProjectId.HasValue)
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
currentProjectId = SettingsService.CurrentProjectId.Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentProjectId > 0)
|
|
|
|
|
await SettingsService.LoadProjectSettingsAsync();
|
|
|
|
|
|
|
|
|
|
var filePath = SettingsService.CurrentFilePath;
|
|
|
|
|
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
|
|
|
|
{
|
|
|
|
|
currentFile = filePath;
|
|
|
|
|
editorContent = await File.ReadAllTextAsync(filePath);
|
2026-05-25 06:38:16 +02:00
|
|
|
_lastSavedContent = editorContent;
|
|
|
|
|
_contentModified = false;
|
|
|
|
|
statusText = $"{Loc["editor.status.loaded"]} {Path.GetFileName(filePath)}";
|
2026-05-20 06:12:54 +02:00
|
|
|
RefreshLatestVersion();
|
2026-05-31 19:31:27 +02:00
|
|
|
|
|
|
|
|
var styleModel = await SettingsService.GetStyleModelAsync();
|
|
|
|
|
if (!string.IsNullOrEmpty(styleModel))
|
|
|
|
|
{
|
|
|
|
|
var ctx = await OllamaService.GetModelContextWindowAsync(styleModel);
|
|
|
|
|
if (ctx.Success && editorContent.Length > ctx.ContextWindow * 0.8)
|
|
|
|
|
{
|
|
|
|
|
statusText = $"⚠ {Loc["editor.warning.context"]} ({editorContent.Length} Tokens ≈ {ctx.ContextWindow} Kontext)";
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private void ClearResults()
|
|
|
|
|
{
|
|
|
|
|
showSpellDiff = false;
|
|
|
|
|
showVersionBrowser = false;
|
|
|
|
|
showVersionDiff = false;
|
|
|
|
|
ragResults = null;
|
|
|
|
|
wikipediaResults = null;
|
|
|
|
|
analysisResult = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
private async Task CheckStyle()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(editorContent)) return;
|
|
|
|
|
|
|
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.analyzing_style"];
|
2026-05-14 12:31:35 +02:00
|
|
|
checkProgress = 50;
|
2026-05-25 06:38:16 +02:00
|
|
|
ClearResults();
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["common.error"]} {ex.Message}";
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-14 12:31:35 +02:00
|
|
|
isChecking = false;
|
|
|
|
|
checkProgress = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SpellCheck()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(editorContent)) return;
|
|
|
|
|
|
|
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.checking_spelling"];
|
2026-05-14 12:31:35 +02:00
|
|
|
checkProgress = 50;
|
2026-05-25 06:38:16 +02:00
|
|
|
ClearResults();
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-17 07:27:18 +02:00
|
|
|
originalText = editorContent;
|
2026-05-31 19:31:27 +02:00
|
|
|
correctedText = "";
|
|
|
|
|
OpState.SetStatus(Loc["editor.status.checking_spelling"], true);
|
|
|
|
|
OpState.SetThought("");
|
|
|
|
|
|
|
|
|
|
await OllamaService.ChatStream(OllamaSettings.SpellingModel, editorContent, async chunk =>
|
|
|
|
|
{
|
|
|
|
|
correctedText += chunk;
|
|
|
|
|
OpState.AppendThought(chunk);
|
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
|
}, OllamaSettings.SpellingPrompt, OllamaSettings.SpellingTemp);
|
|
|
|
|
|
2026-05-17 07:27:18 +02:00
|
|
|
showSpellDiff = true;
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus(Loc["common.status.ready"], false);
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["common.error"]} {ex.Message}";
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", false);
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.Reset();
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-14 12:31:35 +02:00
|
|
|
isChecking = false;
|
|
|
|
|
checkProgress = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private void ApplySpellCorrections()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(correctedText)) return;
|
|
|
|
|
editorContent = correctedText;
|
|
|
|
|
_contentModified = editorContent != _lastSavedContent;
|
|
|
|
|
showSpellDiff = false;
|
|
|
|
|
ResetAutosaveTimer();
|
|
|
|
|
statusText = Loc["editor.status.corrections_applied"];
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
private async Task RagSearch()
|
|
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
if (string.IsNullOrWhiteSpace(searchQuery)) return;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.searching_rag"];
|
|
|
|
|
ClearResults();
|
2026-05-20 06:12:54 +02:00
|
|
|
analysisResult = string.Empty;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (currentProjectId == 0)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.rag.error.no_project"];
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ragResults = await RagService.SearchAsync(currentProjectId, searchQuery, 5);
|
|
|
|
|
|
|
|
|
|
if (ragResults.Count == 0)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.rag.error.no_facts"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["editor.rag.error.generic"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-20 06:12:54 +02:00
|
|
|
isChecking = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SearchWikipedia()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(searchQuery)) return;
|
|
|
|
|
|
|
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.searching_wikipedia"];
|
|
|
|
|
ClearResults();
|
2026-05-20 06:12:54 +02:00
|
|
|
analysisResult = string.Empty;
|
|
|
|
|
loadedArticles.Clear();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var request = new WikipediaSearchRequest
|
|
|
|
|
{
|
|
|
|
|
Query = searchQuery,
|
|
|
|
|
SearchGerman = true,
|
|
|
|
|
SearchEnglish = true,
|
|
|
|
|
MaxResults = 3
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
wikipediaResults = await WikipediaService.SearchAsync(request);
|
|
|
|
|
|
|
|
|
|
foreach (var result in wikipediaResults)
|
|
|
|
|
{
|
|
|
|
|
var extract = await WikipediaService.GetExtractAsync(result.Title, result.Language);
|
|
|
|
|
if (extract != null)
|
|
|
|
|
result.Extract = extract.Length > 800 ? extract[..800] + "..." : extract;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (wikipediaResults.Count == 0)
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.wikipedia.error.not_found"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["editor.wikipedia.error.generic"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-20 06:12:54 +02:00
|
|
|
isChecking = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadFullArticle(WikipediaResult result)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = $"{Loc["editor.status.loading_article"]} {result.Title}...";
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var fullText = await WikipediaService.GetFullArticleAsync(result.Title, result.Language);
|
|
|
|
|
if (fullText != null)
|
|
|
|
|
{
|
|
|
|
|
result.Extract = fullText;
|
|
|
|
|
loadedArticles[result.Url] = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.wikipedia.status.loaded"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.wikipedia.error.load_failed"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = $"{Loc["common.error"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private async Task SaveAsFact(WikipediaResult result)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(result.Extract) || currentProjectId == 0) return;
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.saving_fact"];
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var projects = await ProjectService.GetAllProjectsAsync();
|
|
|
|
|
var project = projects.FirstOrDefault(p => p.Id == currentProjectId);
|
|
|
|
|
if (project == null)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.error.project_not_found"];
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var factsDir = Path.Combine(project.RootPath, "01_Fakten");
|
|
|
|
|
Directory.CreateDirectory(factsDir);
|
|
|
|
|
|
|
|
|
|
var safeName = SanitizeFilename(searchQuery);
|
|
|
|
|
var filePath = Path.Combine(factsDir, $"{safeName}.md");
|
|
|
|
|
|
|
|
|
|
var content = $"# {result.Title}\n" +
|
|
|
|
|
$"- Quelle: [{result.Title}]({result.Url})\n" +
|
|
|
|
|
$"- Sprache: {result.Language.ToUpper()}\n" +
|
|
|
|
|
$"- Abgerufen: {DateTime.Now:dd.MM.yyyy HH:mm}\n\n" +
|
|
|
|
|
result.Extract;
|
|
|
|
|
|
|
|
|
|
await File.WriteAllTextAsync(filePath, content);
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = $"{Loc["editor.status.saved_as"]} {safeName}.md";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = $"{Loc["editor.error.save_failed"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string SanitizeFilename(string name)
|
|
|
|
|
{
|
|
|
|
|
var invalid = Path.GetInvalidFileNameChars();
|
|
|
|
|
var sanitized = new string(name.Where(c => !invalid.Contains(c)).ToArray());
|
|
|
|
|
return string.IsNullOrWhiteSpace(sanitized) ? "fakt" : sanitized.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CancelCreateProject()
|
|
|
|
|
{
|
|
|
|
|
showCreateDialog = false;
|
|
|
|
|
Navigation.NavigateTo("/editor");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task CreateProject()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(newProjectName) || string.IsNullOrWhiteSpace(newProjectPath)) return;
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
if (!ProjectPathHelper.IsPathAllowed(newProjectPath))
|
|
|
|
|
{
|
|
|
|
|
showCreateDialog = false;
|
|
|
|
|
statusText = $"⚠ Pfad nicht erlaubt. Verwende: {ProjectPathHelper.AllowedBasePath}";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.creating_project"];
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(newProjectPath);
|
2026-05-31 19:31:27 +02:00
|
|
|
var userId = AuthService.CurrentUser?.Id ?? 0;
|
|
|
|
|
var id = await ProjectService.CreateNewProjectAsync(newProjectName, newProjectPath, userId, selectedTemplate);
|
2026-05-20 06:12:54 +02:00
|
|
|
SettingsService.CurrentProjectId = id;
|
|
|
|
|
await SettingsService.LoadProjectSettingsAsync();
|
|
|
|
|
showCreateDialog = false;
|
|
|
|
|
Navigation.NavigateTo($"/editor/index/{id}");
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["editor.error.create_failed"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
isChecking = false;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task IndexProject()
|
|
|
|
|
{
|
|
|
|
|
if (currentProjectId == 0)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.index.error.no_project"];
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isChecking = true;
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["editor.status.indexing"];
|
2026-05-20 06:12:54 +02:00
|
|
|
analysisResult = string.Empty;
|
|
|
|
|
ragResults = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var projects = await ProjectService.GetAllProjectsAsync();
|
|
|
|
|
var project = projects.FirstOrDefault(p => p.Id == currentProjectId);
|
|
|
|
|
if (project == null)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.index.error.not_found"];
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var p = new Progress<string>(msg =>
|
|
|
|
|
{
|
|
|
|
|
statusText = msg;
|
|
|
|
|
InvokeAsync(StateHasChanged);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await RagService.IndexProjectAsync(currentProjectId, project.RootPath, p);
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = Loc["editor.index.status.success"];
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
analysisResult = $"{Loc["editor.index.error.generic"]} {ex.Message}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-20 06:12:54 +02:00
|
|
|
isChecking = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SaveCurrentFile()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(currentFile)) return;
|
|
|
|
|
await File.WriteAllTextAsync(currentFile, editorContent);
|
2026-05-25 06:38:16 +02:00
|
|
|
_lastSavedContent = editorContent;
|
|
|
|
|
_contentModified = false;
|
|
|
|
|
statusText = $"{Loc["editor.status.saved"]} {Path.GetFileName(currentFile)}";
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ShowVersionDialog()
|
|
|
|
|
{
|
|
|
|
|
showVersionDialog = true;
|
|
|
|
|
versionComment = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SaveVersioned()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(currentFile)) return;
|
|
|
|
|
|
|
|
|
|
var dir = Path.GetDirectoryName(currentFile)!;
|
|
|
|
|
var name = Path.GetFileNameWithoutExtension(currentFile);
|
|
|
|
|
var ext = Path.GetExtension(currentFile);
|
|
|
|
|
var versionsDir = Path.Combine(dir, "_versions");
|
|
|
|
|
Directory.CreateDirectory(versionsDir);
|
|
|
|
|
|
|
|
|
|
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
|
|
|
|
var versionPath = Path.Combine(versionsDir, $"{name}_{stamp}{ext}");
|
|
|
|
|
|
|
|
|
|
await File.WriteAllTextAsync(currentFile, editorContent);
|
|
|
|
|
await File.WriteAllTextAsync(versionPath, editorContent);
|
|
|
|
|
var meta = $"Kommentar: {versionComment}";
|
|
|
|
|
await File.WriteAllTextAsync(versionPath + ".meta", meta);
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = $"{Loc["editor.status.version_saved"]} {name}_{stamp}{ext}";
|
2026-05-20 06:12:54 +02:00
|
|
|
showVersionDialog = false;
|
|
|
|
|
versionComment = string.Empty;
|
2026-05-25 06:38:16 +02:00
|
|
|
_lastSavedContent = editorContent;
|
|
|
|
|
_contentModified = false;
|
2026-05-20 06:12:54 +02:00
|
|
|
RefreshLatestVersion();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private void ShowVersionBrowser()
|
|
|
|
|
{
|
|
|
|
|
ClearResults();
|
|
|
|
|
LoadVersionList();
|
|
|
|
|
showVersionBrowser = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseVersionBrowser()
|
|
|
|
|
{
|
|
|
|
|
showVersionBrowser = false;
|
|
|
|
|
showVersionDiff = false;
|
|
|
|
|
_selectedVersion = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LoadVersionList()
|
|
|
|
|
{
|
|
|
|
|
_versionList.Clear();
|
|
|
|
|
if (string.IsNullOrEmpty(currentFile)) return;
|
|
|
|
|
|
|
|
|
|
var dir = Path.GetDirectoryName(currentFile)!;
|
|
|
|
|
var name = Path.GetFileNameWithoutExtension(currentFile);
|
|
|
|
|
var ext = Path.GetExtension(currentFile);
|
|
|
|
|
var versionsDir = Path.Combine(dir, "_versions");
|
|
|
|
|
if (!Directory.Exists(versionsDir)) return;
|
|
|
|
|
|
|
|
|
|
var pattern = $"{name}_*{ext}";
|
|
|
|
|
var files = Directory.GetFiles(versionsDir, pattern).OrderByDescending(f => f).ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var file in files)
|
|
|
|
|
{
|
|
|
|
|
var metaFile = file + ".meta";
|
|
|
|
|
string? comment = null;
|
|
|
|
|
if (File.Exists(metaFile))
|
|
|
|
|
{
|
|
|
|
|
var lines = File.ReadAllLines(metaFile);
|
|
|
|
|
if (lines.Length > 0 && lines[0].StartsWith("Kommentar: "))
|
|
|
|
|
comment = lines[0]["Kommentar: ".Length..];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fileName = Path.GetFileNameWithoutExtension(file);
|
|
|
|
|
var stamp = fileName[(name.Length + 1)..];
|
|
|
|
|
var parsed = DateTime.TryParseExact(stamp, "yyyyMMdd_HHmmss", null,
|
|
|
|
|
System.Globalization.DateTimeStyles.None, out var dt);
|
|
|
|
|
|
|
|
|
|
_versionList.Add(new VersionEntry(
|
|
|
|
|
file,
|
|
|
|
|
parsed ? dt : File.GetLastWriteTime(file),
|
|
|
|
|
comment
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void ShowVersionDiff(VersionEntry version)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_versionDiffContent = await File.ReadAllTextAsync(version.FilePath);
|
|
|
|
|
_selectedVersion = version;
|
|
|
|
|
showVersionDiff = true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
statusText = $"{Loc["common.error"]} {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void RestoreVersion(VersionEntry version)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
editorContent = await File.ReadAllTextAsync(version.FilePath);
|
|
|
|
|
_contentModified = editorContent != _lastSavedContent;
|
|
|
|
|
ResetAutosaveTimer();
|
|
|
|
|
statusText = $"{Loc["editor.version.status_loaded_prefix"]} {version.Timestamp:dd.MM.yyyy HH:mm} {Loc["editor.version.status_loaded_suffix"]}";
|
|
|
|
|
showVersionBrowser = false;
|
|
|
|
|
showVersionDiff = false;
|
|
|
|
|
_selectedVersion = null;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
statusText = $"{Loc["common.error"]} {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
private async Task WrapSelection(string prefix, string suffix)
|
|
|
|
|
=> await JS.InvokeVoidAsync("EditorToolbar.wrapSelection", prefix, suffix);
|
|
|
|
|
|
|
|
|
|
private async Task InsertHeading(int level)
|
|
|
|
|
=> await JS.InvokeVoidAsync("EditorToolbar.insertHeading", level);
|
|
|
|
|
|
|
|
|
|
private async Task InsertList(bool ordered)
|
|
|
|
|
=> await JS.InvokeVoidAsync("EditorToolbar.insertList", ordered);
|
|
|
|
|
|
|
|
|
|
private async Task IndentLines()
|
|
|
|
|
=> await JS.InvokeVoidAsync("EditorToolbar.indentLines");
|
|
|
|
|
|
|
|
|
|
private async Task OutdentLines()
|
|
|
|
|
=> await JS.InvokeVoidAsync("EditorToolbar.outdentLines");
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private void RefreshLatestVersion()
|
|
|
|
|
{
|
|
|
|
|
latestVersion = null;
|
|
|
|
|
if (string.IsNullOrEmpty(currentFile)) return;
|
|
|
|
|
|
|
|
|
|
var dir = Path.GetDirectoryName(currentFile)!;
|
|
|
|
|
var name = Path.GetFileNameWithoutExtension(currentFile);
|
|
|
|
|
var ext = Path.GetExtension(currentFile);
|
|
|
|
|
var versionsDir = Path.Combine(dir, "_versions");
|
|
|
|
|
if (!Directory.Exists(versionsDir)) return;
|
|
|
|
|
|
|
|
|
|
var pattern = $"{name}_*{ext}";
|
|
|
|
|
var files = Directory.GetFiles(versionsDir, pattern).OrderByDescending(f => f).ToList();
|
|
|
|
|
if (files.Count == 0) return;
|
|
|
|
|
|
|
|
|
|
var latest = files.First();
|
|
|
|
|
var metaFile = latest + ".meta";
|
|
|
|
|
string? comment = null;
|
|
|
|
|
if (File.Exists(metaFile))
|
|
|
|
|
{
|
|
|
|
|
var lines = File.ReadAllLines(metaFile);
|
|
|
|
|
if (lines.Length > 0 && lines[0].StartsWith("Kommentar: "))
|
|
|
|
|
comment = lines[0]["Kommentar: ".Length..];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fileName = Path.GetFileNameWithoutExtension(latest);
|
|
|
|
|
var stamp = fileName[(name.Length + 1)..];
|
|
|
|
|
var parsed = DateTime.TryParseExact(stamp, "yyyyMMdd_HHmmss", null,
|
|
|
|
|
System.Globalization.DateTimeStyles.None, out var dt);
|
|
|
|
|
latestVersion = new VersionEntry(
|
|
|
|
|
latest,
|
|
|
|
|
parsed ? dt : File.GetLastWriteTime(latest),
|
|
|
|
|
comment
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|