118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
import { KeyValueStore } from '@lib/indexeddb';
|
|
|
|
export class FilePicker {
|
|
#id;
|
|
#fileType;
|
|
#fileExtension;
|
|
#mimeType;
|
|
#store;
|
|
#fileHandlerPromise;
|
|
|
|
// Feature detection. The API needs to be supported and the app not run in an iframe.
|
|
#supportsFileSystemAccess =
|
|
'showSaveFilePicker' in window &&
|
|
(() => {
|
|
try {
|
|
return window.self === window.top;
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
constructor(id: string, fileType: string, mimeType: string) {
|
|
this.#id = id;
|
|
this.#fileType = fileType;
|
|
this.#fileExtension = `.${this.#fileType}`;
|
|
this.#mimeType = mimeType;
|
|
this.#store = new KeyValueStore<FileSystemFileHandle>(this.#id);
|
|
this.#fileHandlerPromise = this.#loadFileHandler();
|
|
}
|
|
|
|
async #loadFileHandler() {
|
|
const file = await this.#store.get('file');
|
|
|
|
if (file === undefined) return undefined;
|
|
|
|
// We need to request permission since the file handler was persisted.
|
|
// Calling `queryPermission` seems unnecessary atm since the browser prompts permission for each session
|
|
const previousPermission = await file.queryPermission({ mode: 'readwrite' });
|
|
if (previousPermission === 'granted') return file;
|
|
|
|
// this requires user interaction
|
|
// const newPermission = await file.requestPermission({ mode: 'readwrite' });
|
|
// if (newPermission === 'granted') return file;
|
|
|
|
return undefined;
|
|
}
|
|
|
|
async open(showPicker = true): Promise<string> {
|
|
let fileHandler = await this.#fileHandlerPromise;
|
|
|
|
if (showPicker) {
|
|
fileHandler = await this.#showOpenFilePicker();
|
|
}
|
|
|
|
if (fileHandler === undefined) return '';
|
|
|
|
const file = await fileHandler.getFile();
|
|
const text = await file.text();
|
|
return text;
|
|
}
|
|
|
|
async save(content: string, promptNewFile = false) {
|
|
// TODO: progressively enhance using anchor downloads?
|
|
if (!this.#supportsFileSystemAccess) {
|
|
throw new Error('File System Access API is not supported.');
|
|
}
|
|
|
|
let fileHandler = await this.#fileHandlerPromise;
|
|
|
|
if (promptNewFile || fileHandler === undefined) {
|
|
fileHandler = await this.#showSaveFilePicker();
|
|
}
|
|
|
|
const writer = await fileHandler.createWritable();
|
|
await writer.write(content);
|
|
await writer.close();
|
|
}
|
|
|
|
clear() {
|
|
this.#store.clear();
|
|
}
|
|
|
|
async #showSaveFilePicker() {
|
|
this.#fileHandlerPromise = window.showSaveFilePicker({
|
|
id: this.#id,
|
|
suggestedName: `${this.#id}.${this.#fileType}`,
|
|
types: [
|
|
{
|
|
description: `${this.#fileType.toUpperCase()} document`,
|
|
accept: { [this.#mimeType]: [this.#fileExtension] } as FilePickerAcceptType['accept'],
|
|
},
|
|
],
|
|
});
|
|
|
|
const fileHandler = (await this.#fileHandlerPromise)!;
|
|
await this.#store.set('file', fileHandler);
|
|
return fileHandler;
|
|
}
|
|
|
|
async #showOpenFilePicker() {
|
|
this.#fileHandlerPromise = window
|
|
.showOpenFilePicker({
|
|
id: this.#id,
|
|
types: [
|
|
{
|
|
description: `${this.#fileType.toUpperCase()} document`,
|
|
accept: { [this.#mimeType]: [this.#fileExtension] } as FilePickerAcceptType['accept'],
|
|
},
|
|
],
|
|
})
|
|
.then((files) => files[0]);
|
|
|
|
const fileHandler = (await this.#fileHandlerPromise)!;
|
|
await this.#store.set('file', fileHandler);
|
|
return fileHandler;
|
|
}
|
|
}
|