Skip to content

Commit

Permalink
Use a controller to work between the dialog and storage, the dialog i…
Browse files Browse the repository at this point in the history
…s now much more polished
  • Loading branch information
curiousdannii committed Oct 7, 2024
1 parent 0f4555e commit 3d94932
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 278 deletions.
128 changes: 104 additions & 24 deletions src/dialog/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ https://github.com/curiousdannii/asyncglk
*/

import {saveAs as filesave_saveAs} from 'file-saver'
import path from 'path-browserify-esm'

import type {DialogDirectories, DialogOptions} from '../common/interface.js'
import {DownloadProvider} from './download.js'
import type {BrowserDialog, DownloadOptions, ProgressCallback, Provider} from './interface.js'
import {DownloadProvider, read_uploaded_file} from './download.js'
import type {BrowserDialog, DirEntry, DownloadOptions, FilesMetadata, ProgressCallback, Provider} from './interface.js'
import {WebStorageProvider} from './storage.js'
import DialogUI from './ui/FileDialog.svelte'
import FileDialog from './ui/FileDialog.svelte'

export class ProviderBasedBrowserDialog implements BrowserDialog {
'async' = true as const
private dialog: DialogUI | undefined
private controller!: DialogController
private dirs: DialogDirectories = {
storyfile: '',
system_cwd: '/',
Expand All @@ -43,16 +44,11 @@ export class ProviderBasedBrowserDialog implements BrowserDialog {
if (next) {
provider.next = next
}
// Set up the Svelte UI
if (provider.browseable) {
this.controller = new DialogController(provider)
}
}

// Set up the Svelte UI
const dir_browser = await this.providers[0].browse()
this.dialog = new DialogUI({
target: document.body,
props: {
dir_browser,
},
})
}

async download(url: string, progress_callback?: ProgressCallback): Promise<string> {
Expand All @@ -65,16 +61,8 @@ export class ProviderBasedBrowserDialog implements BrowserDialog {
return this.dirs
}

async prompt(extension: string, save: boolean): Promise<string | null> {
const action = save ? 'Save' : 'Restore'
const filter = extension_to_filter(extension)
return this.dialog!.prompt({
dir_browser: await this.providers[0].browse(),
filter,
save,
submit_label: action,
title: `${action} a ${filter?.title}`,
})
prompt(extension: string, save: boolean): Promise<string | null> {
return this.controller.prompt(extension, save)
}

set_storyfile_dir(path: string): Partial<DialogDirectories> {
Expand Down Expand Up @@ -109,7 +97,99 @@ export class ProviderBasedBrowserDialog implements BrowserDialog {
const parsed_path = path.parse(file_path)
this.dirs.storyfile = parsed_path.dir
this.dirs.working = '/usr/' + parsed_path.name.toLowerCase().trim()
this.dialog!.update_direntry(this.dirs.working)
this.controller.update_working(this.dirs.working)
}
}

/** Controller for FileDialog */
export class DialogController {
private dialog: FileDialog
private metadata: FilesMetadata = {}
private provider: Provider

constructor(provider: Provider) {
this.dialog = new FileDialog({
target: document.body,
props: {
controller: this,
},
})
this.provider = provider
}

async prompt(extension: string, save: boolean): Promise<string | null> {
const action = save ? 'Save' : 'Restore'
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: DirEntry) {
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]
if (!path.endsWith('.fakefile')) {
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: DirEntry) {
const data = (await this.provider.read(file.full_path))!
filesave_saveAs(new Blob([data]), file.name)
}

new_folder(path: string) {
this.metadata[path + '/.fakefile'] = {atime: 0, mtime: 0}
this.dialog.$set({
cur_dir: path,
metadata: this.metadata,
})
}

async rename(file: DirEntry, new_file_name: string) {
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: boolean = false) {
if (refresh) {
this.metadata = await this.provider.metadata()
}
this.dialog.$set({metadata: this.metadata})
}

update_working(path: string) {
this.dialog.$set({cur_dir: path})
}

async upload(files: Record<string, File>) {
const now = Date.now()
for (const [path, data] of Object.entries(files)) {
await this.provider.write(path, await read_uploaded_file(data))
this.metadata[path] = {
atime: now,
mtime: now,
}
}
await this.update()
}
}

Expand Down
95 changes: 5 additions & 90 deletions src/dialog/browser/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,11 @@ https://github.com/curiousdannii/asyncglk
*/

import path from 'path-browserify-esm'
import {saveAs as filesave_saveAs} from 'file-saver'

import type {DirEntry, FileData, FilesMetadata, Provider} from './interface.js'
import type {FilesMetadata, Provider} from './interface.js'

export class NullProvider implements Provider {
browseable = false
next: Provider = this
async browse(): Promise<DirBrowser> {
throw new Error('A NullProvider should not be browsed')
}
async delete(_path: string) {
return null
}
Expand All @@ -31,90 +26,10 @@ export class NullProvider implements Provider {
async read(_path: string) {
return null
}
rename(): Promise<void> {
throw new Error('Invalid method')
}
async write(_path: string, _data: Uint8Array) {
return null
}
}

interface NestableDirEntry extends DirEntry {
children?: NestableDirEntry[]
dir: boolean
name: string
meta?: FileData
}

/** A caching directory browser that receives the list of files once and remembers for as long as the dialog is open */
export class DirBrowser {
files!: NestableDirEntry
provider: Provider

constructor(files: FilesMetadata, provider: Provider) {
this.provider = provider
this.update(files)
}

async add_files(files: Record<string, Uint8Array>) {
for (const [path, data] of Object.entries(files)) {
await this.provider.write(path, data)
}
this.update(await this.provider.metadata())
}

browse(dir_path: string): DirEntry[] {
if (!dir_path.startsWith('/usr')) {
throw new Error('Can only browse /usr')
}
return this.cd(dir_path).children!
}

private cd(path: string): NestableDirEntry {
const dirs = path.substring(1).split('/')
dirs.shift()
let dir_entry = this.files
for (const subdir of dirs) {
let new_subdir = dir_entry.children!.find(child => child.name === subdir)!
if (!new_subdir) {
new_subdir = {
children: [],
dir: true,
full_path: dir_entry.full_path + '/' + subdir,
name: subdir,
}
dir_entry.children!.push(new_subdir)
}
dir_entry = new_subdir
}
return dir_entry
}

async delete(file: DirEntry) {
await this.provider.delete(file.full_path)
this.update(await this.provider.metadata())
}

async download(file: DirEntry) {
const data = (await this.provider.read(file.full_path))!
filesave_saveAs(new Blob([data]), file.name)
}

private update(metadata: FilesMetadata) {
this.files = {
children: [],
dir: true,
full_path: '/usr',
name: 'usr',
}
for (const [file_path, meta] of Object.entries(metadata)) {
if (file_path.startsWith('/usr/')) {
const parsed_path = path.parse(file_path)
const dir_entry = this.cd(parsed_path.dir)
dir_entry.children!.push({
dir: false,
full_path: file_path,
name: parsed_path.base,
meta,
})
}
}
}
}
11 changes: 6 additions & 5 deletions src/dialog/browser/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ https://github.com/curiousdannii/asyncglk

// The download provider stores its own files just in a map (maybe to be cached in the future), but if files are written next to them, then they need to be done so in another provider

import {DirBrowser, NullProvider} from './common.js'
import {NullProvider} from './common.js'
import type {DownloadOptions, FilesMetadata, ProgressCallback, Provider} from './interface.js'
import {utf8decoder} from '../../common/misc.js'

export class DownloadProvider implements Provider {
browseable = false
next = new NullProvider()
private options: DownloadOptions
private store: Map<string, Uint8Array> = new Map()
Expand All @@ -38,10 +39,6 @@ export class DownloadProvider implements Provider {
return path
}

async browse(): Promise<DirBrowser> {
return this.next.browse()
}

async delete(path: string): Promise<void | null> {
if (this.store.has(path)) {
this.store.delete(path)
Expand Down Expand Up @@ -74,6 +71,10 @@ export class DownloadProvider implements Provider {
// TODO: try downloading a sibling file
}

rename(): Promise<void> {
throw new Error('Invalid method')
}

async write(path: string, data: Uint8Array): Promise<void | null> {
return this.next.write(path, data)
}
Expand Down
10 changes: 5 additions & 5 deletions src/dialog/browser/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ https://github.com/curiousdannii/asyncglk

import type {AsyncDialog} from '../common/interface.js'

import {DirBrowser} from './common.js'

export type ProgressCallback = (bytes: number) => void

export interface BrowserDialog extends AsyncDialog {
Expand All @@ -31,10 +29,10 @@ export interface DownloadOptions {

/** A provider handles part of the filesystem, and can cascade down to another provider for files it doesn't handle */
export interface Provider {
/** Whether we can browse this provider */
browseable: boolean
/** A link to the next provider */
next: Provider
/** Get a `DirBrowser` instance for browsing */
browse(): Promise<DirBrowser>
/** Delete a file */
delete(path: string): Promise<void | null>
/** Check if a file exists */
Expand All @@ -43,15 +41,17 @@ export interface Provider {
metadata(): Promise<FilesMetadata>
/** Read a file */
read(path: string): Promise<Uint8Array | null>
/** Rename a file or folder */
rename(dir: boolean, path: string, new_name: string): Promise<void>
/** Write a file */
write(path: string, data: Uint8Array): Promise<void | null>
}

export interface DirEntry {
dir: boolean
full_path: string
name: string
meta?: FileData
name: string
}

export type FilesMetadata = Record<string, FileData>
Expand Down
Loading

0 comments on commit 3d94932

Please sign in to comment.