Files
BQP/srcs/RobotNet10/Components/RobotNet10.ScriptEditor/Components/FileExplorerItem.razor
2026-07-13 09:25:40 +07:00

271 lines
9.4 KiB
Plaintext

@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using MudBlazor
@using RobotNet10.ScriptEditor.Models
@using RobotNet10.ScriptEditor.Clients
@using RobotNet10.ScriptEditor.Services
@using RobotNet10.ScriptEditor.Dialogs
@inject FileManagerHubClient FileManagerClient
@inject ScriptWorkspace Workspace
@inject IDialogService DialogService
@inject IJSRuntime JSRuntime
@inject ISnackbar Snackbar
@implements IDisposable
<MudMenu Class="w-100" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft" Size="@Size.Small" PositionAtCursor Dense ActivationEvent="@MouseEvent.RightClick" @bind-Open="ShowContextMenu">
<ActivatorContent>
<div class="file-explorer-item"
data-file-path="@File.Path"
@onclick:stopPropagation="true"
@onclick:preventDefault="true"
@onclick="HandleClick"
@oncontextmenu="HandleClick">
<input id="@ItemId" type="radio" name="@RadioName" hidden />
<div class="file-item-content" data-file-id="@File.Id" data-level="@File.Level">
@for (int i = 0; i < File.Level - 1; i++)
{
<div class="file-indent-guide"></div>
}
<span class="file-icon-spacer"></span>
<span class="file-icon mdi mdi-language-csharp"></span>
<span class="file-name" data-file-id="@File.Id" data-name="name">@File.Name</span>
<span class="file-badge warning" data-file-id="@File.Id" data-badge="warning" data-count="@File.WarningCount" title="@File.WarningCount warning(s)">@File.WarningCount</span>
<span class="file-badge error" data-file-id="@File.Id" data-badge="error" data-count="@File.ErrorCount" title="@File.ErrorCount error(s)">@File.ErrorCount</span>
<span class="file-badge modified" data-file-id="@File.Id" data-badge="modified" title="Modified">●</span>
</div>
</div>
</ActivatorContent>
<ChildContent>
<MudMenuItem Icon="@Icons.Material.Filled.Edit" OnClick="HandleRename">
Rename
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Save" OnClick="HandleSave" Disabled="@(!File.IsModified)">
Save
</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="HandleDelete">
<MudText Color="Color.Error">Delete</MudText>
</MudMenuItem>
</ChildContent>
</MudMenu>
@code {
[CascadingParameter(Name = "FileExplorerRadioName")]
protected string RadioName { get; set; } = "script-explorer-item";
[Parameter, EditorRequired]
public ScriptFile File { get; set; } = null!;
private Guid ItemId = Guid.NewGuid();
private bool ShowContextMenu { get; set; }
private ScriptFile? _previousFile;
public override async Task SetParametersAsync(ParameterView parameters)
{
// Try to get the File parameter using nameof for type safety
if (parameters.TryGetValue<ScriptFile>(nameof(File), out var fileParameter))
{
// Unsubscribe from old file events if file changed
if (_previousFile != null && _previousFile != fileParameter)
{
_previousFile.Modified -= OnFileModified;
_previousFile.NameChanged -= OnFileNameChanged;
_previousFile.DiagnosticsChanged -= OnFileDiagnosticsChanged;
}
}
// Set parameters first
await base.SetParametersAsync(parameters);
// Subscribe to new file events using the parameter from TryGetValue
if (parameters.TryGetValue<ScriptFile>(nameof(File), out var newFile) && newFile != null)
{
// Only subscribe if this is a different file
if (_previousFile != newFile)
{
newFile.Modified += OnFileModified;
newFile.NameChanged += OnFileNameChanged;
newFile.DiagnosticsChanged += OnFileDiagnosticsChanged;
_previousFile = newFile;
// Update UI via JavaScript
await UpdateBadgesVisibility();
}
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Only update badges on first render or when explicitly needed
if (firstRender && File != null)
{
await UpdateBadgesVisibility();
}
// Radio button state is managed via CheckRadioById and UncheckRadioByName
}
private async Task UpdateBadgesVisibility()
{
try
{
var fileId = File.Id.ToString();
await JSRuntime.InvokeVoidAsync("fileExplorer.updateFileBadges",
fileId,
File.WarningCount > 0,
File.ErrorCount > 0,
File.IsModified,
File.WarningCount.ToString(),
File.ErrorCount.ToString(),
File.Name);
}
catch
{
// Ignore JS errors
}
}
private async Task HandleClick(MouseEventArgs e)
{
await JSRuntime.InvokeVoidAsync("fileExplorer.UncheckRadioByName", RadioName);
await JSRuntime.InvokeVoidAsync("fileExplorer.CheckRadioById", ItemId);
Workspace.SelectedFile = File;
StateHasChanged();
}
private void OnFileModified()
{
InvokeAsync(async () =>
{
await UpdateBadgesVisibility();
StateHasChanged();
});
}
private void OnFileNameChanged()
{
InvokeAsync(async () =>
{
await UpdateBadgesVisibility();
StateHasChanged();
});
}
private void OnFileDiagnosticsChanged(int warningCount, int errorCount)
{
InvokeAsync(async () =>
{
await UpdateBadgesVisibility();
StateHasChanged();
});
}
private async Task HandleRename()
{
ShowContextMenu = false;
var parameters = new DialogParameters<RenameDialog>
{
{ x => x.CurrentName, File.Name },
{ x => x.ItemType, "File" },
{ x => x.RequireCsExtension, true }
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<RenameDialog>("Rename File", parameters, options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string newName)
{
try
{
var parentPath = File.Parent?.Path ?? "";
var newPath = string.IsNullOrEmpty(parentPath) ? newName : System.IO.Path.Combine(parentPath, newName);
await FileManagerClient.CreateFileAsync(newPath, File.Code);
await FileManagerClient.DeleteFileAsync(File.Path);
File.Name = newName;
}
catch (Exception ex)
{
Snackbar.Add($"Failed to rename file: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleSave()
{
if (!File.IsModified) return;
ShowContextMenu = false;
try
{
await FileManagerClient.SaveFileAsync(File.Path, File.Code);
File.Saved();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to save file: {ex.Message}", Severity.Error);
}
StateHasChanged();
}
private async Task HandleDelete()
{
ShowContextMenu = false;
var parameters = new DialogParameters<ConfirmDialog>
{
{ x => x.Title, "Delete File" },
{ x => x.Message, $"Are you sure you want to delete '{File.Name}'?" },
{ x => x.ConfirmText, "Delete" },
{ x => x.ConfirmColor, Color.Error }
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<ConfirmDialog>("Delete File", parameters, options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is bool confirmed && confirmed)
{
try
{
await FileManagerClient.DeleteFileAsync(File.Path);
// Update workspace immediately after successful deletion
// (Server may not send FileDeleted event to the client that performed the deletion)
Workspace.RemoveFile(File);
}
catch (Exception ex)
{
Snackbar.Add($"Failed to delete file: {ex.Message}", Severity.Error);
}
}
}
public void Dispose()
{
if (File != null)
{
File.Modified -= OnFileModified;
File.NameChanged -= OnFileNameChanged;
File.DiagnosticsChanged -= OnFileDiagnosticsChanged;
}
}
}