Skip to content

Commit 72627f3

Browse files
Tom BrewerTom Brewer
authored andcommitted
chore: code cleanup
add storybook for save service add parity to webassembly registrations make simpler and more generic desktop extensions
1 parent 6e023ec commit 72627f3

File tree

9 files changed

+271
-46
lines changed

9 files changed

+271
-46
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Mythetech.Components.Desktop;
2+
3+
public enum DesktopHost
4+
{
5+
Photino,
6+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Mythetech.Components.Desktop.Photino;
3+
4+
namespace Mythetech.Components.Desktop;
5+
6+
/// <summary>
7+
/// Generic desktop service registration extensions
8+
/// </summary>
9+
public static class DesktopRegistrationExtensions
10+
{
11+
/// <summary>
12+
/// Adds required desktop services for a given host
13+
/// </summary>
14+
public static IServiceCollection AddDesktopServices(this IServiceCollection services, DesktopHost host = DesktopHost.Photino)
15+
{
16+
switch (host)
17+
{
18+
case DesktopHost.Photino:
19+
services.AddPhotinoServices();
20+
break;
21+
default:
22+
throw new ArgumentException("Invalid host type", nameof(host));
23+
}
24+
25+
return services;
26+
}
27+
}

Mythetech.Components.Desktop/Photino/PhotinoRegistrationExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ public static IServiceCollection AddPhotinoServices(this IServiceCollection serv
2020

2121
return services;
2222
}
23+
24+
/// <summary>
25+
/// Registers an instance of the running Photino App into the DI container for interop options
26+
/// </summary>
27+
public static PhotinoBlazorApp RegisterProvider(this PhotinoBlazorApp app)
28+
{
29+
var provider = app.Services;
30+
31+
return RegisterProvider(app, provider);
32+
}
2333

2434
/// <summary>
2535
/// Registers an instance of the running Photino App into the DI container for interop options

Mythetech.Components.Storybook/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
builder.Services.AddMudMarkdownServices();
2323

2424

25-
builder.Services.AddLinkOpeningService();
26-
builder.Services.AddFileOpenService();
25+
builder.Services.AddWebAssemblyServices();
2726

2827
builder.Services.AddMessageBus();
2928

3029
var host = builder.Build();
3130

3231
host.Services.UseMessageBus();
33-
32+
3433
await host.RunAsync();

Mythetech.Components.Storybook/Stories/FileServices.stories.razor

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<MudDivider Class="my-2" />
1717

1818
<MudGrid>
19-
<MudItem xs="12" md="6">
19+
<MudItem xs="12" md="4">
2020
<MudPaper Class="pa-4" Elevation="2">
2121
<MudText Typo="Typo.h6" Class="mb-3">Open Files</MudText>
2222

@@ -39,7 +39,7 @@
3939
</MudPaper>
4040
</MudItem>
4141

42-
<MudItem xs="12" md="6">
42+
<MudItem xs="12" md="4">
4343
<MudPaper Class="pa-4" Elevation="2">
4444
<MudText Typo="Typo.h6" Class="mb-3">Open Folder</MudText>
4545

@@ -51,6 +51,28 @@
5151
</MudStack>
5252
</MudPaper>
5353
</MudItem>
54+
55+
<MudItem xs="12" md="4">
56+
<MudPaper Class="pa-4" Elevation="2">
57+
<MudText Typo="Typo.h6" Class="mb-3">Save File</MudText>
58+
59+
<MudStack Spacing="2">
60+
<MudTextField @bind-Value="_saveFileName"
61+
Label="File Name"
62+
Variant="Variant.Outlined" />
63+
64+
<MudTextField @bind-Value="_saveContent"
65+
Label="Content"
66+
Variant="Variant.Outlined"
67+
Lines="3" />
68+
69+
<Button Text="Save File"
70+
Variant="Variant.Filled"
71+
Color="Color.Success"
72+
OnClick="@SaveFile" />
73+
</MudStack>
74+
</MudPaper>
75+
</MudItem>
5476
</MudGrid>
5577

5678
@if (_selectedPaths.Length > 0)
@@ -68,6 +90,13 @@
6890
</MudPaper>
6991
}
7092

93+
@if (!string.IsNullOrEmpty(_savedFileName))
94+
{
95+
<MudAlert Severity="Severity.Success" Class="mt-4">
96+
File saved as: @_savedFileName
97+
</MudAlert>
98+
}
99+
71100
@if (!string.IsNullOrEmpty(_errorMessage))
72101
{
73102
<MudAlert Severity="Severity.Error" Class="mt-4" ShowCloseIcon="true" CloseIconClicked="@(() => _errorMessage = null)">
@@ -87,11 +116,17 @@
87116
[Inject]
88117
protected IFileOpenService FileOpenService { get; set; } = default!;
89118

119+
[Inject]
120+
protected IFileSaveService FileSaveService { get; set; } = default!;
121+
90122
[Inject]
91123
protected ISnackbar Snackbar { get; set; } = default!;
92124

93125
private string[] _selectedPaths = [];
94126
private string? _errorMessage;
127+
private string? _savedFileName;
128+
private string _saveFileName = "example.txt";
129+
private string _saveContent = "Hello, World!";
95130

96131
private async Task OpenSingleFile()
97132
{
@@ -155,10 +190,31 @@
155190
});
156191
}
157192

193+
private async Task SaveFile()
194+
{
195+
_savedFileName = null;
196+
197+
await ExecuteWithErrorHandling(async () =>
198+
{
199+
var result = await FileSaveService.SaveFileAsync(_saveFileName, _saveContent);
200+
201+
if (result)
202+
{
203+
_savedFileName = _saveFileName;
204+
Snackbar.Add($"File saved successfully", Severity.Success);
205+
}
206+
else
207+
{
208+
Snackbar.Add("Save cancelled", Severity.Info);
209+
}
210+
});
211+
}
212+
158213
private async Task ExecuteWithErrorHandling(Func<Task> action)
159214
{
160215
_errorMessage = null;
161216
_selectedPaths = [];
217+
_savedFileName = null;
162218

163219
try
164220
{
@@ -181,7 +237,7 @@
181237
private string _explanationMarkdown = @"
182238
## File Services
183239
184-
The `IFileOpenService` provides a platform-agnostic way to open files and folders. It has different implementations for WebAssembly and Desktop (Photino).
240+
The `IFileOpenService` and `IFileSaveService` provide platform-agnostic ways to open and save files. They have different implementations for WebAssembly and Desktop (Photino).
185241
186242
### Opening Files
187243
@@ -209,6 +265,16 @@ var files = await FileOpenService.OpenFileAsync(filters: filters);
209265
var folders = await FileOpenService.OpenFolderAsync(""Select a folder"");
210266
```
211267
268+
### Saving Files
269+
270+
```csharp
271+
// Prompt and save
272+
var success = await FileSaveService.SaveFileAsync(""myfile.txt"", ""content"");
273+
274+
// Just get the save location
275+
var path = await FileSaveService.PromptFileSaveAsync(""myfile"", ""txt"");
276+
```
277+
212278
### Error Handling
213279
214280
In WebAssembly, the File System Access API may not be supported in all browsers. Wrap calls in a try-catch to handle `UnsupportedBrowserApiException`:
@@ -230,6 +296,7 @@ catch (UnsupportedBrowserApiException ex)
230296
**WebAssembly:**
231297
```csharp
232298
builder.Services.AddFileOpenService();
299+
builder.Services.AddFileSaveService();
233300
// or register all WebAssembly services
234301
builder.Services.AddWebAssemblyServices();
235302
```
@@ -240,4 +307,3 @@ builder.Services.AddPhotinoServices();
240307
```
241308
";
242309
}
243-

Mythetech.Components.WebAssembly/FileSystemAccessFileOpenService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,3 @@ private static string GetMimeTypeFromExtensions(string[] extensions)
131131
};
132132
}
133133
}
134-
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using KristofferStrube.Blazor.FileSystemAccess;
2+
using Microsoft.JSInterop;
3+
using Mythetech.Components.Infrastructure;
4+
using Mythetech.Components.WebAssembly.Exceptions;
5+
6+
namespace Mythetech.Components.WebAssembly;
7+
8+
/// <summary>
9+
/// WebAssembly implementation of file save service using the File System Access API
10+
/// </summary>
11+
public class FileSystemAccessFileSaveService : IFileSaveService
12+
{
13+
private readonly IFileSystemAccessService _fileSystemAccess;
14+
15+
/// <summary>
16+
/// Creates a new instance of the file system access file save service
17+
/// </summary>
18+
/// <param name="fileSystemAccess">The file system access service</param>
19+
public FileSystemAccessFileSaveService(IFileSystemAccessService fileSystemAccess)
20+
{
21+
_fileSystemAccess = fileSystemAccess;
22+
}
23+
24+
/// <inheritdoc />
25+
public async Task<bool> SaveFileAsync(string fileName, string data)
26+
{
27+
var location = await PromptFileSaveAsync(fileName);
28+
29+
if (string.IsNullOrWhiteSpace(location))
30+
return false;
31+
32+
return true;
33+
}
34+
35+
/// <inheritdoc />
36+
public async Task<string?> PromptFileSaveAsync(string fileName, string extension = "txt")
37+
{
38+
try
39+
{
40+
var options = new SaveFilePickerOptionsStartInWellKnownDirectory
41+
{
42+
StartIn = WellKnownDirectory.Downloads,
43+
SuggestedName = fileName,
44+
Types =
45+
[
46+
new FilePickerAcceptType
47+
{
48+
Description = $"{extension.ToUpperInvariant()} files",
49+
Accept = new Dictionary<string, string[]>
50+
{
51+
{ GetMimeTypeFromExtension(extension), [$".{extension}"] }
52+
}
53+
}
54+
]
55+
};
56+
57+
var fileHandle = await _fileSystemAccess.ShowSaveFilePickerAsync(options);
58+
59+
if (fileHandle is null)
60+
return null;
61+
62+
return await fileHandle.GetNameAsync();
63+
}
64+
catch (JSException ex) when (IsUserCancellation(ex))
65+
{
66+
return null;
67+
}
68+
catch (JSException ex)
69+
{
70+
throw new UnsupportedBrowserApiException("File System Access", ex);
71+
}
72+
}
73+
74+
private static bool IsUserCancellation(JSException ex)
75+
{
76+
return ex.Message.Contains("AbortError") || ex.Message.Contains("The user aborted a request");
77+
}
78+
79+
private static string GetMimeTypeFromExtension(string extension)
80+
{
81+
var ext = extension.ToLowerInvariant();
82+
return ext switch
83+
{
84+
"txt" => "text/plain",
85+
"json" => "application/json",
86+
"xml" => "application/xml",
87+
"html" or "htm" => "text/html",
88+
"css" => "text/css",
89+
"js" => "application/javascript",
90+
"png" => "image/png",
91+
"jpg" or "jpeg" => "image/jpeg",
92+
"gif" => "image/gif",
93+
"svg" => "image/svg+xml",
94+
"pdf" => "application/pdf",
95+
"zip" => "application/zip",
96+
"csv" => "text/csv",
97+
_ => "application/octet-stream"
98+
};
99+
}
100+
}
101+

Mythetech.Components.WebAssembly/JavaScriptLinkOpenService.cs

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,42 +26,4 @@ public async Task OpenLinkAsync(string url)
2626
{
2727
await _jsRuntime.InvokeVoidAsync("open", url, "_blank");
2828
}
29-
}
30-
31-
/// <summary>
32-
/// WebAssembly service registration extensions
33-
/// </summary>
34-
public static class WebAssemblyServiceExtensions
35-
{
36-
/// <summary>
37-
/// Registers the link opening service for WebAssembly
38-
/// </summary>
39-
public static IServiceCollection AddLinkOpeningService(this IServiceCollection services)
40-
{
41-
services.AddTransient<ILinkOpenService, JavaScriptLinkOpenService>();
42-
43-
return services;
44-
}
45-
46-
/// <summary>
47-
/// Registers the file open service for WebAssembly using the File System Access API
48-
/// </summary>
49-
public static IServiceCollection AddFileOpenService(this IServiceCollection services)
50-
{
51-
services.AddFileSystemAccessServiceInProcess();
52-
services.AddTransient<IFileOpenService, FileSystemAccessFileOpenService>();
53-
54-
return services;
55-
}
56-
57-
/// <summary>
58-
/// Registers all WebAssembly-specific services
59-
/// </summary>
60-
public static IServiceCollection AddWebAssemblyServices(this IServiceCollection services)
61-
{
62-
services.AddLinkOpeningService();
63-
services.AddFileOpenService();
64-
65-
return services;
66-
}
6729
}

0 commit comments

Comments
 (0)