Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-maui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 12 additions & 14 deletions MakerPrompt.Blazor/App.razor
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<CascadingBlazoredModal>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="[typeof(MainLayout).Assembly]">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingBlazoredModal>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="[typeof(MainLayout).Assembly]">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
2 changes: 1 addition & 1 deletion MakerPrompt.Blazor/MakerPrompt.Blazor.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
Expand Down
2 changes: 2 additions & 0 deletions MakerPrompt.Blazor/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,6 +12,7 @@
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.RegisterMakerPromptSharedServices<AppConfigurationService, WebSerialService>();
builder.Services.AddScoped<MakerPrompt.Shared.Infrastructure.IAppLocalStorageProvider, BlazorAppLocalStorageProvider>();

var host = builder.Build();
const string defaultCulture = "en-US";
Expand Down
60 changes: 58 additions & 2 deletions MakerPrompt.Blazor/Services/WebSerialService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MakerPrompt.Shared.Infrastructure;
using MakerPrompt.Shared.Models;
using MakerPrompt.Shared.Services;
using Microsoft.JSInterop;

namespace MakerPrompt.Blazor.Services
Expand Down Expand Up @@ -42,14 +43,19 @@ public async Task<IEnumerable<string>> 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<bool> ConnectAsync(PrinterConnectionSettings connectionSettings)
Expand All @@ -67,16 +73,62 @@ public async Task OpenPortAsync(string port, int baudRate, int dataBits = 8,
_portReference = await module.InvokeAsync<IJSObjectReference>("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");
var module = await _moduleTask.Value;
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)
{
Expand All @@ -87,6 +139,10 @@ public void OnDataReceived(string data)
public void OnConnectionChanged(bool isConnected)
{
IsConnected = isConnected;
if (!IsConnected)
{
updateTimer.Stop();
}
RaiseConnectionChanged();
}

Expand Down
104 changes: 104 additions & 0 deletions MakerPrompt.Blazor/Storage/BlazorAppLocalStorageProvider.cs
Original file line number Diff line number Diff line change
@@ -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<List<FileEntry>> ListFilesAsync(CancellationToken cancellationToken = default)
{
var json = await js.InvokeAsync<string>("localStorage.getItem", ManifestKey);
var entries = string.IsNullOrEmpty(json) ? [] : (JsonSerializer.Deserialize<List<ManifestEntry>>(json) ?? []);
return entries.Select(e => new FileEntry
{
FullPath = e.Name,
Size = e.Size,
ModifiedDate = e.Modified,
IsAvailable = true
}).ToList();
}

public async Task<Stream?> OpenReadAsync(string fullPath, CancellationToken cancellationToken = default)
{
var key = FilePrefix + fullPath;
var base64 = await js.InvokeAsync<string>("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<List<ManifestEntry>> GetManifestAsync()
{
var json = await js.InvokeAsync<string>("localStorage.getItem", ManifestKey);
return string.IsNullOrEmpty(json) ? [] : (JsonSerializer.Deserialize<List<ManifestEntry>>(json) ?? []);
}

private Task SaveManifestAsync(List<ManifestEntry> 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; }
}
}
}
7 changes: 3 additions & 4 deletions MakerPrompt.Blazor/_Imports.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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
@using MakerPrompt.Shared.Infrastructure
@using MakerPrompt.Shared.Utils
@using BlazorBootstrap
40 changes: 38 additions & 2 deletions MakerPrompt.Blazor/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">

<head>
Expand All @@ -9,11 +9,46 @@
<meta name="description" content="Cross-platform interface for 3D printer management. Supports Prusalink, Moonraker, and direct serial control. Developed at x-hain hackspace using Blazor Hybrid.">
<meta name="keywords" content="3D printer management, 3D printer maintenance, Blazor Hybrid, open source 3D printing, Pronterface alternative, Prusalink, Moonraker, Web Serial control" />
<meta name="robots" content="index, follow">
<meta name="author" content="Akın Bender"/>
<meta name="author" content="Akın Bender" />
<base href="/" />
<script>
(function () {
try {
var configJson = window.localStorage.getItem('AppConfig');
var themeKey = 'auto';
if (configJson) {
var config = JSON.parse(configJson);
if (config && config.Theme !== undefined && config.Theme !== null) {
var raw = config.Theme;
if (typeof raw === 'string') {
themeKey = raw.toLowerCase();
} else if (typeof raw === 'number') {
switch (raw) {
case 1: themeKey = 'light'; break; // Theme.Light
case 2: themeKey = 'dark'; break; // Theme.Dark
case 3: themeKey = 'mpdark'; break; // Theme.MpDark
default: themeKey = 'auto'; break; // Theme.Auto or unknown
}
}
}
}

var resolvedTheme = themeKey;
if (!resolvedTheme || resolvedTheme === 'auto') {
resolvedTheme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}

document.documentElement.setAttribute('data-bs-theme', resolvedTheme);
document.documentElement.style.colorScheme = resolvedTheme;
} catch { }
})();
</script>
<link rel="stylesheet" href="MakerPrompt.Blazor.styles.css" />
<link rel="stylesheet" href="_content/MakerPrompt.Shared/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="_content/MakerPrompt.Shared/lib/bootstrap-icons/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="_content/Blazor.Bootstrap/blazor.bootstrap.css" />
<link rel="stylesheet" href="_content/MakerPrompt.Shared/css/app.css" />
<link rel="icon" type="image/png" href="_content/MakerPrompt.Shared/favicon.png" />
<link href="manifest.webmanifest" rel="manifest" />
Expand All @@ -37,6 +72,7 @@
<script src="_content/MakerPrompt.Shared/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="_content/MakerPrompt.Shared/lib/gcode-viewer/_bundles/gcode-viewer.min.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
<script>
window.blazorCulture = {
Expand Down
Loading
Loading