2026-05-14 12:31:35 +02:00
|
|
|
@inherits LayoutComponentBase
|
2026-05-20 06:12:54 +02:00
|
|
|
@implements IDisposable
|
2026-05-14 12:31:35 +02:00
|
|
|
@inject IProjectService ProjectService
|
2026-05-16 16:13:35 +02:00
|
|
|
@inject ISettingsService SettingsService
|
|
|
|
|
@inject IThemeService ThemeService
|
2026-05-31 19:31:27 +02:00
|
|
|
@inject IOllamaSetupService SetupService
|
2026-05-16 16:13:35 +02:00
|
|
|
@inject OllamaSettings OllamaSettings
|
2026-05-21 11:04:42 +02:00
|
|
|
@inject IAuthService AuthService
|
|
|
|
|
@inject CustomAuthStateProvider AuthStateProvider
|
2026-05-25 06:38:16 +02:00
|
|
|
@inject ILocalizationService Loc
|
2026-05-31 19:31:27 +02:00
|
|
|
@inject OperationState OpState
|
|
|
|
|
@using AuthorBuddy.Web.Models
|
2026-05-14 12:31:35 +02:00
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
@if (_isLoading)
|
|
|
|
|
{
|
|
|
|
|
<div class="splash-screen">
|
|
|
|
|
<div class="splash-bg" style="background-image:url('/Assets/splash.jpg')"></div>
|
|
|
|
|
<div class="splash-content">
|
|
|
|
|
<div class="splash-logo">
|
|
|
|
|
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="splash-loader"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<div class="page">
|
2026-05-14 12:31:35 +02:00
|
|
|
<div class="main-content">
|
2026-05-21 11:04:42 +02:00
|
|
|
@if (_isAuthenticated)
|
|
|
|
|
{
|
2026-05-14 12:31:35 +02:00
|
|
|
<aside class="sidebar">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="sidebar-logo">
|
|
|
|
|
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
|
|
|
|
</div>
|
2026-05-14 12:31:35 +02:00
|
|
|
<div class="nav-section">
|
|
|
|
|
<button class="nav-section-header" @onclick='() => ToggleSection("file")'>
|
|
|
|
|
<span class="nav-icon">@(activeSection == "file" ? "▼" : "▶")</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["nav.menu.file"]
|
2026-05-14 12:31:35 +02:00
|
|
|
</button>
|
|
|
|
|
@if (activeSection == "file")
|
|
|
|
|
{
|
|
|
|
|
<div class="nav-section-body">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick="CreateNewProject">@Loc["nav.menu.file.new_project"]</button>
|
|
|
|
|
<button class="nav-item" @onclick="OpenEditor">@Loc["nav.menu.file.open_editor"]</button>
|
2026-05-14 12:31:35 +02:00
|
|
|
<div class="nav-separator"></div>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick="ExitApp">@Loc["nav.menu.file.exit"]</button>
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="nav-section">
|
|
|
|
|
<button class="nav-section-header" @onclick='() => ToggleSection("extras")'>
|
|
|
|
|
<span class="nav-icon">@(activeSection == "extras" ? "▼" : "▶")</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["nav.menu.extras"]
|
2026-05-14 12:31:35 +02:00
|
|
|
</button>
|
|
|
|
|
@if (activeSection == "extras")
|
|
|
|
|
{
|
|
|
|
|
<div class="nav-section-body">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/settings")'>@Loc["nav.menu.extras.settings"]</button>
|
|
|
|
|
@if (_currentUserRole == UserRole.Administrator)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/models")'>@Loc["nav.menu.extras.manage_models"]</button>
|
|
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/assistant")'>@Loc["nav.menu.extras.assistant"]</button>
|
|
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/setup-assistant")'>Setup Assistent</button>
|
|
|
|
|
<div class="nav-separator"></div>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/user-management")'>@Loc["nav.menu.extras.user_management"]</button>
|
2026-05-31 19:31:27 +02:00
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/db-admin")'>📊 DB-Admin</button>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<button class="nav-item" @onclick='() => NavigateTo("/assistant")'>@Loc["nav.menu.extras.assistant"]</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="nav-section">
|
|
|
|
|
<button class="nav-section-header" @onclick='() => ToggleSection("projects")'>
|
|
|
|
|
<span class="nav-icon">@(activeSection == "projects" ? "▼" : "▶")</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["nav.menu.projects"]
|
2026-05-14 12:31:35 +02:00
|
|
|
</button>
|
|
|
|
|
@if (activeSection == "projects")
|
|
|
|
|
{
|
|
|
|
|
<div class="nav-section-body">
|
2026-05-14 12:42:37 +02:00
|
|
|
<button class="nav-item nav-item-toggle" @onclick='() => showProjectList = !showProjectList'>
|
|
|
|
|
<span class="nav-icon">@(showProjectList ? "▼" : "▶")</span>
|
2026-05-25 06:38:16 +02:00
|
|
|
@Loc["nav.projects.list_title"]
|
2026-05-14 12:42:37 +02:00
|
|
|
</button>
|
|
|
|
|
@if (showProjectList)
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-14 12:42:37 +02:00
|
|
|
<div class="nav-sublist">
|
|
|
|
|
@if (projects == null)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<span class="nav-hint">@Loc["nav.projects.loading"]</span>
|
2026-05-14 12:42:37 +02:00
|
|
|
}
|
|
|
|
|
else if (projects.Count == 0)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<span class="nav-hint">@Loc["nav.projects.empty"]</span>
|
2026-05-14 12:42:37 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
@foreach (var p in projects)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="nav-item nav-item-sub @(p.Id == currentProjectId ? "active" : "")">
|
|
|
|
|
<button class="nav-item-text" @onclick='() => SelectProject(p)'>@p.Name</button>
|
|
|
|
|
<button class="nav-item-icon" @onclick='() => BeginCopyProject(p)' title="@Loc["nav.projects.copy"]">⧉</button>
|
|
|
|
|
</div>
|
2026-05-14 12:42:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</div>
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
2026-05-14 12:42:37 +02:00
|
|
|
<div class="nav-separator"></div>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick="CreateNewProject">@Loc["nav.projects.create"]</button>
|
2026-05-14 12:31:35 +02:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
2026-05-20 06:12:54 +02:00
|
|
|
@if (currentProjectId > 0 && fileTree != null)
|
|
|
|
|
{
|
|
|
|
|
<div class="nav-section nav-section-open">
|
|
|
|
|
<div class="nav-section-header">
|
2026-05-25 06:38:16 +02:00
|
|
|
<span>@Loc["nav.projects.files"]</span>
|
2026-05-20 06:12:54 +02:00
|
|
|
<span class="nav-header-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item-icon" @onclick="RefreshFileTree" title="@Loc["nav.projects.refresh_tree"]">↻</button>
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (_selectedFiles.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
<button class="nav-item-icon" @onclick="() => _showExportDialog = true" title="Export (@(_selectedFiles.Count))">⬇</button>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="nav-section-body">
|
|
|
|
|
@foreach (var item in fileTree)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
var cat = item.Category;
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="tree-item @(item.IsDirectory ? "dir" : "file") @(item.FullPath == currentFilePath ? "active" : "")"
|
|
|
|
|
@onclick="() => OpenFile(item)">
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (!item.IsDirectory)
|
|
|
|
|
{
|
|
|
|
|
<input type="checkbox" class="tree-checkbox"
|
|
|
|
|
checked="@_selectedFiles.Contains(item.FullPath)"
|
|
|
|
|
@onclick:stopPropagation
|
|
|
|
|
@onchange="() => ToggleFileSelection(item.FullPath)" />
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
<span class="tree-icon">@(item.IsDirectory ? "📁" : "📄")</span>
|
|
|
|
|
<span class="tree-name">@item.Name</span>
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (cat != FileCategory.None)
|
|
|
|
|
{
|
|
|
|
|
<span class="tree-category-badge @FileCategoryHelper.CssClass(cat)" title="@FileCategoryHelper.DisplayName(cat)">@FileCategoryHelper.DisplayIcon(cat)</span>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
@if (item.IsDirectory)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="tree-rename" @onclick="() => BeginNewFile(item)" @onclick:stopPropagation title="@Loc["nav.projects.file.new"]">+</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
<button class="tree-cat-btn" @onclick="() => ToggleCategoryEditor(item)" @onclick:stopPropagation title="Kategorie">🏷</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="tree-rename" @onclick="() => BeginRename(item)" @onclick:stopPropagation title="@Loc["nav.projects.file.rename"]">✎</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
</div>
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (_editingCategoryPath == item.FullPath)
|
|
|
|
|
{
|
|
|
|
|
<div class="tree-cat-editor" @onclick:stopPropagation>
|
|
|
|
|
<CategorySelector Current="@item.Category" ProjectId="@currentProjectId" FilePath="@item.FullPath" OnCategoryChanged="@(v => OnCategoryChanged(item, v))" />
|
|
|
|
|
<button class="tree-cat-close" @onclick="() => _editingCategoryPath = null">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
@foreach (var child in item.Children)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
var ccat = child.Category;
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="tree-item file tree-child @(child.FullPath == currentFilePath ? "active" : "")"
|
|
|
|
|
@onclick="() => OpenFile(child)">
|
2026-05-31 19:31:27 +02:00
|
|
|
<input type="checkbox" class="tree-checkbox"
|
|
|
|
|
checked="@_selectedFiles.Contains(child.FullPath)"
|
|
|
|
|
@onclick:stopPropagation
|
|
|
|
|
@onchange="() => ToggleFileSelection(child.FullPath)" />
|
2026-05-20 06:12:54 +02:00
|
|
|
<span class="tree-icon">📄</span>
|
|
|
|
|
<span class="tree-name">@child.Name</span>
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (ccat != FileCategory.None)
|
|
|
|
|
{
|
|
|
|
|
<span class="tree-category-badge @FileCategoryHelper.CssClass(ccat)" title="@FileCategoryHelper.DisplayName(ccat)">@FileCategoryHelper.DisplayIcon(ccat)</span>
|
|
|
|
|
}
|
|
|
|
|
<button class="tree-cat-btn" @onclick="() => ToggleCategoryEditor(child)" @onclick:stopPropagation title="Kategorie">🏷</button>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="tree-rename" @onclick="() => BeginRename(child)" @onclick:stopPropagation title="@Loc["nav.projects.file.rename"]">✎</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (_editingCategoryPath == child.FullPath)
|
|
|
|
|
{
|
|
|
|
|
<div class="tree-cat-editor" @onclick:stopPropagation>
|
|
|
|
|
<CategorySelector Current="@child.Category" ProjectId="@currentProjectId" FilePath="@child.FullPath" OnCategoryChanged="@(v => OnCategoryChanged(child, v))" />
|
|
|
|
|
<button class="tree-cat-close" @onclick="() => _editingCategoryPath = null">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (_showExportDialog)
|
|
|
|
|
{
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog" style="min-width:360px;">
|
|
|
|
|
<div class="modal-title">@Loc["export.title"]</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<p style="margin-bottom:12px;">@Loc["export.file_count"]: <strong>@_selectedFiles.Count</strong></p>
|
|
|
|
|
<div class="form-group" style="margin-bottom:12px;">
|
|
|
|
|
<label>@Loc["export.label.filename"]</label>
|
|
|
|
|
<input @bind="exportFileName" placeholder="@Loc["export.placeholder.filename"]" />
|
|
|
|
|
<small class="form-hint">@Loc["export.hint.filename"]</small>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>@Loc["export.label.format"]</label>
|
|
|
|
|
<select class="form-select" @bind="exportFormat">
|
|
|
|
|
<option value="Html">HTML (.html)</option>
|
|
|
|
|
<option value="Markdown">@Loc["export.format.markdown"] (.md)</option>
|
|
|
|
|
<option value="Zip">@Loc["export.format.zip"] (.zip)</option>
|
|
|
|
|
<option value="Docx">DOCX (.docx)</option>
|
|
|
|
|
<option value="Pdf">PDF (.pdf)</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button @onclick='() => _showExportDialog = false'>@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="DoExport" class="primary">@Loc["export.button.export"]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
@if (showRenameDialog)
|
|
|
|
|
{
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="modal-title">@Loc["nav.rename.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["nav.rename.label.new_name"]</label>
|
|
|
|
|
<input @bind="renameNewName" placeholder="@Loc["nav.rename.placeholder"]" />
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="CancelRename">@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="ConfirmRename" disabled="@(string.IsNullOrWhiteSpace(renameNewName))" class="primary">@Loc["nav.rename.button.confirm"]</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (showCopyProjectDialog)
|
|
|
|
|
{
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-title">@Loc["nav.copy_project.title"]</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>@Loc["nav.copy_project.label.name"]</label>
|
|
|
|
|
<input @bind="copyProjectName" placeholder="@Loc["nav.copy_project.placeholder.name"]" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label>@Loc["nav.copy_project.label.path"]</label>
|
|
|
|
|
<input @bind="copyProjectPath" placeholder="@Loc["nav.copy_project.placeholder.path"]" />
|
2026-05-31 19:31:27 +02:00
|
|
|
<small class="form-hint">Erlaubter Pfad: @ProjectPathHelper.AllowedBasePath</small>
|
2026-05-25 06:38:16 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button @onclick="CancelCopyProject">@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="ConfirmCopyProject" disabled="@(string.IsNullOrWhiteSpace(copyProjectName) || string.IsNullOrWhiteSpace(copyProjectPath))" class="primary">@Loc["nav.copy_project.button.copy"]</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (showNewFileDialog)
|
|
|
|
|
{
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-dialog">
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="modal-title">@Loc["nav.newfile.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["nav.newfile.label.filename"]</label>
|
|
|
|
|
<input @bind="newFileName" placeholder="@Loc["nav.newfile.placeholder"]" />
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="CancelNewFile">@Loc["common.cancel"]</button>
|
|
|
|
|
<button @onclick="ConfirmNewFile" disabled="@(string.IsNullOrWhiteSpace(newFileName))" class="primary">@Loc["nav.newfile.button.create"]</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-21 11:04:42 +02:00
|
|
|
|
|
|
|
|
@if (!string.IsNullOrEmpty(_username))
|
|
|
|
|
{
|
|
|
|
|
<div class="nav-section" style="border-top:1px solid var(--border-color);margin-top:8px;padding-top:4px;">
|
|
|
|
|
<div class="nav-section-header" style="cursor:default;">
|
2026-05-25 06:38:16 +02:00
|
|
|
<span style="font-size:11px;font-weight:normal;">@Loc["nav.user.logged_in_as"] @_username</span>
|
2026-05-21 11:04:42 +02:00
|
|
|
</div>
|
|
|
|
|
<div class="nav-section-body">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button class="nav-item" @onclick="NavigateToSetup2FA">@Loc["nav.user.manage_2fa"]</button>
|
|
|
|
|
<button class="nav-item" @onclick="Logout">@Loc["nav.user.logout"]</button>
|
2026-05-21 11:04:42 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
<div class="sidebar-spacer"></div>
|
2026-05-31 19:31:27 +02:00
|
|
|
<div class="sidebar-bottom-btns">
|
|
|
|
|
<button class="sidebar-info-btn" @onclick="ToggleInfoOverlay" title="Info">i</button>
|
|
|
|
|
<button class="sidebar-info-btn" @onclick='() => Navigation.NavigateTo("/help")' title="@Loc["nav.menu.extras.help"]">?</button>
|
|
|
|
|
</div>
|
2026-05-25 06:38:16 +02:00
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
</aside>
|
2026-05-21 11:04:42 +02:00
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
<main class="detail-panel">
|
2026-05-31 19:31:27 +02:00
|
|
|
@if (currentProjectId > 0)
|
|
|
|
|
{
|
|
|
|
|
<div class="main-header">
|
|
|
|
|
<h2>@currentProjectName</h2>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
@Body
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<footer class="status-bar">
|
|
|
|
|
<span class="status-dot connected"></span>
|
2026-05-31 19:31:27 +02:00
|
|
|
<span class="status-text">@OpState.StatusText</span>
|
|
|
|
|
@if (OpState.IsBusy)
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
<progress value="@OpState.Progress" max="100"></progress>
|
|
|
|
|
}
|
|
|
|
|
@if (!string.IsNullOrEmpty(OpState.ThoughtText))
|
|
|
|
|
{
|
|
|
|
|
<span class="status-thought">@OpState.ThoughtText</span>
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
</footer>
|
|
|
|
|
</div>
|
2026-05-25 06:38:16 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
<style>
|
|
|
|
|
.tree-category-badge {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.tree-cat-btn {
|
|
|
|
|
background: none;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: 0 2px;
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
.tree-cat-btn:hover {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
.tree-cat-editor {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 4px 8px 4px 24px;
|
|
|
|
|
background: var(--bg-card, #f0f0f0);
|
|
|
|
|
border-bottom: 1px solid var(--border-color, #ddd);
|
|
|
|
|
}
|
|
|
|
|
.tree-cat-close {
|
|
|
|
|
background: none;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted, #999);
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
}
|
|
|
|
|
.tree-cat-close:hover {
|
|
|
|
|
color: var(--text-main, #333);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
@if (_showInfoOverlay)
|
|
|
|
|
{
|
|
|
|
|
<div class="splash-screen" @onclick="CloseInfoOverlay">
|
|
|
|
|
<div class="splash-bg" style="background-image:url('/Assets/splash.jpg')"></div>
|
|
|
|
|
<div class="splash-content info-content" @onclick:stopPropagation>
|
|
|
|
|
<div class="splash-logo">
|
|
|
|
|
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
|
|
|
|
</div>
|
|
|
|
|
<h1>Author Buddy</h1>
|
|
|
|
|
<p class="info-version">Version 1.0</p>
|
|
|
|
|
<p class="info-copyright">© 2026 Author Buddy. Alle Rechte vorbehalten.</p>
|
|
|
|
|
<p class="info-homepage">
|
|
|
|
|
<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>
|
|
|
|
|
</p>
|
|
|
|
|
<button class="info-close-btn" @onclick="CloseInfoOverlay">Schließen</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
@code {
|
2026-05-21 11:04:42 +02:00
|
|
|
private string _username = string.Empty;
|
|
|
|
|
private bool _isAuthenticated;
|
2026-05-25 06:38:16 +02:00
|
|
|
private UserRole _currentUserRole;
|
|
|
|
|
private bool _isLoading = true;
|
|
|
|
|
private bool _showInfoOverlay;
|
2026-05-14 12:31:35 +02:00
|
|
|
private string? activeSection;
|
2026-05-14 12:42:37 +02:00
|
|
|
private bool showProjectList;
|
2026-05-14 12:31:35 +02:00
|
|
|
private List<Project>? projects;
|
2026-05-20 06:12:54 +02:00
|
|
|
private List<FileTreeItem>? fileTree;
|
|
|
|
|
private int currentProjectId;
|
2026-05-31 19:31:27 +02:00
|
|
|
private string? currentProjectName;
|
2026-05-20 06:12:54 +02:00
|
|
|
private string? currentFilePath;
|
2026-05-31 19:31:27 +02:00
|
|
|
private string copyProjectName = string.Empty;
|
|
|
|
|
private string copyProjectPath = string.Empty;
|
|
|
|
|
private Project? copyProjectSource;
|
2026-05-20 06:12:54 +02:00
|
|
|
private bool showRenameDialog;
|
|
|
|
|
private string renameNewName = string.Empty;
|
|
|
|
|
private FileTreeItem? renameTarget;
|
|
|
|
|
private bool showNewFileDialog;
|
|
|
|
|
private string newFileName = string.Empty;
|
|
|
|
|
private FileTreeItem? newFileParentDir;
|
2026-05-25 06:38:16 +02:00
|
|
|
private bool showCopyProjectDialog;
|
2026-05-31 19:31:27 +02:00
|
|
|
private Action? _opStateHandler;
|
|
|
|
|
private string? _editingCategoryPath;
|
|
|
|
|
private HashSet<string> _selectedFiles = new();
|
|
|
|
|
private bool _showExportDialog;
|
|
|
|
|
private string exportFileName = string.Empty;
|
|
|
|
|
private string exportFormat = "Html";
|
2026-05-20 07:27:41 +02:00
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
[Inject]
|
|
|
|
|
private NavigationManager Navigation { get; set; } = default!;
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
[Inject]
|
|
|
|
|
private IJSRuntime JS { get; set; } = default!;
|
|
|
|
|
|
|
|
|
|
[Inject]
|
|
|
|
|
private IExportService ExportService { get; set; } = default!;
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
_opStateHandler = () => InvokeAsync(StateHasChanged);
|
|
|
|
|
OpState.OnChange += _opStateHandler;
|
2026-05-20 06:12:54 +02:00
|
|
|
await SettingsService.InitializeAsync();
|
2026-05-25 06:38:16 +02:00
|
|
|
await Loc.InitAsync();
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus(Loc["common.status.ready"]);
|
2026-05-20 06:12:54 +02:00
|
|
|
Navigation.LocationChanged += OnLocationChanged;
|
2026-05-31 19:31:27 +02:00
|
|
|
|
|
|
|
|
// Startup Setup Check
|
|
|
|
|
_ = Task.Run(async () =>
|
|
|
|
|
{
|
|
|
|
|
var result = await SetupService.AnalyzeSettingsAsync();
|
|
|
|
|
if (!result.IsCorrect)
|
|
|
|
|
{
|
|
|
|
|
OpState.SetStatus("Ollama-Einstellungen suboptimal. Setup-Assistent empfohlen.", false);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-21 11:04:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadCurrentUser()
|
|
|
|
|
{
|
|
|
|
|
var user = await AuthService.GetCurrentUserAsync();
|
|
|
|
|
_username = user?.Name ?? string.Empty;
|
|
|
|
|
_isAuthenticated = user != null;
|
2026-05-25 06:38:16 +02:00
|
|
|
_currentUserRole = user?.Role ?? UserRole.Author;
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task AutoLoadLastProject()
|
|
|
|
|
{
|
|
|
|
|
if (currentProjectId > 0) return;
|
|
|
|
|
var lastId = await SettingsService.GetLastProjectIdAsync();
|
|
|
|
|
if (lastId == null) return;
|
|
|
|
|
var project = projects?.FirstOrDefault(p => p.Id == lastId.Value);
|
|
|
|
|
if (project != null)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
SettingsService.CurrentProjectId = project.Id;
|
|
|
|
|
await SettingsService.InitializeAsync();
|
2026-05-20 06:12:54 +02:00
|
|
|
await SelectProject(project);
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-16 16:13:35 +02:00
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
var themeChanged = OllamaSettings.Theme != ThemeService.Theme;
|
|
|
|
|
|
|
|
|
|
|
2026-05-16 16:13:35 +02:00
|
|
|
if (firstRender)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
await ThemeService.ApplyTheme(OllamaSettings.Theme);
|
2026-05-21 11:04:42 +02:00
|
|
|
|
|
|
|
|
// Auto-login runs AFTER circuit establishment (correct scoped services)
|
|
|
|
|
await LoadCurrentUser();
|
|
|
|
|
#if DEBUG
|
|
|
|
|
if (!_isAuthenticated && Program.DebugAutoLogin)
|
|
|
|
|
{
|
|
|
|
|
var users = await SettingsService.GetUsersAsync();
|
2026-05-31 19:31:27 +02:00
|
|
|
var first = users.FirstOrDefault(u => u.Role == UserRole.Administrator);
|
2026-05-21 11:04:42 +02:00
|
|
|
if (first != null)
|
|
|
|
|
{
|
|
|
|
|
await AuthService.AdminBypassLoginAsync(first.Id);
|
|
|
|
|
AuthStateProvider.NotifyStateChanged();
|
|
|
|
|
await LoadCurrentUser();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
if (_isAuthenticated)
|
|
|
|
|
{
|
|
|
|
|
await LoadProjects();
|
|
|
|
|
await LoadProjectContext();
|
|
|
|
|
await AutoLoadLastProject();
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
|
|
|
|
|
_isLoading = false;
|
2026-05-21 11:04:42 +02:00
|
|
|
StateHasChanged();
|
2026-05-16 16:13:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
public void Dispose()
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
if (_opStateHandler != null)
|
|
|
|
|
OpState.OnChange -= _opStateHandler;
|
2026-05-20 06:12:54 +02:00
|
|
|
Navigation.LocationChanged -= OnLocationChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
|
|
|
|
{
|
2026-05-21 11:04:42 +02:00
|
|
|
await LoadCurrentUser();
|
|
|
|
|
if (_isAuthenticated)
|
|
|
|
|
{
|
|
|
|
|
await LoadProjects();
|
|
|
|
|
await LoadProjectContext();
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadProjectContext()
|
|
|
|
|
{
|
|
|
|
|
currentProjectId = SettingsService.CurrentProjectId ?? 0;
|
|
|
|
|
currentFilePath = SettingsService.CurrentFilePath;
|
|
|
|
|
|
|
|
|
|
if (currentProjectId > 0)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
var project = await ProjectService.GetProjectAsync(currentProjectId);
|
|
|
|
|
currentProjectName = project?.Name;
|
2026-05-20 06:12:54 +02:00
|
|
|
await LoadFileTree();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
currentProjectName = null;
|
2026-05-20 06:12:54 +02:00
|
|
|
fileTree = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadFileTree()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
fileTree = await ProjectService.GetProjectFileTreeAsync(currentProjectId);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
fileTree = new();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
private void ToggleCategoryEditor(FileTreeItem item)
|
|
|
|
|
{
|
|
|
|
|
if (_editingCategoryPath == item.FullPath)
|
|
|
|
|
_editingCategoryPath = null;
|
|
|
|
|
else
|
|
|
|
|
_editingCategoryPath = item.FullPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnCategoryChanged(FileTreeItem item, FileCategory newCategory)
|
|
|
|
|
{
|
|
|
|
|
item.Category = newCategory;
|
|
|
|
|
_editingCategoryPath = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
private async Task LoadProjects()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
var currentUser = AuthService.CurrentUser;
|
|
|
|
|
int? filterUserId = currentUser?.Role == UserRole.Administrator ? null : currentUser?.Id;
|
|
|
|
|
projects = await ProjectService.GetAllProjectsAsync(filterUserId);
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
projects = new();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ToggleSection(string section)
|
|
|
|
|
{
|
|
|
|
|
activeSection = activeSection == section ? null : section;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ExitApp()
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
|
|
|
|
Navigation.NavigateTo("/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CreateNewProject()
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
2026-05-20 06:12:54 +02:00
|
|
|
Navigation.NavigateTo("/editor/newproject");
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private void BeginCopyProject(Project project)
|
|
|
|
|
{
|
|
|
|
|
copyProjectSource = project;
|
2026-05-31 19:31:27 +02:00
|
|
|
copyProjectName = project.Name + " (Kopie)";
|
|
|
|
|
copyProjectPath = ProjectPathHelper.SuggestPath(project.Name + "_Kopie");
|
2026-05-25 06:38:16 +02:00
|
|
|
showCopyProjectDialog = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CancelCopyProject()
|
|
|
|
|
{
|
|
|
|
|
showCopyProjectDialog = false;
|
|
|
|
|
copyProjectSource = null;
|
|
|
|
|
copyProjectName = string.Empty;
|
|
|
|
|
copyProjectPath = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ConfirmCopyProject()
|
|
|
|
|
{
|
|
|
|
|
if (copyProjectSource == null || string.IsNullOrWhiteSpace(copyProjectName) || string.IsNullOrWhiteSpace(copyProjectPath))
|
|
|
|
|
return;
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
if (!ProjectPathHelper.IsPathAllowed(copyProjectPath))
|
|
|
|
|
{
|
|
|
|
|
OpState.SetStatus($"⚠ Pfad nicht erlaubt. Verwende: {ProjectPathHelper.AllowedBasePath}", true);
|
|
|
|
|
showCopyProjectDialog = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var userId = (await AuthService.GetCurrentUserAsync())?.Id;
|
|
|
|
|
if (userId == null) return;
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["nav.status.project_copied"]} {copyProjectName}...", true);
|
2026-05-25 06:38:16 +02:00
|
|
|
StateHasChanged();
|
|
|
|
|
|
|
|
|
|
var newId = await ProjectService.CopyProjectAsync(copyProjectSource.Id, copyProjectName, copyProjectPath, userId.Value);
|
|
|
|
|
await LoadProjects();
|
|
|
|
|
|
|
|
|
|
var newProject = projects?.FirstOrDefault(p => p.Id == newId);
|
|
|
|
|
if (newProject != null)
|
|
|
|
|
{
|
|
|
|
|
await SelectProject(newProject);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["nav.status.project_copied"]} {copyProjectName}", true);
|
2026-05-25 06:38:16 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", true);
|
2026-05-25 06:38:16 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus(OpState.StatusText, false);
|
2026-05-25 06:38:16 +02:00
|
|
|
showCopyProjectDialog = false;
|
|
|
|
|
copyProjectSource = null;
|
|
|
|
|
copyProjectName = string.Empty;
|
|
|
|
|
copyProjectPath = string.Empty;
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
private void OpenEditor()
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
|
|
|
|
Navigation.NavigateTo("/editor");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void NavigateTo(string path)
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
|
|
|
|
Navigation.NavigateTo(path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
private void ToggleInfoOverlay()
|
|
|
|
|
{
|
|
|
|
|
_showInfoOverlay = !_showInfoOverlay;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseInfoOverlay()
|
|
|
|
|
{
|
|
|
|
|
_showInfoOverlay = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 11:04:42 +02:00
|
|
|
private void NavigateToSetup2FA()
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
|
|
|
|
Navigation.NavigateTo("/setup-2fa");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task Logout()
|
|
|
|
|
{
|
|
|
|
|
activeSection = null;
|
|
|
|
|
await AuthService.LogoutAsync();
|
|
|
|
|
_username = string.Empty;
|
|
|
|
|
AuthStateProvider.NotifyStateChanged();
|
|
|
|
|
Navigation.NavigateTo("/login", true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private async Task SelectProject(Project project)
|
|
|
|
|
{
|
|
|
|
|
activeSection = "files";
|
|
|
|
|
SettingsService.CurrentProjectId = project.Id;
|
|
|
|
|
currentProjectId = project.Id;
|
2026-05-31 19:31:27 +02:00
|
|
|
currentProjectName = project.Name;
|
2026-05-20 06:12:54 +02:00
|
|
|
SettingsService.CurrentFilePath = null;
|
|
|
|
|
await SettingsService.SaveLastProjectIdAsync(project.Id);
|
|
|
|
|
await SettingsService.LoadProjectSettingsAsync();
|
|
|
|
|
await LoadFileTree();
|
|
|
|
|
Navigation.NavigateTo($"/editor/index/{project.Id}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RefreshFileTree()
|
|
|
|
|
{
|
|
|
|
|
await LoadFileTree();
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BeginRename(FileTreeItem item)
|
|
|
|
|
{
|
|
|
|
|
renameTarget = item;
|
|
|
|
|
renameNewName = item.Name;
|
|
|
|
|
showRenameDialog = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CancelRename()
|
|
|
|
|
{
|
|
|
|
|
showRenameDialog = false;
|
|
|
|
|
renameTarget = null;
|
|
|
|
|
renameNewName = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ConfirmRename()
|
|
|
|
|
{
|
|
|
|
|
if (renameTarget == null || string.IsNullOrWhiteSpace(renameNewName)) return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var dir = Path.GetDirectoryName(renameTarget.FullPath)!;
|
|
|
|
|
var newPath = Path.Combine(dir, renameNewName);
|
|
|
|
|
if (File.Exists(newPath))
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus(Loc["nav.error.file_exists"]);
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
File.Move(renameTarget.FullPath, newPath);
|
|
|
|
|
|
|
|
|
|
if (currentFilePath == renameTarget.FullPath)
|
|
|
|
|
SettingsService.CurrentFilePath = newPath;
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["nav.status.renamed_to"]} {renameNewName}");
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}");
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
showRenameDialog = false;
|
|
|
|
|
renameTarget = null;
|
|
|
|
|
renameNewName = string.Empty;
|
|
|
|
|
await LoadFileTree();
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BeginNewFile(FileTreeItem dir)
|
|
|
|
|
{
|
|
|
|
|
newFileParentDir = dir;
|
|
|
|
|
newFileName = string.Empty;
|
|
|
|
|
showNewFileDialog = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CancelNewFile()
|
|
|
|
|
{
|
|
|
|
|
showNewFileDialog = false;
|
|
|
|
|
newFileParentDir = null;
|
|
|
|
|
newFileName = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ConfirmNewFile()
|
|
|
|
|
{
|
|
|
|
|
if (newFileParentDir == null || string.IsNullOrWhiteSpace(newFileName)) return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var path = Path.Combine(newFileParentDir.FullPath, newFileName);
|
|
|
|
|
if (!path.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
path += ".md";
|
|
|
|
|
if (File.Exists(path))
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus(Loc["nav.error.file_exists"]);
|
2026-05-20 06:12:54 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
File.WriteAllText(path, $"# {Path.GetFileNameWithoutExtension(path)}\n\n");
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["nav.status.created"]} {Path.GetFileName(path)}");
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}");
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
showNewFileDialog = false;
|
|
|
|
|
newFileParentDir = null;
|
|
|
|
|
newFileName = string.Empty;
|
|
|
|
|
await LoadFileTree();
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OpenFile(FileTreeItem item)
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
if (item.IsDirectory) return;
|
2026-05-14 12:31:35 +02:00
|
|
|
activeSection = null;
|
2026-05-20 06:12:54 +02:00
|
|
|
SettingsService.CurrentFilePath = item.FullPath;
|
|
|
|
|
Navigation.NavigateTo($"/editor/file/{currentProjectId}?t={DateTime.Now.Ticks}");
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
private void ToggleFileSelection(string path)
|
2026-05-14 12:31:35 +02:00
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
if (!_selectedFiles.Remove(path))
|
|
|
|
|
_selectedFiles.Add(path);
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|
2026-05-31 19:31:27 +02:00
|
|
|
|
|
|
|
|
private async Task DoExport()
|
|
|
|
|
{
|
|
|
|
|
var format = exportFormat switch
|
|
|
|
|
{
|
|
|
|
|
"Html" => ExportFormat.Html,
|
|
|
|
|
"Markdown" => ExportFormat.Markdown,
|
|
|
|
|
"Zip" => ExportFormat.Zip,
|
|
|
|
|
"Docx" => ExportFormat.Docx,
|
|
|
|
|
"Pdf" => ExportFormat.Pdf,
|
|
|
|
|
_ => ExportFormat.Html
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var ext = format switch
|
|
|
|
|
{
|
|
|
|
|
ExportFormat.Html => ".html",
|
|
|
|
|
ExportFormat.Markdown => ".md",
|
|
|
|
|
ExportFormat.Zip => ".zip",
|
|
|
|
|
ExportFormat.Docx => ".docx",
|
|
|
|
|
ExportFormat.Pdf => ".pdf",
|
|
|
|
|
_ => ".html"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var name = string.IsNullOrWhiteSpace(exportFileName)
|
|
|
|
|
? $"Export_{DateTime.Now:yyyyMMdd_HHmmss}"
|
|
|
|
|
: exportFileName;
|
|
|
|
|
|
|
|
|
|
var data = await ExportService.ExportAsync(currentProjectId, _selectedFiles.ToList(), format);
|
|
|
|
|
|
|
|
|
|
var mime = format switch
|
|
|
|
|
{
|
|
|
|
|
ExportFormat.Html => "text/html",
|
|
|
|
|
ExportFormat.Markdown => "text/markdown",
|
|
|
|
|
ExportFormat.Zip => "application/zip",
|
|
|
|
|
ExportFormat.Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
|
|
|
ExportFormat.Pdf => "application/pdf",
|
|
|
|
|
_ => "text/html"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await JS.InvokeVoidAsync("downloadFile", $"{name}{ext}", Convert.ToBase64String(data), mime);
|
|
|
|
|
|
|
|
|
|
_showExportDialog = false;
|
|
|
|
|
_selectedFiles.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 12:31:35 +02:00
|
|
|
}
|