diff --git a/.github/workflows/publish-maui.yml b/.github/workflows/publish-maui.yml index 59ea433..6b984f8 100644 --- a/.github/workflows/publish-maui.yml +++ b/.github/workflows/publish-maui.yml @@ -73,7 +73,7 @@ jobs: - name: Zip MacCatalyst Release uses: vimtor/action-zip@v1.2 with: - files: MakerPrompt.MAUI/bin/Release/net8.0-maccatalyst + files: MakerPrompt.MAUI/bin/Release/net10.0-maccatalyst dest: maccatalyst-release.zip - name: Release diff --git a/MakerPrompt.Blazor/App.razor b/MakerPrompt.Blazor/App.razor index 67af70f..aca6f16 100644 --- a/MakerPrompt.Blazor/App.razor +++ b/MakerPrompt.Blazor/App.razor @@ -1,14 +1,12 @@ - - - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
-
\ No newline at end of file + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
\ No newline at end of file diff --git a/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj b/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj index 73a3a9e..aa38775 100644 --- a/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj +++ b/MakerPrompt.Blazor/MakerPrompt.Blazor.csproj @@ -1,4 +1,4 @@ - + net10.0 diff --git a/MakerPrompt.Blazor/Program.cs b/MakerPrompt.Blazor/Program.cs index 5db4987..459e150 100644 --- a/MakerPrompt.Blazor/Program.cs +++ b/MakerPrompt.Blazor/Program.cs @@ -1,6 +1,7 @@ using System.Globalization; using MakerPrompt.Blazor; using MakerPrompt.Blazor.Services; +using MakerPrompt.Blazor.Storage; using MakerPrompt.Shared.Utils; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -11,6 +12,7 @@ builder.RootComponents.Add("head::after"); builder.Services.RegisterMakerPromptSharedServices(); +builder.Services.AddScoped(); var host = builder.Build(); const string defaultCulture = "en-US"; diff --git a/MakerPrompt.Blazor/Services/WebSerialService.cs b/MakerPrompt.Blazor/Services/WebSerialService.cs index 1166085..5f4b407 100644 --- a/MakerPrompt.Blazor/Services/WebSerialService.cs +++ b/MakerPrompt.Blazor/Services/WebSerialService.cs @@ -1,5 +1,6 @@ using MakerPrompt.Shared.Infrastructure; using MakerPrompt.Shared.Models; +using MakerPrompt.Shared.Services; using Microsoft.JSInterop; namespace MakerPrompt.Blazor.Services @@ -42,14 +43,19 @@ public async Task> GetAvailablePortsAsync() public async Task DisconnectAsync() { + // Stop telemetry timer and detach handler first + updateTimer.Stop(); + updateTimer.Elapsed -= OnUpdateTimerElapsed; + if (_portReference != null) { var module = await _moduleTask.Value; await module.InvokeVoidAsync("closePort", _portReference); _portReference = null; - IsConnected = false; - RaiseConnectionChanged(); } + + IsConnected = false; + RaiseConnectionChanged(); } public async Task ConnectAsync(PrinterConnectionSettings connectionSettings) @@ -67,9 +73,34 @@ public async Task OpenPortAsync(string port, int baudRate, int dataBits = 8, _portReference = await module.InvokeAsync("openPort", options, _dotNetRef); IsConnected = true; ConnectionName = port; + + // Ensure only one subscription to the telemetry timer + updateTimer.Stop(); + updateTimer.Elapsed -= OnUpdateTimerElapsed; + updateTimer.Elapsed += OnUpdateTimerElapsed; + updateTimer.Start(); + RaiseConnectionChanged(); } + private async void OnUpdateTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + // Guard against callbacks after disconnect + if (!IsConnected || _portReference == null) + { + return; + } + + try + { + await GetPrinterTelemetryAsync(); + } + catch + { + // Swallow background telemetry errors + } + } + public override async Task WriteDataAsync(string data) { if (_portReference == null) throw new InvalidOperationException("Port not open"); @@ -77,6 +108,27 @@ public override async Task WriteDataAsync(string data) await module.InvokeVoidAsync("writeData", _portReference, data); } + public Task StartPrint(GCodeDoc gcodeDoc) + { + if (!IsConnected || string.IsNullOrEmpty(gcodeDoc.Content)) + { + return Task.CompletedTask; + } + + return Task.Run(async () => + { + await foreach (var command in gcodeDoc.EnumerateCommandsAsync()) + { + if (!IsConnected) + { + break; + } + + await WriteDataAsync(command); + } + }); + } + [JSInvokable] public void OnDataReceived(string data) { @@ -87,6 +139,10 @@ public void OnDataReceived(string data) public void OnConnectionChanged(bool isConnected) { IsConnected = isConnected; + if (!IsConnected) + { + updateTimer.Stop(); + } RaiseConnectionChanged(); } diff --git a/MakerPrompt.Blazor/Storage/BlazorAppLocalStorageProvider.cs b/MakerPrompt.Blazor/Storage/BlazorAppLocalStorageProvider.cs new file mode 100644 index 0000000..aaf6f55 --- /dev/null +++ b/MakerPrompt.Blazor/Storage/BlazorAppLocalStorageProvider.cs @@ -0,0 +1,104 @@ +using MakerPrompt.Shared.Models; +using MakerPrompt.Shared.Infrastructure; +using Microsoft.JSInterop; +using System.Text.Json; + +namespace MakerPrompt.Blazor.Storage +{ + public sealed class BlazorAppLocalStorageProvider : IAppLocalStorageProvider + { + private const string ManifestKey = "MakerPrompt.LocalFiles.Manifest"; + private const string FilePrefix = "MakerPrompt.LocalFiles.File."; + private readonly IJSRuntime js; + + public BlazorAppLocalStorageProvider(IJSRuntime jsRuntime) + { + js = jsRuntime; + } + + public string DisplayName => "App storage"; + public string Key => "app"; + public string RootPath => "localStorage://MakerPrompt/LocalFiles"; + + public async Task> ListFilesAsync(CancellationToken cancellationToken = default) + { + var json = await js.InvokeAsync("localStorage.getItem", ManifestKey); + var entries = string.IsNullOrEmpty(json) ? [] : (JsonSerializer.Deserialize>(json) ?? []); + return entries.Select(e => new FileEntry + { + FullPath = e.Name, + Size = e.Size, + ModifiedDate = e.Modified, + IsAvailable = true + }).ToList(); + } + + public async Task OpenReadAsync(string fullPath, CancellationToken cancellationToken = default) + { + var key = FilePrefix + fullPath; + var base64 = await js.InvokeAsync("localStorage.getItem", key); + if (string.IsNullOrEmpty(base64)) return null; + try + { + var bytes = Convert.FromBase64String(base64); + return new MemoryStream(bytes); + } + catch + { + return null; + } + } + + public async Task SaveFileAsync(string fullPath, Stream content, CancellationToken cancellationToken = default) + { + using var ms = new MemoryStream(); + await content.CopyToAsync(ms, cancellationToken); + var bytes = ms.ToArray(); + var base64 = Convert.ToBase64String(bytes); + var key = FilePrefix + fullPath; + await js.InvokeVoidAsync("localStorage.setItem", key, base64); + + var manifest = await GetManifestAsync(); + var existing = manifest.FirstOrDefault(m => m.Name == fullPath); + var now = DateTime.Now; + if (existing != null) + { + existing.Size = bytes.Length; + existing.Modified = now; + } + else + { + manifest.Add(new ManifestEntry { Name = fullPath, Size = bytes.Length, Modified = now }); + } + await SaveManifestAsync(manifest); + } + + public async Task DeleteFileAsync(string fullPath, CancellationToken cancellationToken = default) + { + var key = FilePrefix + fullPath; + await js.InvokeVoidAsync("localStorage.removeItem", key); + var manifest = await GetManifestAsync(); + manifest = manifest.Where(m => m.Name != fullPath).ToList(); + await SaveManifestAsync(manifest); + } + + private async Task> GetManifestAsync() + { + var json = await js.InvokeAsync("localStorage.getItem", ManifestKey); + return string.IsNullOrEmpty(json) ? [] : (JsonSerializer.Deserialize>(json) ?? []); + } + + private Task SaveManifestAsync(List entries) + { + var json = JsonSerializer.Serialize(entries); + return js.InvokeVoidAsync("localStorage.setItem", ManifestKey, json).AsTask(); + } + + private sealed class ManifestEntry + { + public string Name { get; set; } = string.Empty; + public int Size { get; set; } + public DateTime? Modified { get; set; } + } + } +} diff --git a/MakerPrompt.Blazor/_Imports.razor b/MakerPrompt.Blazor/_Imports.razor index 89a6219..cdc2dab 100644 --- a/MakerPrompt.Blazor/_Imports.razor +++ b/MakerPrompt.Blazor/_Imports.razor @@ -10,7 +10,6 @@ @using MakerPrompt.Blazor @using MakerPrompt.Shared.Layout @using MakerPrompt.Shared.Pages -@using MakerPrompt.Shared.Infrastructure; -@using MakerPrompt.Shared.Utils; -@using Blazored.Modal -@using Blazored.Modal.Services \ No newline at end of file +@using MakerPrompt.Shared.Infrastructure +@using MakerPrompt.Shared.Utils +@using BlazorBootstrap \ No newline at end of file diff --git a/MakerPrompt.Blazor/wwwroot/index.html b/MakerPrompt.Blazor/wwwroot/index.html index 688457d..cbb9a89 100644 --- a/MakerPrompt.Blazor/wwwroot/index.html +++ b/MakerPrompt.Blazor/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -9,11 +9,46 @@ - + + + @@ -37,6 +72,7 @@ + +