8 changed files with 637 additions and 12 deletions
@ -0,0 +1,200 @@ |
|||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
using Avalonia.Platform.Storage; |
||||
|
|
||||
|
using Microsoft.JSInterop; |
||||
|
|
||||
|
namespace Avalonia.Web.Blazor.Interop.Storage |
||||
|
{ |
||||
|
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); |
||||
|
|
||||
|
internal record FileProperties(ulong Size, long LastModified, string? Type); |
||||
|
|
||||
|
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider |
||||
|
{ |
||||
|
private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js"; |
||||
|
private const string PickerCancelMessage = "The user aborted a request"; |
||||
|
|
||||
|
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js) |
||||
|
{ |
||||
|
var interop = new StorageProviderInterop(js); |
||||
|
await interop.ImportAsync(); |
||||
|
return interop; |
||||
|
} |
||||
|
|
||||
|
public StorageProviderInterop(IJSRuntime js) |
||||
|
: base(js, JsFilename) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public bool CanOpen => Invoke<bool>("StorageProvider.canOpen"); |
||||
|
public bool CanSave => Invoke<bool>("StorageProvider.canSave"); |
||||
|
public bool CanPickFolder => Invoke<bool>("StorageProvider.canPickFolder"); |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); |
||||
|
var items = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll); |
||||
|
var count = items.Invoke<int>("count"); |
||||
|
|
||||
|
return Enumerable.Range(0, count) |
||||
|
.Select(index => new JSStorageFile(items.Invoke<IJSInProcessObjectReference>("at", index))) |
||||
|
.ToArray(); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return Array.Empty<IStorageFile>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); |
||||
|
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.saveFileDialog", startIn, options.SuggestedFileName, types, exludeAll); |
||||
|
|
||||
|
return item is not null ? new JSStorageFile(item) : null; |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.selectFolderDialog", startIn); |
||||
|
|
||||
|
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>(); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return Array.Empty<IStorageFolder>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); |
||||
|
return item is not null ? new JSStorageFile(item) : null; |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); |
||||
|
return item is not null ? new JSStorageFolder(item) : null; |
||||
|
} |
||||
|
|
||||
|
private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) |
||||
|
{ |
||||
|
var types = input? |
||||
|
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) |
||||
|
.Select(t => new FilePickerAcceptType(t.Name, t.MimeTypes! |
||||
|
.ToDictionary(m => m, _ => (IReadOnlyList<string>)Array.Empty<string>()))) |
||||
|
.ToArray(); |
||||
|
if (types?.Length == 0) |
||||
|
{ |
||||
|
types = null; |
||||
|
} |
||||
|
|
||||
|
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; |
||||
|
|
||||
|
return (types, !inlcudeAll); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal abstract class JSStorageItem : IStorageBookmarkItem |
||||
|
{ |
||||
|
internal IJSInProcessObjectReference? _fileHandle; |
||||
|
|
||||
|
protected JSStorageItem(IJSInProcessObjectReference fileHandle) |
||||
|
{ |
||||
|
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); |
||||
|
} |
||||
|
|
||||
|
internal IJSInProcessObjectReference FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); |
||||
|
|
||||
|
public string Name => FileHandle.Invoke<string>("getName"); |
||||
|
|
||||
|
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
||||
|
{ |
||||
|
uri = new Uri(Name, UriKind.Relative); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public async Task<StorageItemProperties> GetBasicPropertiesAsync() |
||||
|
{ |
||||
|
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); |
||||
|
|
||||
|
return new StorageItemProperties( |
||||
|
properties?.Size, |
||||
|
dateCreated: null, |
||||
|
dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null); |
||||
|
} |
||||
|
|
||||
|
public bool CanBookmark => true; |
||||
|
|
||||
|
public Task<string?> SaveBookmark() |
||||
|
{ |
||||
|
return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask(); |
||||
|
} |
||||
|
|
||||
|
public Task<IStorageFolder?> GetParentAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IStorageFolder?>(null); |
||||
|
} |
||||
|
|
||||
|
public Task ReleaseBookmark() |
||||
|
{ |
||||
|
return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_fileHandle?.Dispose(); |
||||
|
_fileHandle = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile |
||||
|
{ |
||||
|
public JSStorageFile(IJSInProcessObjectReference fileHandle) : base(fileHandle) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public bool CanOpenRead => true; |
||||
|
public async Task<Stream> OpenRead() |
||||
|
{ |
||||
|
var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead"); |
||||
|
// Remove maxAllowedSize limit, as developer can decide if they read only small part or everything.
|
||||
|
return await stream.OpenReadStreamAsync(long.MaxValue, CancellationToken.None); |
||||
|
} |
||||
|
|
||||
|
public bool CanOpenWrite => true; |
||||
|
public async Task<Stream> OpenWrite() |
||||
|
{ |
||||
|
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); |
||||
|
var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite"); |
||||
|
|
||||
|
return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder |
||||
|
{ |
||||
|
public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
using System.Buffers; |
||||
|
using System.Text.Json.Serialization; |
||||
|
|
||||
|
using Microsoft.JSInterop; |
||||
|
|
||||
|
namespace Avalonia.Web.Blazor.Interop.Storage |
||||
|
{ |
||||
|
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
|
||||
|
internal sealed class JSWriteableStream : Stream |
||||
|
{ |
||||
|
private IJSInProcessObjectReference? _jSReference; |
||||
|
|
||||
|
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
|
||||
|
private long _length, _position; |
||||
|
|
||||
|
internal JSWriteableStream(IJSInProcessObjectReference jSReference, long initialLength) |
||||
|
{ |
||||
|
_jSReference = jSReference; |
||||
|
_length = initialLength; |
||||
|
} |
||||
|
|
||||
|
private IJSInProcessObjectReference JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); |
||||
|
|
||||
|
public override bool CanRead => false; |
||||
|
|
||||
|
public override bool CanSeek => true; |
||||
|
|
||||
|
public override bool CanWrite => true; |
||||
|
|
||||
|
public override long Length => _length; |
||||
|
|
||||
|
public override long Position |
||||
|
{ |
||||
|
get => _position; |
||||
|
set => Seek(_position, SeekOrigin.Begin); |
||||
|
} |
||||
|
|
||||
|
public override void Flush() |
||||
|
{ |
||||
|
// no-op
|
||||
|
} |
||||
|
|
||||
|
public override int Read(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public override long Seek(long offset, SeekOrigin origin) |
||||
|
{ |
||||
|
var position = origin switch |
||||
|
{ |
||||
|
SeekOrigin.Current => _position + offset, |
||||
|
SeekOrigin.End => _length + offset, |
||||
|
_ => offset |
||||
|
}; |
||||
|
JSReference.InvokeVoid("seek", position); |
||||
|
return position; |
||||
|
} |
||||
|
|
||||
|
public override void SetLength(long value) |
||||
|
{ |
||||
|
_length = value; |
||||
|
|
||||
|
// See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate
|
||||
|
// If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size
|
||||
|
if (_position > _length) |
||||
|
{ |
||||
|
_position = _length; |
||||
|
} |
||||
|
|
||||
|
JSReference.InvokeVoid("truncate", value); |
||||
|
} |
||||
|
|
||||
|
public override void Write(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
throw new NotSupportedException("Synchronous writes are not supported."); |
||||
|
} |
||||
|
|
||||
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (offset != 0 || count != buffer.Length) |
||||
|
{ |
||||
|
// TODO, we need to pass prepared buffer to the JS
|
||||
|
// Can't use ArrayPool as it can return bigger array than requested
|
||||
|
// Can't use Span/Memory, as it's not supported by JS interop yet.
|
||||
|
// Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?)
|
||||
|
buffer = buffer.AsMemory(offset, count).ToArray(); |
||||
|
} |
||||
|
return WriteAsyncInternal(buffer, cancellationToken).AsTask(); |
||||
|
} |
||||
|
|
||||
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return WriteAsyncInternal(buffer.ToArray(), cancellationToken); |
||||
|
} |
||||
|
|
||||
|
private ValueTask WriteAsyncInternal(byte[] buffer, CancellationToken _) |
||||
|
{ |
||||
|
_position += buffer.Length; |
||||
|
|
||||
|
return JSReference.InvokeVoidAsync("write", buffer); |
||||
|
} |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (_jSReference is { } jsReference) |
||||
|
{ |
||||
|
_jSReference = null; |
||||
|
jsReference.InvokeVoid("close"); |
||||
|
jsReference.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override async ValueTask DisposeAsync() |
||||
|
{ |
||||
|
if (_jSReference is { } jsReference) |
||||
|
{ |
||||
|
_jSReference = null; |
||||
|
await jsReference.InvokeVoidAsync("close"); |
||||
|
await jsReference.DisposeAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,292 @@ |
|||||
|
// As we don't have proper package managing for Avalonia.Web project, declare types manually
|
||||
|
declare global { |
||||
|
interface FileSystemWritableFileStream { |
||||
|
write(position: number, data: BufferSource | Blob | string): Promise<void>; |
||||
|
truncate(size: number): Promise<void>; |
||||
|
close(): Promise<void>; |
||||
|
} |
||||
|
type PermissionsMode = "read" | "readwrite"; |
||||
|
interface FileSystemFileHandle { |
||||
|
name: string, |
||||
|
kind: "file" | "directory", |
||||
|
getFile(): Promise<File>; |
||||
|
createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>; |
||||
|
|
||||
|
queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; |
||||
|
requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; |
||||
|
} |
||||
|
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; |
||||
|
type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; |
||||
|
interface FilePickerAcceptType { |
||||
|
description: string, |
||||
|
// mime -> ext[] array
|
||||
|
accept: { [mime: string]: string | string[] } |
||||
|
} |
||||
|
interface FilePickerOptions { |
||||
|
types?: FilePickerAcceptType[], |
||||
|
excludeAcceptAllOption: boolean, |
||||
|
id?: string, |
||||
|
startIn?: StartInDirectory |
||||
|
} |
||||
|
interface OpenFilePickerOptions extends FilePickerOptions { |
||||
|
multiple: boolean |
||||
|
} |
||||
|
interface SaveFilePickerOptions extends FilePickerOptions { |
||||
|
suggestedName?: string |
||||
|
} |
||||
|
interface DirectoryPickerOptions { |
||||
|
id?: string, |
||||
|
startIn?: StartInDirectory |
||||
|
} |
||||
|
|
||||
|
interface Window { |
||||
|
showOpenFilePicker: (options: OpenFilePickerOptions) => Promise<FileSystemFileHandle[]>; |
||||
|
showSaveFilePicker: (options: SaveFilePickerOptions) => Promise<FileSystemFileHandle>; |
||||
|
showDirectoryPicker: (options: DirectoryPickerOptions) => Promise<FileSystemFileHandle>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TODO move to another file and use import
|
||||
|
class IndexedDbWrapper { |
||||
|
constructor(private databaseName: string, private objectStores: [ string ]) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public connect(): Promise<InnerDbConnection> { |
||||
|
var conn = window.indexedDB.open(this.databaseName, 1); |
||||
|
|
||||
|
conn.onupgradeneeded = event => { |
||||
|
const db = (<IDBRequest<IDBDatabase>>event.target).result; |
||||
|
this.objectStores.forEach(store => { |
||||
|
db.createObjectStore(store); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
conn.onsuccess = event => { |
||||
|
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result)); |
||||
|
} |
||||
|
conn.onerror = event => { |
||||
|
reject((<IDBRequest<IDBDatabase>>event.target).error); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class InnerDbConnection { |
||||
|
constructor(private database: IDBDatabase) { } |
||||
|
|
||||
|
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
||||
|
const tx = this.database.transaction(store, mode); |
||||
|
return tx.objectStore(store); |
||||
|
} |
||||
|
|
||||
|
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
||||
|
const os = this.openStore(store, "readwrite"); |
||||
|
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
var response = os.put(obj, key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(response.result); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public get(store: string, key: IDBValidKey): any { |
||||
|
const os = this.openStore(store, "readonly"); |
||||
|
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
var response = os.get(key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(response.result); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public delete(store: string, key: IDBValidKey): Promise<void> { |
||||
|
const os = this.openStore(store, "readwrite"); |
||||
|
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
var response = os.delete(key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public close() { |
||||
|
this.database.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const fileBookmarksStore: string = "fileBookmarks"; |
||||
|
const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ |
||||
|
fileBookmarksStore |
||||
|
]) |
||||
|
|
||||
|
class StorageItem { |
||||
|
constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { } |
||||
|
|
||||
|
public getName(): string { |
||||
|
return this.handle.name |
||||
|
} |
||||
|
|
||||
|
public async openRead(): Promise<Blob> { |
||||
|
await this.verityPermissions('read'); |
||||
|
|
||||
|
var file = await this.handle.getFile(); |
||||
|
return file; |
||||
|
} |
||||
|
|
||||
|
public async openWrite(): Promise<FileSystemWritableFileStream> { |
||||
|
await this.verityPermissions('readwrite'); |
||||
|
|
||||
|
return await this.handle.createWritable({ keepExistingData: true }); |
||||
|
} |
||||
|
|
||||
|
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> { |
||||
|
var file = this.handle.getFile && await this.handle.getFile(); |
||||
|
|
||||
|
return file && { |
||||
|
Size: file.size, |
||||
|
LastModified: file.lastModified, |
||||
|
Type: file.type |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async verityPermissions(mode: PermissionsMode): Promise<void | never> { |
||||
|
if (await this.handle.queryPermission({ mode }) === 'granted') { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (await this.handle.requestPermission({ mode }) === "denied") { |
||||
|
throw new Error("Read permissions denied"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async saveBookmark(): Promise<string> { |
||||
|
// If file was previously bookmarked, just return old one.
|
||||
|
if (this.bookmarkId) { |
||||
|
return this.bookmarkId; |
||||
|
} |
||||
|
|
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); |
||||
|
return <string>key; |
||||
|
} |
||||
|
finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async deleteBookmark(): Promise<void> { |
||||
|
if (!this.bookmarkId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
const key = await connection.delete(fileBookmarksStore, this.bookmarkId); |
||||
|
} |
||||
|
finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private generateBookmarkId(): string { |
||||
|
return Date.now().toString(36) + Math.random().toString(36).substring(2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class StorageItems { |
||||
|
constructor(private items: StorageItem[]) { } |
||||
|
|
||||
|
public count(): number { |
||||
|
return this.items.length; |
||||
|
} |
||||
|
|
||||
|
public at(index: number): StorageItem { |
||||
|
return this.items[index]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class StorageProvider { |
||||
|
|
||||
|
public static canOpen(): boolean { |
||||
|
return typeof window.showOpenFilePicker !== 'undefined'; |
||||
|
} |
||||
|
|
||||
|
public static canSave(): boolean { |
||||
|
return typeof window.showSaveFilePicker !== 'undefined'; |
||||
|
} |
||||
|
|
||||
|
public static canPickFolder(): boolean { |
||||
|
return typeof window.showDirectoryPicker !== 'undefined'; |
||||
|
} |
||||
|
|
||||
|
public static async selectFolderDialog( |
||||
|
startIn: StartInDirectory | null) |
||||
|
: Promise<StorageItem> { |
||||
|
|
||||
|
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
|
||||
|
const options: DirectoryPickerOptions = { |
||||
|
startIn: (startIn || undefined) |
||||
|
}; |
||||
|
|
||||
|
const handle = await window.showDirectoryPicker(options); |
||||
|
return new StorageItem(handle); |
||||
|
} |
||||
|
|
||||
|
public static async openFileDialog( |
||||
|
startIn: StartInDirectory | null, multiple: boolean, |
||||
|
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) |
||||
|
: Promise<StorageItems> { |
||||
|
|
||||
|
const options: OpenFilePickerOptions = { |
||||
|
startIn: (startIn || undefined), |
||||
|
multiple, |
||||
|
excludeAcceptAllOption, |
||||
|
types: (types || undefined) |
||||
|
}; |
||||
|
|
||||
|
const handles = await window.showOpenFilePicker(options); |
||||
|
return new StorageItems(handles.map(handle => new StorageItem(handle))); |
||||
|
} |
||||
|
|
||||
|
public static async saveFileDialog( |
||||
|
startIn: StartInDirectory | null, suggestedName: string | null, |
||||
|
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) |
||||
|
: Promise<StorageItem> { |
||||
|
|
||||
|
const options: SaveFilePickerOptions = { |
||||
|
startIn: (startIn || undefined), |
||||
|
suggestedName: (suggestedName || undefined), |
||||
|
excludeAcceptAllOption, |
||||
|
types: (types || undefined) |
||||
|
}; |
||||
|
|
||||
|
const handle = await window.showSaveFilePicker(options); |
||||
|
return new StorageItem(handle); |
||||
|
} |
||||
|
|
||||
|
public static async openBookmark(key: string): Promise<StorageItem | null> { |
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
const handle = await connection.get(fileBookmarksStore, key); |
||||
|
return handle && new StorageItem(handle, key); |
||||
|
} |
||||
|
finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue