Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,555 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using MudBlazor
@using RobotNet10.ScriptEditor.Clients
@using RobotNet10.ScriptEditor.Services
@using RobotNet10.ScriptEditor.Models
@using RobotNet10.ScriptEditor.Components
@using RobotNet10.ScriptEditor.Dialogs
@using RobotNet10.ScriptEngine.Shared
@inject FileManagerHubClient FileManagerClient
@inject ScriptManagerHubClient ScriptManagerClient
@inject ScriptWorkspace Workspace
@inject IJSRuntime JSRuntime
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@implements IAsyncDisposable
<SidebarAccordionItem TabName="workspace" Label="WORKSPACE">
<HeaderActions>
<IconButton Icon="folder-plus" Title="Add Folder" OnClick="HandleAddFolder" />
<IconButton Icon="file-plus" Title="Add File" OnClick="HandleAddFile" />
<IconButton Icon="content-save-all text-primary" Title="Save All" OnClick="HandleSaveAll" />
<IconButton Icon="package-down" Title="Backup" OnClick="HandleBackup" />
<IconButton Icon="package-up" Title="Restore" OnClick="HandleRestore" />
</HeaderActions>
<ChildContent>
<CascadingValue Value="@RadioName" Name="FileExplorerRadioName">
<div class="file-explorer-container" @onclick="HandleContainerClick" @oncontextmenu="HandleContainerRightClick" @oncontextmenu:preventDefault="true" @oncontextmenu:stopPropagation="true">
<div class="file-explorer-tree" @ref="TreeContainerRef">
@foreach (var folder in Workspace.Folders.OrderBy(f => f.Name))
{
<FolderExplorerItem Folder="@folder" @key="@folder.Path" />
}
@foreach (var file in Workspace.Files.OrderBy(f => f.Name))
{
<FileExplorerItem File="@file" @key="@file.Path" />
}
</div>
</div>
<MudMenu Class="flex-grow-1 w-100" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft" Size="@Size.Small" PositionAtCursor Dense ActivationEvent="@MouseEvent.RightClick">
<ActivatorContent>
<div class="w-100 h-100" id="@_contextMenuActivatorId" @ref="ContextMenuActivator"></div>
</ActivatorContent>
<ChildContent>
<MudMenuItem Icon="@Icons.Material.Filled.CreateNewFolder" OnClick="HandleCreateFolderFromContext">
Create Folder
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.NoteAdd" OnClick="HandleCreateFileFromContext">
Create File
</MudMenuItem>
</ChildContent>
</MudMenu>
</CascadingValue>
</ChildContent>
</SidebarAccordionItem>
@code {
private ElementReference TreeContainerRef;
private ElementReference ContextMenuActivator;
private readonly string _contextMenuActivatorId = $"context-menu-activator-{Guid.NewGuid()}";
private const string RadioName = "script-explorer-item";
private bool ShowContextMenu { get; set; }
private IJSObjectReference? _jsModule;
private DotNetObjectReference<FileExplorer>? _dotNetRef;
private double _contextMenuX;
private double _contextMenuY;
protected override void OnInitialized()
{
Workspace.RootChanged += OnWorkspaceRootChanged;
// Subscribe to FileManagerHubClient events to update workspace
FileManagerClient.FileCreated += OnFileCreated;
FileManagerClient.FileDeleted += OnFileDeleted;
FileManagerClient.FolderCreated += OnFolderCreated;
FileManagerClient.FolderDeleted += OnFolderDeleted;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./_content/RobotNet10.ScriptEditor/fileExplorer.js");
if (_jsModule != null)
{
await _jsModule.InvokeVoidAsync("init", TreeContainerRef);
// Register Esc key handler at document level
_dotNetRef = DotNetObjectReference.Create(this);
await _jsModule.InvokeVoidAsync("registerEscapeKeyHandler", _dotNetRef);
}
}
}
[JSInvokable]
public async Task HandleEscapeKey()
{
// Reset selection when Esc is pressed
Workspace.SelectedFile = null;
Workspace.SelectedFolder = null;
// Uncheck all radio buttons
if (_jsModule != null)
{
await _jsModule.InvokeVoidAsync("UncheckRadioByName", RadioName);
}
StateHasChanged();
}
private void OnWorkspaceRootChanged()
{
InvokeAsync(() =>
{
StateHasChanged();
});
}
private void HandleContainerClick(MouseEventArgs e)
{
// Clear selection when clicking on empty area (container itself, not children)
// Children will handle their own clicks and stop propagation
Workspace.SelectedFile = null;
Workspace.SelectedFolder = null;
StateHasChanged();
}
private async Task HandleContainerRightClick(MouseEventArgs e)
{
// Right click on empty area - show context menu
// Only show if clicking directly on container (not on file/folder items)
// Clear selection first
Workspace.SelectedFile = null;
Workspace.SelectedFolder = null;
// Store click position for context menu (use ClientX/ClientY for viewport coordinates)
_contextMenuX = e.ClientX;
_contextMenuY = e.ClientY;
// Update activator position via JavaScript
if (_jsModule != null)
{
await _jsModule.InvokeVoidAsync("setElementPosition", _contextMenuActivatorId, _contextMenuX, _contextMenuY);
}
// Update activator position and show menu
await InvokeAsync(() =>
{
ShowContextMenu = true;
StateHasChanged();
});
}
private async Task HandleCreateFolderFromContext()
{
ShowContextMenu = false;
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<CreateFolderDialog>("Create New Folder", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string folderName)
{
try
{
// Create folder at root level (no parent)
var newPath = folderName;
await FileManagerClient.CreateFolderAsync(newPath);
// Add folder to workspace at root level
var level = 1;
var folderDto = new ScriptFolderDto(folderName, level, [], []);
Workspace.AddFolder(folderDto, null);
}
catch (Exception ex)
{
Snackbar.Add($"Failed to create folder: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleCreateFileFromContext()
{
ShowContextMenu = false;
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<CreateFileDialog>("Create New File", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string fileName)
{
try
{
// Create file at root level (no parent)
var newPath = fileName;
await FileManagerClient.CreateFileAsync(newPath, "");
// Add file to workspace at root level
var level = 1;
var fileDto = new ScriptFileDto(fileName, level, "");
Workspace.AddFile(fileDto, null);
}
catch (Exception ex)
{
Snackbar.Add($"Failed to create file: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleAddFolder(MouseEventArgs e)
{
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<CreateFolderDialog>("Create New Folder", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string folderName)
{
try
{
var parentFolder = Workspace.SelectedFolder;
var parentPath = parentFolder?.Path ?? "";
var newPath = string.IsNullOrEmpty(parentPath) ? folderName : System.IO.Path.Combine(parentPath, folderName);
await FileManagerClient.CreateFolderAsync(newPath);
// Add folder to workspace directly
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
var folderDto = new ScriptFolderDto(folderName, level, [], []);
Workspace.AddFolder(folderDto, parentFolder);
}
catch (Exception ex)
{
Snackbar.Add($"Failed to create folder: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleAddFile(MouseEventArgs e)
{
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<CreateFileDialog>("Create New File", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string fileName)
{
try
{
var parentFolder = Workspace.SelectedFolder;
var parentPath = parentFolder?.Path ?? "";
var newPath = string.IsNullOrEmpty(parentPath) ? fileName : System.IO.Path.Combine(parentPath, fileName);
await FileManagerClient.CreateFileAsync(newPath, "");
// Add file to workspace directly
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
var fileDto = new ScriptFileDto(fileName, level, "");
Workspace.AddFile(fileDto, parentFolder);
// Expand parent folder if it exists (ChildrenChanged event will handle the expansion)
// The expansion will be handled by FolderExplorerItem.OnFolderChildrenChanged
}
catch (Exception ex)
{
Snackbar.Add($"Failed to create file: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleSaveAll(MouseEventArgs e)
{
// Save all modified files
var modifiedFiles = GetAllModifiedFiles(Workspace.Folders).Concat(Workspace.Files.Where(f => f.IsModified));
foreach (var file in modifiedFiles)
{
try
{
await FileManagerClient.SaveFileAsync(file.Path, file.Code);
file.Saved();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to save file '{file.Name}': {ex.Message}", Severity.Error);
}
}
StateHasChanged();
}
private IEnumerable<ScriptFile> GetAllModifiedFiles(IEnumerable<ScriptFolder> folders)
{
foreach (var folder in folders)
{
foreach (var file in folder.Files.Where(f => f.IsModified))
{
yield return file;
}
foreach (var modifiedFile in GetAllModifiedFiles(folder.Folders))
{
yield return modifiedFile;
}
}
}
private async Task HandleBackup(MouseEventArgs e)
{
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<CreateBackupDialog>("Create Backup", options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string backupName)
{
try
{
var backupFileName = await FileManagerClient.CreateBackupAsync(backupName);
Snackbar.Add($"Backup created successfully: {backupFileName}", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Failed to create backup: {ex.Message}", Severity.Error);
}
}
}
private async Task HandleRestore(MouseEventArgs e)
{
try
{
var backups = await FileManagerClient.ListBackupsAsync();
var parameters = new DialogParameters<RestoreBackupDialog>
{
{ x => x.Backups, backups }
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<RestoreBackupDialog>("Restore Backup", parameters, options);
var result = await dialog.Result;
if (result is not null && !result.Canceled && result.Data is string backupFileName)
{
try
{
await FileManagerClient.RestoreBackupAsync(backupFileName, replaceExisting: true);
// Request edit permission before reinitializing workspace
try
{
await FileManagerClient.RequestEditPermissionAsync();
}
catch (Exception)
{
// Ignore if edit permission cannot be requested (e.g., state is not Idle)
}
// Reinitialize workspace with new root folder
var rootFolder = await FileManagerClient.GetRootFolderAsync();
await Workspace.ReinitializeAsync(rootFolder);
// Reset selections
Workspace.SelectedFile = null;
Workspace.SelectedFolder = null;
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Failed to restore backup: {ex.Message}", Severity.Error);
}
}
}
catch (Exception ex)
{
Snackbar.Add($"Failed to list backups: {ex.Message}", Severity.Error);
}
}
private void OnFileCreated(string path, string? userId)
{
// Skip if this is our own action (we already updated workspace)
// Only handle actions from other clients
try
{
var file = Workspace.FindFileByPath(path);
if (file == null)
{
// File doesn't exist in workspace, need to add it
// Parse path to get name and parent
var pathParts = path.Split(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
var fileName = pathParts[^1];
var parentPath = pathParts.Length > 1
? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), pathParts.Take(pathParts.Length - 1))
: "";
var parentFolder = string.IsNullOrEmpty(parentPath) ? null : Workspace.FindFolderByPath(parentPath);
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
// Get file content from server (we need to read it)
// For now, create with empty content - it will be loaded when opened
var fileDto = new ScriptFileDto(fileName, level, "");
Workspace.AddFile(fileDto, parentFolder);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error updating workspace after file creation: {ex.Message}", Severity.Error);
}
}
private void OnFolderCreated(string path, string? userId)
{
// Skip if this is our own action (we already updated workspace)
// Only handle actions from other clients
try
{
var folder = Workspace.FindFolderByPath(path);
if (folder == null)
{
// Folder doesn't exist in workspace, need to add it
// Parse path to get name and parent
var pathParts = path.Split(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
var folderName = pathParts[^1];
var parentPath = pathParts.Length > 1
? string.Join(System.IO.Path.DirectorySeparatorChar.ToString(), pathParts.Take(pathParts.Length - 1))
: "";
var parentFolder = string.IsNullOrEmpty(parentPath) ? null : Workspace.FindFolderByPath(parentPath);
var level = parentFolder != null ? parentFolder.Level + 1 : 1;
var folderDto = new ScriptFolderDto(folderName, level, [], []);
Workspace.AddFolder(folderDto, parentFolder);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error updating workspace after folder creation: {ex.Message}", Severity.Error);
}
}
private void OnFileDeleted(string path, string? userId)
{
// Remove file from workspace directly
InvokeAsync(() =>
{
try
{
var file = Workspace.FindFileByPath(path);
if (file != null)
{
Workspace.RemoveFile(file);
// RootChanged event will trigger OnWorkspaceRootChanged which calls StateHasChanged
// But we also call StateHasChanged here to ensure immediate UI update
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error updating workspace after file deletion: {ex.Message}", Severity.Error);
}
});
}
private void OnFolderDeleted(string path, string? userId)
{
// Remove folder from workspace directly
InvokeAsync(() =>
{
try
{
var folder = Workspace.FindFolderByPath(path);
if (folder != null)
{
Workspace.RemoveFolder(folder);
// RootChanged event will trigger OnWorkspaceRootChanged which calls StateHasChanged
// But we also call StateHasChanged here to ensure immediate UI update
StateHasChanged();
}
}
catch (Exception ex)
{
Snackbar.Add($"Error updating workspace after folder deletion: {ex.Message}", Severity.Error);
}
});
}
public async ValueTask DisposeAsync()
{
Workspace.RootChanged -= OnWorkspaceRootChanged;
// Unsubscribe from FileManagerHubClient events
FileManagerClient.FileCreated -= OnFileCreated;
FileManagerClient.FolderCreated -= OnFolderCreated;
FileManagerClient.FileDeleted -= OnFileDeleted;
FileManagerClient.FolderDeleted -= OnFolderDeleted;
// Unregister Esc key handler
if (_jsModule != null)
{
try
{
await _jsModule.InvokeVoidAsync("unregisterEscapeKeyHandler");
await _jsModule.DisposeAsync();
}
catch (JSDisconnectedException)
{
// Ignore if JS context is disconnected
}
}
_dotNetRef?.Dispose();
}
}