Skip to content

Commit

Permalink
Add files in the dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
curiousdannii committed Oct 4, 2024
1 parent fe6fcdf commit b838bb1
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/dialog/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ 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()
await this.dialog!.update_direntry(this.dirs.working)
this.dialog!.update_direntry(this.dirs.working)
return file_path
}

Expand Down
59 changes: 37 additions & 22 deletions src/dialog/browser/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ https://github.com/curiousdannii/asyncglk

import path from 'path-browserify-esm'

Check failure on line 12 in src/dialog/browser/common.ts

View workflow job for this annotation

GitHub Actions / test (Node v18)

Cannot find module 'path-browserify-esm' or its corresponding type declarations.

Check failure on line 12 in src/dialog/browser/common.ts

View workflow job for this annotation

GitHub Actions / test (Node v20)

Cannot find module 'path-browserify-esm' or its corresponding type declarations.

Check failure on line 12 in src/dialog/browser/common.ts

View workflow job for this annotation

GitHub Actions / test (Node v22)

Cannot find module 'path-browserify-esm' or its corresponding type declarations.

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

export class NullProvider implements Provider {
next: Provider = this
async browse(): Promise<DirBrowser> {
return new CachingDirBrowser({}, this)
throw new Error('A NullProvider should not be browsed')
}
async delete(_path: string) {
return null
}
async exists(_path: string) {
return null
}
metadata(): Promise<FilesMetadata> {
throw new Error('Cannot get metadata from NullProvider')
}
async read(_path: string) {
return null
}
Expand All @@ -40,32 +43,23 @@ interface NestableDirEntry extends DirEntry {
}

/** A caching directory browser that receives the list of files once and remembers for as long as the dialog is open */
export class CachingDirBrowser implements DirBrowser {
files: NestableDirEntry = {
children: [],
dir: true,
full_path: '/usr',
name: 'usr',
}
export class DirBrowser {
files!: NestableDirEntry
provider: Provider

constructor(files: Record<string, FileData>, provider: Provider) {
constructor(files: FilesMetadata, provider: Provider) {
this.provider = provider
for (const [file_path, meta] of Object.entries(files)) {
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,
})
}
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())
}

async browse(dir_path: string): Promise<DirEntry[]> {
browse(dir_path: string): DirEntry[] {
if (!dir_path.startsWith('/usr')) {
throw new Error('Can only browse /usr')
}
Expand All @@ -91,4 +85,25 @@ export class CachingDirBrowser implements DirBrowser {
}
return dir_entry
}

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,
})
}
}
}
}
18 changes: 16 additions & 2 deletions src/dialog/browser/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ 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 {NullProvider} from './common.js'
import type {DirBrowser, DownloadOptions, ProgressCallback, Provider} from './interface.js'
import {DirBrowser, NullProvider} from './common.js'
import type {DownloadOptions, FilesMetadata, ProgressCallback, Provider} from './interface.js'
import {utf8decoder} from '../../common/misc.js'

export class DownloadProvider implements Provider {
Expand Down Expand Up @@ -53,6 +53,10 @@ export class DownloadProvider implements Provider {
}
}

metadata(): Promise<FilesMetadata> {
throw new Error('Cannot get metadata from DownloadProvider')
}

async read(path: string): Promise<Uint8Array | null> {
if (this.store.has(path)) {
return this.store.get(path)!
Expand Down Expand Up @@ -178,6 +182,16 @@ export async function read_response(response: Response, progress_callback?: Prog
return result
}

/** Read an uploaded file and return it as a Uint8Array */
export function read_uploaded_file(file: File): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onerror = () => reject(reader.error)
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer))
reader.readAsArrayBuffer(file)
})
}

function url_to_path(url: string) {
if (url.startsWith('https:')) {
return '/download/https/' + url.substring(8)
Expand Down
11 changes: 6 additions & 5 deletions src/dialog/browser/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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 @@ -36,24 +38,23 @@ export interface Provider {
delete(path: string): Promise<void | null>
/** Check if a file exists */
exists(path: string): Promise<boolean | null>
/** Get all file metadata */
metadata(): Promise<FilesMetadata>
/** Read a file */
read(path: string): Promise<Uint8Array | null>
/** Write a file */
write(path: string, data: Uint8Array): Promise<void | null>
}

/** Browse a directory; may cache all the files or request each time you change directory */
export interface DirBrowser {
browse(path: string): Promise<DirEntry[]>
}

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

export type FilesMetadata = Record<string, FileData>

export interface FileData {
atime: number
etag?: string
Expand Down
20 changes: 10 additions & 10 deletions src/dialog/browser/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ https://github.com/curiousdannii/asyncglk

import {decode as base32768_decode, encode as base32768_encode} from 'base32768'

import {CachingDirBrowser, NullProvider} from './common.js'
import type {DirBrowser, FileData, Provider} from './interface.js'
import {DirBrowser, NullProvider} from './common.js'
import type {Provider} from './interface.js'

type WebStorageFileMetadata = Pick<FileData, 'atime' | 'mtime'>
//type WebStorageFileMetadata = Pick<FileData, 'atime' | 'mtime'>

const METADATA_KEY = 'dialog_metadata'

Expand All @@ -40,8 +40,8 @@ export class WebStorageProvider implements Provider {

async browse(): Promise<DirBrowser> {
if (this.browseable) {
const metadata = this.get_metadata()
return new CachingDirBrowser(metadata, this)
const metadata = this.metadata()
return new DirBrowser(metadata, this)
}
else {
return this.next.browse()
Expand All @@ -67,6 +67,10 @@ export class WebStorageProvider implements Provider {
}
}

metadata() {
return JSON.parse(this.store.getItem(METADATA_KEY) || '{}')
}

async read(path: string): Promise<Uint8Array | null> {
if (path.startsWith(this.prefix)) {
const res = this.store.getItem(path)
Expand All @@ -93,13 +97,9 @@ export class WebStorageProvider implements Provider {
}
}

private get_metadata(): Record<string, WebStorageFileMetadata> {
return JSON.parse(this.store.getItem(METADATA_KEY) || '{}')
}

private update_metadata(path: string, op: MetadataUpdateOperation) {
const now = Date.now()
const metadata = this.get_metadata()
const metadata = this.metadata()
switch (op) {
case MetadataUpdateOperation.DELETE:
delete metadata[path]
Expand Down
72 changes: 53 additions & 19 deletions src/dialog/browser/ui/FileDialog.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script context="module" lang="ts">
import type {Filter} from '../browser.js'
import type {DirBrowser, DirEntry} from '../interface.js'
import type {DirBrowser} from '../common.js'
import type {DirEntry} from '../interface.js'
export interface PromptOptions {
dir_browser: DirBrowser
Expand All @@ -12,11 +13,14 @@
</script>

<script lang="ts">
import {read_uploaded_file} from '../download.js'
import AlertDialog, {ALERT_MODE_CONFIRM, ALERT_MODE_PROMPT} from './AlertDialog.svelte'
import BaseDialog from './BaseDialog.svelte'
import DirTree from './DirTree.svelte'
import FileList from './FileList.svelte'
let upload_files: HTMLInputElement
let alert_dialog: AlertDialog
let base_dialog: BaseDialog
let cur_dir: string
Expand All @@ -37,7 +41,7 @@
}
}
export async function prompt(opts: PromptOptions): Promise<string | null> {
export function prompt(opts: PromptOptions): Promise<string | null> {
dir_browser = opts.dir_browser
if (opts.filter?.extensions.join() !== filter?.extensions.join()) {
if (cur_filter !== '*') {
Expand All @@ -50,7 +54,7 @@
}
saving = opts.save
submit_label = opts.submit_label
await update_direntry(cur_dir)
update_direntry(cur_dir)
const promise = base_dialog.open(opts.title)
file_list.clear()
if (saving && filename_input) {
Expand All @@ -62,10 +66,10 @@
})
}
export async function update_direntry(path: string) {
export function update_direntry(path: string) {
file_list.clear()
cur_dir = path
cur_direntry = (await dir_browser.browse(path)).sort((a, b) => {
cur_direntry = (dir_browser.browse(path)).sort((a, b) => {
if (a.dir !== b.dir) {
return +b.dir - +a.dir
}
Expand All @@ -74,6 +78,19 @@
dir_tree = path.substring(1).split('/')
}
async function check_overwrite(filename: string) {
for (const entry of cur_direntry) {
if (!entry.dir && filename === entry.name) {
return !!(await alert_dialog.open(ALERT_MODE_CONFIRM, 'Overwrite file', `Are you sure you want to overwrite ${filename}?`))
}
}
return true
}
function on_add_file() {
upload_files.click()
}
function on_change_dir(ev: CustomEvent) {
const path: string = ev.detail
update_direntry(path)
Expand Down Expand Up @@ -117,19 +134,32 @@
async function on_submit() {
if (saving) {
const filename = filename_input.value.trim()
for (const entry of cur_direntry) {
if (!entry.dir && filename === entry.name) {
const overwrite = await alert_dialog.open(ALERT_MODE_CONFIRM, 'Overwrite file', `Are you sure you want to overwrite ${filename}?`)
if (!overwrite) {
return
}
}
if (await check_overwrite(filename)) {
base_dialog.resolve(filename ? cur_dir + '/' + filename : false)
}
base_dialog.resolve(filename ? '/' + dir_tree.join('/') + '/' + filename : false)
}
else {
base_dialog.resolve('/' + dir_tree.join('/') + '/' + selected_filename || false)
base_dialog.resolve(selected_filename ? cur_dir + '/' + selected_filename : false)
}
}
async function on_upload_files() {
if (upload_files.files) {
const files: Record<string, Uint8Array> = {}
let have_files = false
for (const file of upload_files.files) {
if (await check_overwrite(file.name)) {
files[cur_dir + '/' + file.name] = await read_uploaded_file(file)
have_files = true
}
}
if (have_files) {
await dir_browser.add_files(files)
}
}
upload_files.value = ''
// Update the current file list
update_direntry(cur_dir)
}
</script>

Expand All @@ -146,6 +176,10 @@
flex-grow: 1;
margin-left: 6px;
}
#add_file {
display: none;
}
</style>

<BaseDialog
Expand All @@ -154,11 +188,10 @@
max_height=500px
max_width=700px
>
{#if saving}
<div class="actions">
<button on:click={on_new_folder}>New Folder</button>
</div>
{/if}
<div class="actions">
<button on:click={on_add_file}>Add file</button>
<button on:click={on_new_folder}>New Folder</button>
</div>
<DirTree
dir_tree={dir_tree}
on:change_dir={on_change_dir}
Expand Down Expand Up @@ -196,5 +229,6 @@
{/if}
</div>
</div>
<input bind:this={upload_files} id="add_file" type="file" multiple on:change={on_upload_files}>
<AlertDialog bind:this={alert_dialog}></AlertDialog>
</BaseDialog>

0 comments on commit b838bb1

Please sign in to comment.