AuthorBuddy/Components/Layout/MainLayout.razor

648 lines
23 KiB
Text
Raw Normal View History

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
@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-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>
<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>
@if (_currentUserRole == UserRole.Administrator)
{
<div class="nav-separator"></div>
<button class="nav-item" @onclick='() => NavigateTo("/user-management")'>@Loc["nav.menu.extras.user_management"]</button>
}
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-20 06:12:54 +02:00
</span>
</div>
<div class="nav-section-body">
@foreach (var item in fileTree)
{
<div class="tree-item @(item.IsDirectory ? "dir" : "file") @(item.FullPath == currentFilePath ? "active" : "")"
@onclick="() => OpenFile(item)">
<span class="tree-icon">@(item.IsDirectory ? "📁" : "📄")</span>
<span class="tree-name">@item.Name</span>
@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-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>
@foreach (var child in item.Children)
{
<div class="tree-item file tree-child @(child.FullPath == currentFilePath ? "active" : "")"
@onclick="() => OpenFile(child)">
<span class="tree-icon">📄</span>
<span class="tree-name">@child.Name</span>
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>
}
}
</div>
</div>
}
@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"]" />
</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>
<button class="sidebar-info-btn" @onclick="ToggleInfoOverlay" title="Info">i</button>
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">
@Body
</main>
</div>
<footer class="status-bar">
<span class="status-dot connected"></span>
<span class="status-text">@statusText</span>
@if (isBusy)
{
<progress value="@operationProgress" max="100"></progress>
}
</footer>
</div>
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">&copy; 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;
private string? currentFilePath;
2026-05-14 12:31:35 +02:00
private string statusText = "Bereit";
private bool isBusy;
private double operationProgress;
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;
private string copyProjectName = string.Empty;
private string copyProjectPath = string.Empty;
private Project? copyProjectSource;
2026-05-20 07:27:41 +02:00
2026-05-14 12:31:35 +02:00
[Inject]
private NavigationManager Navigation { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
2026-05-20 06:12:54 +02:00
await SettingsService.InitializeAsync();
2026-05-25 06:38:16 +02:00
await Loc.InitAsync();
statusText = Loc["common.status.ready"];
2026-05-20 06:12:54 +02:00
Navigation.LocationChanged += OnLocationChanged;
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();
var first = users.FirstOrDefault();
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()
{
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)
{
await LoadFileTree();
}
else
{
fileTree = null;
}
}
private async Task LoadFileTree()
{
try
{
fileTree = await ProjectService.GetProjectFileTreeAsync(currentProjectId);
}
catch
{
fileTree = new();
}
}
2026-05-14 12:31:35 +02:00
private async Task LoadProjects()
{
try
{
projects = await ProjectService.GetAllProjectsAsync();
}
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;
copyProjectName = $"{project.Name} (Kopie)";
copyProjectPath = project.RootPath + "_Kopie";
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;
try
{
var userId = (await AuthService.GetCurrentUserAsync())?.Id;
if (userId == null) return;
statusText = $"{Loc["nav.status.project_copied"]} {copyProjectName}...";
isBusy = true;
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);
}
statusText = $"{Loc["nav.status.project_copied"]} {copyProjectName}";
}
catch (Exception ex)
{
statusText = $"{Loc["common.error"]} {ex.Message}";
}
finally
{
isBusy = false;
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;
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-25 06:38:16 +02:00
statusText = 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-25 06:38:16 +02:00
statusText = $"{Loc["nav.status.renamed_to"]} {renameNewName}";
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
}
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-25 06:38:16 +02:00
statusText = Loc["nav.error.file_exists"];
2026-05-20 06:12:54 +02:00
return;
}
File.WriteAllText(path, $"# {Path.GetFileNameWithoutExtension(path)}\n\n");
2026-05-25 06:38:16 +02:00
statusText = $"{Loc["nav.status.created"]} {Path.GetFileName(path)}";
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
}
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
}
public void SetStatus(string text, bool busy = false, double progress = 0)
{
statusText = text;
isBusy = busy;
operationProgress = progress;
StateHasChanged();
}
}