20 changed files with 691 additions and 91 deletions
@ -0,0 +1,55 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Web.Interop; |
|||
|
|||
internal static partial class StorageHelper |
|||
{ |
|||
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")] |
|||
public static partial bool CanShowOpenFilePicker(); |
|||
|
|||
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")] |
|||
public static partial bool CanShowSaveFilePicker(); |
|||
|
|||
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")] |
|||
public static partial bool CanShowDirectoryPicker(); |
|||
|
|||
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn); |
|||
|
|||
[JSImport("StorageProvider.openFileDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple, |
|||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
|||
|
|||
[JSImport("StorageProvider.saveFileDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName, |
|||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
|||
|
|||
[JSImport("StorageProvider.openBookmark", "storage.ts")] |
|||
public static partial Task<JSObject?> OpenBookmark(string key); |
|||
|
|||
[JSImport("StorageItem.saveBookmark", "storage.ts")] |
|||
public static partial Task<string?> SaveBookmark(JSObject item); |
|||
|
|||
[JSImport("StorageItem.deleteBookmark", "storage.ts")] |
|||
public static partial Task DeleteBookmark(JSObject item); |
|||
|
|||
[JSImport("StorageItem.getProperties", "storage.ts")] |
|||
public static partial Task<JSObject?> GetProperties(JSObject item); |
|||
|
|||
[JSImport("StorageItem.openWrite", "storage.ts")] |
|||
public static partial Task<JSObject> OpenWrite(JSObject item); |
|||
|
|||
[JSImport("StorageItem.openRead", "storage.ts")] |
|||
public static partial Task<JSObject> OpenRead(JSObject item); |
|||
|
|||
[JSImport("StorageItem.getItems", "storage.ts")] |
|||
[return: JSMarshalAs<JSType.Promise<JSType.Object>>] |
|||
public static partial Task<JSObject> GetItems(JSObject item); |
|||
|
|||
[JSImport("StorageItems.itemsArray", "storage.ts")] |
|||
public static partial JSObject[] ItemsArray(JSObject item); |
|||
|
|||
[JSImport("StorageProvider.createAcceptType", "storage.ts")] |
|||
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); |
|||
} |
|||
@ -0,0 +1,257 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Runtime.Versioning; |
|||
using System.Threading.Tasks; |
|||
|
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Web.Interop; |
|||
|
|||
namespace Avalonia.Web.Storage; |
|||
|
|||
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); |
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
internal class BrowserStorageProvider : IStorageProvider |
|||
{ |
|||
internal const string PickerCancelMessage = "The user aborted a request"; |
|||
internal const string NoPermissionsMessage = "Permissions denied"; |
|||
|
|||
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js")); |
|||
|
|||
public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); |
|||
public bool CanSave => StorageHelper.CanShowSaveFilePicker(); |
|||
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); |
|||
|
|||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); |
|||
|
|||
try |
|||
{ |
|||
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); |
|||
if (items is null) |
|||
{ |
|||
return Array.Empty<IStorageFile>(); |
|||
} |
|||
|
|||
var itemsArray = StorageHelper.ItemsArray(items); |
|||
return itemsArray.Select(item => new JSStorageFile(item)).ToArray(); |
|||
} |
|||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
|||
{ |
|||
return Array.Empty<IStorageFile>(); |
|||
} |
|||
finally |
|||
{ |
|||
if (types is not null) |
|||
{ |
|||
foreach (var type in types) |
|||
{ |
|||
type.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); |
|||
|
|||
try |
|||
{ |
|||
var item = await StorageHelper.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; |
|||
} |
|||
finally |
|||
{ |
|||
if (types is not null) |
|||
{ |
|||
foreach (var type in types) |
|||
{ |
|||
type.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
try |
|||
{ |
|||
var item = await StorageHelper.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) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var item = await StorageHelper.OpenBookmark(bookmark); |
|||
return item is not null ? new JSStorageFile(item) : null; |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var item = await StorageHelper.OpenBookmark(bookmark); |
|||
return item is not null ? new JSStorageFolder(item) : null; |
|||
} |
|||
|
|||
private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) |
|||
{ |
|||
var types = input? |
|||
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) |
|||
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) |
|||
.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 JSObject? _fileHandle; |
|||
|
|||
protected JSStorageItem(JSObject fileHandle) |
|||
{ |
|||
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); |
|||
} |
|||
|
|||
internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); |
|||
|
|||
public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty; |
|||
|
|||
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
|||
{ |
|||
uri = new Uri(Name, UriKind.Relative); |
|||
return false; |
|||
} |
|||
|
|||
public async Task<StorageItemProperties> GetBasicPropertiesAsync() |
|||
{ |
|||
using var properties = await StorageHelper.GetProperties(FileHandle); |
|||
var size = (long?)properties?.GetPropertyAsDouble("Size"); |
|||
var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified"); |
|||
|
|||
return new StorageItemProperties( |
|||
(ulong?)size, |
|||
dateCreated: null, |
|||
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); |
|||
} |
|||
|
|||
public bool CanBookmark => true; |
|||
|
|||
public Task<string?> SaveBookmarkAsync() |
|||
{ |
|||
return StorageHelper.SaveBookmark(FileHandle); |
|||
} |
|||
|
|||
public Task<IStorageFolder?> GetParentAsync() |
|||
{ |
|||
return Task.FromResult<IStorageFolder?>(null); |
|||
} |
|||
|
|||
public Task ReleaseBookmarkAsync() |
|||
{ |
|||
return StorageHelper.DeleteBookmark(FileHandle); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_fileHandle?.Dispose(); |
|||
_fileHandle = null; |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile |
|||
{ |
|||
public JSStorageFile(JSObject fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public bool CanOpenRead => true; |
|||
public async Task<Stream> OpenReadAsync() |
|||
{ |
|||
try |
|||
{ |
|||
var blob = await StorageHelper.OpenRead(FileHandle); |
|||
return new BlobReadableStream(blob); |
|||
} |
|||
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
|||
{ |
|||
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
|||
} |
|||
} |
|||
|
|||
public bool CanOpenWrite => true; |
|||
public async Task<Stream> OpenWriteAsync() |
|||
{ |
|||
try |
|||
{ |
|||
using var properties = await StorageHelper.GetProperties(FileHandle); |
|||
var streamWriter = await StorageHelper.OpenWrite(FileHandle); |
|||
var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0; |
|||
|
|||
return new WriteableStream(streamWriter, size); |
|||
} |
|||
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
|||
{ |
|||
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder |
|||
{ |
|||
public JSStorageFolder(JSObject fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() |
|||
{ |
|||
using var items = await StorageHelper.GetItems(FileHandle); |
|||
if (items is null) |
|||
{ |
|||
return Array.Empty<IStorageItem>(); |
|||
} |
|||
|
|||
var itemsArray = StorageHelper.ItemsArray(items); |
|||
|
|||
return itemsArray |
|||
.Select(reference => reference.GetPropertyAsString("kind") switch |
|||
{ |
|||
"directory" => (IStorageItem)new JSStorageFolder(reference), |
|||
"file" => new JSStorageFile(reference), |
|||
_ => null |
|||
}) |
|||
.Where(i => i is not null) |
|||
.ToArray()!; |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
export class Caniuse { |
|||
public static canShowOpenFilePicker(): boolean { |
|||
return typeof window.showOpenFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowSaveFilePicker(): boolean { |
|||
return typeof window.showSaveFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowDirectoryPicker(): boolean { |
|||
return typeof window.showDirectoryPicker !== "undefined"; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
import { IMemoryView } from "../../types/dotnet"; |
|||
|
|||
export class StreamHelper { |
|||
public static async seek(stream: FileSystemWritableFileStream, position: number) { |
|||
return await stream.seek(position); |
|||
} |
|||
|
|||
public static async truncate(stream: FileSystemWritableFileStream, size: number) { |
|||
return await stream.truncate(size); |
|||
} |
|||
|
|||
public static async close(stream: FileSystemWritableFileStream) { |
|||
return await stream.close(); |
|||
} |
|||
|
|||
public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) { |
|||
const array = new Uint8Array(span.byteLength); |
|||
span.copyTo(array); |
|||
|
|||
const data: WriteParams = { |
|||
type: "write", |
|||
data: array |
|||
}; |
|||
|
|||
return await stream.write(data); |
|||
} |
|||
|
|||
public static byteLength(stream: Blob) { |
|||
return stream.size; |
|||
} |
|||
|
|||
public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) { |
|||
const buffer = await stream.slice(offset, offset + count).arrayBuffer(); |
|||
return new Uint8Array(buffer); |
|||
} |
|||
|
|||
public static toMemoryView(buffer: Uint8Array): Uint8Array { |
|||
return buffer; |
|||
} |
|||
} |
|||
@ -1,5 +1,2 @@ |
|||
export class StorageProvider { |
|||
static isFileApiSupported(): boolean { |
|||
return (globalThis as any).showOpenFilePicker !== undefined; |
|||
} |
|||
} |
|||
export { StorageItem, StorageItems } from "./storage/storageItem"; |
|||
export { StorageProvider } from "./storage/storageProvider"; |
|||
|
|||
@ -0,0 +1,84 @@ |
|||
class InnerDbConnection { |
|||
constructor(private readonly database: IDBDatabase) { } |
|||
|
|||
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
|||
const tx = this.database.transaction(store, mode); |
|||
return tx.objectStore(store); |
|||
} |
|||
|
|||
public async put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
const 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) => { |
|||
const response = os.get(key); |
|||
response.onsuccess = () => { |
|||
resolve(response.result); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public async delete(store: string, key: IDBValidKey): Promise<void> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
const response = os.delete(key); |
|||
response.onsuccess = () => { |
|||
resolve(); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public close() { |
|||
this.database.close(); |
|||
} |
|||
} |
|||
|
|||
class IndexedDbWrapper { |
|||
constructor(private readonly databaseName: string, private readonly objectStores: [string]) { |
|||
} |
|||
|
|||
public async connect(): Promise<InnerDbConnection> { |
|||
const conn = window.indexedDB.open(this.databaseName, 1); |
|||
|
|||
conn.onupgradeneeded = event => { |
|||
const db = (event.target as IDBRequest<IDBDatabase>).result; |
|||
this.objectStores.forEach(store => { |
|||
db.createObjectStore(store); |
|||
}); |
|||
}; |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
conn.onsuccess = event => { |
|||
resolve(new InnerDbConnection((event.target as IDBRequest<IDBDatabase>).result)); |
|||
}; |
|||
conn.onerror = event => { |
|||
reject((event.target as IDBRequest<IDBDatabase>).error); |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export const fileBookmarksStore: string = "fileBookmarks"; |
|||
export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ |
|||
fileBookmarksStore |
|||
]); |
|||
@ -0,0 +1,111 @@ |
|||
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
|||
|
|||
export class StorageItem { |
|||
constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { } |
|||
|
|||
public get name(): string { |
|||
return this.handle.name; |
|||
} |
|||
|
|||
public get kind(): string { |
|||
return this.handle.kind; |
|||
} |
|||
|
|||
public static async openRead(item: StorageItem): Promise<Blob> { |
|||
if (!(item.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await item.verityPermissions("read"); |
|||
|
|||
const file = await item.handle.getFile(); |
|||
return file; |
|||
} |
|||
|
|||
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> { |
|||
if (!(item.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await item.verityPermissions("readwrite"); |
|||
|
|||
return await item.handle.createWritable({ keepExistingData: true }); |
|||
} |
|||
|
|||
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { |
|||
const file = item.handle instanceof FileSystemFileHandle && |
|||
await item.handle.getFile(); |
|||
|
|||
if (!file) { |
|||
return null; |
|||
} |
|||
|
|||
return { |
|||
Size: file.size, |
|||
LastModified: file.lastModified, |
|||
Type: file.type |
|||
}; |
|||
} |
|||
|
|||
public static async getItems(item: StorageItem): Promise<StorageItems> { |
|||
if (item.handle.kind !== "directory") { |
|||
return new StorageItems([]); |
|||
} |
|||
|
|||
const items: StorageItem[] = []; |
|||
for await (const [, value] of (item.handle as any).entries()) { |
|||
items.push(new StorageItem(value)); |
|||
} |
|||
return new StorageItems(items); |
|||
} |
|||
|
|||
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> { |
|||
if (await this.handle.queryPermission({ mode }) === "granted") { |
|||
return; |
|||
} |
|||
|
|||
if (await this.handle.requestPermission({ mode }) === "denied") { |
|||
throw new Error("Permissions denied"); |
|||
} |
|||
} |
|||
|
|||
public static async saveBookmark(item: StorageItem): Promise<string> { |
|||
// If file was previously bookmarked, just return old one.
|
|||
if (item.bookmarkId) { |
|||
return item.bookmarkId; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId()); |
|||
return key as string; |
|||
} finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
public static async deleteBookmark(item: StorageItem): Promise<void> { |
|||
if (!item.bookmarkId) { |
|||
return; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
await connection.delete(fileBookmarksStore, item.bookmarkId); |
|||
} finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
private generateBookmarkId(): string { |
|||
return Date.now().toString(36) + Math.random().toString(36).substring(2); |
|||
} |
|||
} |
|||
|
|||
export class StorageItems { |
|||
constructor(private readonly items: StorageItem[]) { } |
|||
|
|||
public static itemsArray(instance: StorageItems): StorageItem[] { |
|||
return instance.items; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
|||
import { StorageItem, StorageItems } from "./storageItem"; |
|||
|
|||
declare global { |
|||
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; |
|||
type StartInDirectory = WellKnownDirectory | FileSystemHandle; |
|||
interface OpenFilePickerOptions { |
|||
startIn?: StartInDirectory; |
|||
} |
|||
interface SaveFilePickerOptions { |
|||
startIn?: StartInDirectory; |
|||
} |
|||
} |
|||
|
|||
export class StorageProvider { |
|||
public static async selectFolderDialog( |
|||
startIn: StorageItem | null): Promise<StorageItem> { |
|||
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
|
|||
const options: DirectoryPickerOptions = { |
|||
startIn: (startIn?.handle ?? undefined) |
|||
}; |
|||
|
|||
const handle = await window.showDirectoryPicker(options); |
|||
return new StorageItem(handle); |
|||
} |
|||
|
|||
public static async openFileDialog( |
|||
startIn: StorageItem | null, multiple: boolean, |
|||
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> { |
|||
const options: OpenFilePickerOptions = { |
|||
startIn: (startIn?.handle ?? undefined), |
|||
multiple, |
|||
excludeAcceptAllOption, |
|||
types: (types ?? undefined) |
|||
}; |
|||
|
|||
const handles = await window.showOpenFilePicker(options); |
|||
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); |
|||
} |
|||
|
|||
public static async saveFileDialog( |
|||
startIn: StorageItem | null, suggestedName: string | null, |
|||
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> { |
|||
const options: SaveFilePickerOptions = { |
|||
startIn: (startIn?.handle ?? 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(); |
|||
} |
|||
} |
|||
|
|||
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { |
|||
const accept: Record<string, string[]> = {}; |
|||
mimeTypes.forEach(a => { accept[a] = []; }); |
|||
return { description, accept }; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue