@bitblit/asyncglk
Version:
A Typescript Glk library
211 lines (207 loc) • 6.46 kB
JavaScript
/*
Browser Dialog
==============
Copyright (c) 2024 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import { saveAs as filesave_saveAs } from 'file-saver';
import path from 'path-browserify-esm';
import { show_alert } from './common.js';
import { DownloadProvider, read_uploaded_file } from './download.js';
import { WebStorageProvider } from './storage.js';
//import FileDialog from './ui/FileDialog.svelte'
export class ProviderBasedBrowserDialog {
'async' = true;
controller;
dirs = {
storyfile: '',
system_cwd: '/',
temp: '/tmp',
working: '/usr',
};
downloader;
providers = [];
async init(options) {
this.downloader = new DownloadProvider(options);
try {
this.providers = [
this.downloader,
new WebStorageProvider('/tmp', sessionStorage, this.dirs),
new WebStorageProvider('/', localStorage, this.dirs, true),
];
}
catch {
this.providers = [
this.downloader,
];
}
for (const [i, provider] of this.providers.entries()) {
const next = this.providers[i + 1];
if (next) {
provider.next = next;
}
// Set up the Svelte UI
if (provider.browseable) {
this.controller = new DialogController(provider);
}
}
}
async download(url, progress_callback) {
const file_path = await this.downloader.download(url, progress_callback);
this.setup(file_path);
return file_path;
}
get_dirs() {
return this.dirs;
}
async prompt(extension, save) {
if (this.controller) {
return this.controller.prompt(extension, save);
}
else {
await show_alert(`Cannot ${save ? 'save' : 'open'}`, 'LocalStorage is not currently supported in this browser. You should be able to enable it in your browser settings; it may be under a setting about cookies.');
return null;
}
}
set_storyfile_dir(path) {
return {
storyfile: path,
};
}
async upload(file) {
const file_path = await this.downloader.upload(file);
this.setup(file_path);
return file_path;
}
async delete(path) {
await this.providers[0].delete(path);
}
async exists(path) {
return !!(await this.providers[0].exists(path));
}
async read(path) {
return this.providers[0].read(path);
}
async write(files) {
await this.providers[0].write(files);
}
setup(file_path) {
const parsed_path = path.parse(file_path);
this.dirs.storyfile = parsed_path.dir;
this.dirs.working = '/usr/' + parsed_path.name.toLowerCase().trim();
this.controller?.update_working(this.dirs.working);
}
}
/** Controller for FileDialog */
export class DialogController {
dialog; //FileDialog
metadata = {};
provider;
constructor(provider) {
this.dialog = null;
/*this.dialog = new FileDialog({
target: document.body,
props: {
controller: this,
},
})*/
this.provider = provider;
}
async prompt(extension, save) {
const action = save ? 'Save' : 'Open';
const filter = extension_to_filter(extension);
this.metadata = await this.provider.metadata();
return this.dialog.prompt({
filter,
metadata: this.metadata,
save,
submit_label: action,
title: `${action} a ${filter?.title}`,
});
}
async delete(file) {
if (file.dir) {
const dir_path = file.full_path + '/';
for (const path of Object.keys(this.metadata)) {
if (path.startsWith(dir_path)) {
delete this.metadata[path];
await this.provider.delete(path);
}
}
}
else {
await this.provider.delete(file.full_path);
delete this.metadata[file.full_path];
}
await this.update();
}
async download(file) {
const data = (await this.provider.read(file.full_path));
filesave_saveAs(new Blob([data]), file.name);
}
async new_folder(path) {
// Consider making this a function of the provider, to better support providers which can store actual empty folders
const now = Date.now();
const dir_path = path + '/.dir';
await this.provider.write({ [dir_path]: new Uint8Array(0) });
this.metadata[dir_path] = {
atime: now,
mtime: now,
};
this.dialog.$set({
cur_dir: path,
metadata: this.metadata,
});
}
async rename(file, new_file_name) {
const parsed_path = path.parse(file.full_path);
const new_path = parsed_path.dir + '/' + new_file_name;
await this.provider.rename(file.dir, file.full_path, new_path);
await this.update(true);
}
async update(refresh = false) {
if (refresh) {
this.metadata = await this.provider.metadata();
}
this.dialog.$set({ metadata: this.metadata });
}
update_working(path) {
this.dialog.$set({ cur_dir: path });
}
async upload(files) {
const now = Date.now();
const read_files = {};
for (const [path, data] of Object.entries(files)) {
read_files[path] = await read_uploaded_file(data);
this.metadata[path] = {
atime: now,
mtime: now,
};
}
await this.provider.write(read_files);
await this.update();
}
}
function extension_to_filter(extension) {
switch (extension) {
case '.glkdata':
return {
extensions: ['.glkdata'],
label: 'Data file',
title: 'data file',
};
case '.glksave':
return {
extensions: ['.glksave', '.sav'],
label: 'Save file',
title: 'savefile',
};
case '.txt':
return {
extensions: ['.txt'],
label: 'Transcript or log',
title: 'log',
};
}
}