Add directory uploader
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
{{ template "opengraph" .OGData }}
|
||||
<script>
|
||||
window.initial_node = {{.Other}};
|
||||
window.user = {{.User}};
|
||||
window.api_endpoint = '{{.APIEndpoint}}';
|
||||
</script>
|
||||
|
||||
|
@@ -31,7 +31,6 @@ const builddir = "../res/static/svelte"
|
||||
export default [
|
||||
"file_viewer",
|
||||
"filesystem",
|
||||
"modal",
|
||||
"user_home",
|
||||
"user_file_manager",
|
||||
"admin_panel",
|
||||
|
@@ -148,7 +148,10 @@ let update_chart = async (base, timespan, interval) => {
|
||||
{/if}
|
||||
{#if state.base.type === "file"}
|
||||
<tr><td>File type</td><td>{state.base.file_type}</td></tr>
|
||||
<tr><td>File size</td><td>{formatDataVolume(state.base.file_size)}</td></tr>
|
||||
<tr>
|
||||
<td>File size</td>
|
||||
<td>{formatDataVolume(state.base.file_size, 4)} ( {formatThousands(state.base.file_size)} B )</td>
|
||||
</tr>
|
||||
<tr><td>Unique downloads</td><td>{formatThousands(total_downloads)}</td></tr>
|
||||
<tr>
|
||||
<td>Free bandwidth used</td>
|
||||
@@ -193,7 +196,6 @@ let update_chart = async (base, timespan, interval) => {
|
||||
|
||||
<style>
|
||||
td:first-child {
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
}
|
||||
td {
|
||||
|
@@ -10,6 +10,7 @@ import DetailsWindow from './DetailsWindow.svelte';
|
||||
import Navigator from './Navigator.svelte';
|
||||
import FilePreview from './viewers/FilePreview.svelte';
|
||||
import SearchView from './SearchView.svelte';
|
||||
import UploadWidget from './upload_widget/UploadWidget.svelte';
|
||||
|
||||
let loading = true
|
||||
let toolbar_visible = (window.innerWidth > 600)
|
||||
@@ -110,8 +111,6 @@ const loading_evt = e => {
|
||||
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<Navigator
|
||||
bind:this={fs_navigator}
|
||||
bind:state
|
||||
@@ -140,18 +139,6 @@ const loading_evt = e => {
|
||||
</div>
|
||||
|
||||
<div class="viewer_area">
|
||||
<Toolbar
|
||||
visible={toolbar_visible}
|
||||
fs_navigator={fs_navigator}
|
||||
state={state}
|
||||
bind:details_visible={details_visible}
|
||||
edit_window={edit_window}
|
||||
bind:edit_visible={edit_visible}
|
||||
bind:view={view}
|
||||
on:download={download}
|
||||
on:search={search}
|
||||
/>
|
||||
|
||||
<div class="file_preview checkers" class:toolbar_visible>
|
||||
{#if view === "file"}
|
||||
<FilePreview
|
||||
@@ -171,6 +158,18 @@ const loading_evt = e => {
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Toolbar
|
||||
visible={toolbar_visible}
|
||||
fs_navigator={fs_navigator}
|
||||
state={state}
|
||||
bind:details_visible={details_visible}
|
||||
edit_window={edit_window}
|
||||
bind:edit_visible={edit_visible}
|
||||
bind:view={view}
|
||||
on:download={download}
|
||||
on:search={search}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- This frame will load the download URL when a download button is pressed -->
|
||||
@@ -179,21 +178,25 @@ const loading_evt = e => {
|
||||
title="Frame for downloading files"
|
||||
style="display: none; width: 1px; height: 1px;">
|
||||
</iframe>
|
||||
|
||||
<DetailsWindow
|
||||
state={state}
|
||||
bind:visible={details_visible}
|
||||
/>
|
||||
|
||||
<EditWindow
|
||||
bind:this={edit_window}
|
||||
bind:visible={edit_visible}
|
||||
bucket={state.root.id}
|
||||
fs_navigator={fs_navigator}
|
||||
on:loading={loading_evt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DetailsWindow
|
||||
state={state}
|
||||
bind:visible={details_visible}
|
||||
/>
|
||||
|
||||
<EditWindow
|
||||
bind:this={edit_window}
|
||||
bind:visible={edit_visible}
|
||||
bucket={state.root.id}
|
||||
fs_navigator={fs_navigator}
|
||||
on:loading={loading_evt}
|
||||
/>
|
||||
|
||||
<UploadWidget fs_state={state} drop_upload on:uploads_finished={() => fs_navigator.reload()}/>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<style>
|
||||
/* Viewer container */
|
||||
.file_viewer {
|
||||
@@ -215,7 +218,6 @@ const loading_evt = e => {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
z-index: 10;
|
||||
box-shadow: none;
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -258,7 +260,6 @@ const loading_evt = e => {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.file_preview {
|
||||
|
@@ -1,5 +1,19 @@
|
||||
import { fs_path_url } from './FilesystemUtil.js'
|
||||
|
||||
export const fs_check_response = async resp => {
|
||||
let text = await resp.text()
|
||||
if (resp.status >= 400) {
|
||||
let error
|
||||
try {
|
||||
error = JSON.parse(text)
|
||||
} catch (err) {
|
||||
error = text
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return JSON.parse(text)
|
||||
}
|
||||
|
||||
export const fs_mkdir = async (bucket, path, opts = null) => {
|
||||
const form = new FormData()
|
||||
form.append("action", "mkdir")
|
||||
@@ -8,18 +22,28 @@ export const fs_mkdir = async (bucket, path, opts = null) => {
|
||||
form.append("mode", opts.mode)
|
||||
}
|
||||
|
||||
const resp = await fetch(fs_path_url(bucket, path), { method: "POST", body: form });
|
||||
if (resp.status >= 400) {
|
||||
throw new Error(await resp.text())
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path), { method: "POST", body: form })
|
||||
)
|
||||
}
|
||||
|
||||
export const fs_mkdirall = async (bucket, path, opts = null) => {
|
||||
const form = new FormData()
|
||||
form.append("action", "mkdirall")
|
||||
|
||||
if (opts && opts.mode) {
|
||||
form.append("mode", opts.mode)
|
||||
}
|
||||
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path), { method: "POST", body: form })
|
||||
)
|
||||
}
|
||||
|
||||
export const fs_get_node = async (bucket, path) => {
|
||||
const resp = await fetch(fs_path_url(bucket, path) + "?stat");
|
||||
if (resp.status >= 400) {
|
||||
throw await resp.text()
|
||||
}
|
||||
return resp.json()
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path) + "?stat")
|
||||
)
|
||||
}
|
||||
|
||||
// Updates a node's parameters. Available options are:
|
||||
@@ -52,11 +76,9 @@ export const fs_update = async (bucket, path, opts) => {
|
||||
form.append("write_password", opts.write_password)
|
||||
}
|
||||
|
||||
const resp = await fetch(fs_path_url(bucket, path), { method: "POST", body: form })
|
||||
if (resp.status >= 400) {
|
||||
throw new Error(await resp.text())
|
||||
}
|
||||
return resp.json()
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path), { method: "POST", body: form })
|
||||
)
|
||||
}
|
||||
|
||||
export const fs_rename = async (bucket, old_path, new_path) => {
|
||||
@@ -64,47 +86,40 @@ export const fs_rename = async (bucket, old_path, new_path) => {
|
||||
form.append("action", "rename")
|
||||
form.append("target", new_path)
|
||||
|
||||
const resp = await fetch(fs_path_url(bucket, old_path), { method: "POST", body: form })
|
||||
if (resp.status >= 400) {
|
||||
throw new Error(await resp.text())
|
||||
}
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, old_path), { method: "POST", body: form })
|
||||
)
|
||||
}
|
||||
|
||||
export const fs_delete = async (bucket, path) => {
|
||||
const resp = await fetch(fs_path_url(bucket, path), { method: "DELETE" });
|
||||
if (resp.status >= 400) {
|
||||
throw new Error(await resp.text())
|
||||
}
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path), { method: "DELETE" })
|
||||
)
|
||||
}
|
||||
export const fs_delete_all = async (bucket, path) => {
|
||||
const resp = await fetch(fs_path_url(bucket, path) + "?recursive", { method: "DELETE" });
|
||||
if (resp.status >= 400) {
|
||||
throw new Error(await resp.text())
|
||||
}
|
||||
return await fs_check_response(
|
||||
await fetch(fs_path_url(bucket, path) + "?recursive", { method: "DELETE" })
|
||||
)
|
||||
}
|
||||
|
||||
export const fs_search = async (bucket, path, term, limit = 10) => {
|
||||
const resp = await fetch(
|
||||
fs_path_url(bucket, path) +
|
||||
"?search=" + encodeURIComponent(term) +
|
||||
"&limit=" + limit
|
||||
return await fs_check_response(
|
||||
await fetch(
|
||||
fs_path_url(bucket, path) +
|
||||
"?search=" + encodeURIComponent(term) +
|
||||
"&limit=" + limit
|
||||
)
|
||||
)
|
||||
if (resp.status >= 400) {
|
||||
throw await resp.text()
|
||||
}
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
export const fs_timeseries = async (bucket, path, start, end, interval = 60) => {
|
||||
const resp = await fetch(
|
||||
fs_path_url(bucket, path) +
|
||||
"?timeseries" +
|
||||
"&start=" + start.toISOString() +
|
||||
"&end=" + end.toISOString() +
|
||||
"&interval=" + interval
|
||||
return await fs_check_response(
|
||||
await fetch(
|
||||
fs_path_url(bucket, path) +
|
||||
"?timeseries" +
|
||||
"&start=" + start.toISOString() +
|
||||
"&end=" + end.toISOString() +
|
||||
"&interval=" + interval
|
||||
)
|
||||
)
|
||||
if (resp.status >= 400) {
|
||||
throw await resp.text()
|
||||
}
|
||||
return resp.json()
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ export const fs_path_url = (bucket, path) => {
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
split[i] = encodeURIComponent(split[i])
|
||||
}
|
||||
return api_endpoint + "/filesystem/" + bucket + split.join("/")
|
||||
return window.api_endpoint + "/filesystem/" + bucket + split.join("/")
|
||||
}
|
||||
|
||||
export const fs_file_url = (bucket, path) => {
|
||||
|
@@ -37,10 +37,7 @@ export const navigate = async (path, push_history) => {
|
||||
let resp = await fs_get_node(state.root.id, path)
|
||||
open_node(resp, push_history)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
let errj = JSON.parse(err)
|
||||
|
||||
if (errj.value === "path_not_found") {
|
||||
if (err.value && err.value === "path_not_found") {
|
||||
if (path !== "/" && path !== "") {
|
||||
console.debug("Path", path, "was not found, trying to navigate to parent")
|
||||
navigate(fs_split_path(path).parent, push_history)
|
||||
|
@@ -130,7 +130,6 @@ const share_tumblr = () => {
|
||||
float: left;
|
||||
background: var(--shaded_background);
|
||||
text-align: center;
|
||||
z-index: 48;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: left 0.3s, opacity 0.3s;
|
||||
|
@@ -134,7 +134,6 @@ let share = async () => {
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
width: 8em;
|
||||
z-index: 49;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
left: -8em;
|
||||
|
@@ -17,7 +17,11 @@ let create_dir = () => {
|
||||
).then(resp => {
|
||||
new_dir_name = "" // Clear input field
|
||||
}).catch(err => {
|
||||
alert("Error: "+err)
|
||||
if (err.value && err.value === "node_already_exists") {
|
||||
alert("A directory with this name already exists")
|
||||
} else {
|
||||
alert(err)
|
||||
}
|
||||
}).finally(() => {
|
||||
dispatch("done")
|
||||
})
|
||||
|
78
svelte/src/filesystem/upload_widget/DropUpload.svelte
Normal file
78
svelte/src/filesystem/upload_widget/DropUpload.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
let dragging = false
|
||||
|
||||
const paste = (e) => {
|
||||
if (e.clipboardData.files.length !== 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch("upload", e.clipboardData.files)
|
||||
}
|
||||
}
|
||||
|
||||
const drop = async e => {
|
||||
dragging = false;
|
||||
|
||||
if (e.dataTransfer.files || e.dataTransfer.items) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// if directory support is available
|
||||
if(e.dataTransfer && e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
for (let i = 0; i < e.dataTransfer.items.length; i++) {
|
||||
let entry = await e.dataTransfer.items[i].webkitGetAsEntry();
|
||||
if (entry) {
|
||||
await read_dir_recursive(entry);
|
||||
}
|
||||
}
|
||||
} else if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
dispatch("upload_files", e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
const read_dir_recursive = item => {
|
||||
if (item.isDirectory) {
|
||||
item.createReader().readEntries(entries => {
|
||||
entries.forEach(entry => {
|
||||
read_dir_recursive(entry);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
item.file(file => {
|
||||
dispatch("upload_file", file)
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
|
||||
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
|
||||
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
|
||||
on:drop={drop}
|
||||
on:paste={paste}
|
||||
/>
|
||||
|
||||
{#if dragging}
|
||||
<div class="drag_target" transition:fade={{duration: 200}}>
|
||||
Drop files here to upload them
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.drag_target {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 50px;
|
||||
font-size: 2em;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
106
svelte/src/filesystem/upload_widget/UploadFunc.js
Normal file
106
svelte/src/filesystem/upload_widget/UploadFunc.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// Uploads a file to the logged in user's pixeldrain account. If no user is
|
||||
// logged in the file is uploaded anonymously.
|
||||
//
|
||||
// on_progress reports progress on the file upload, parameter 1 is the uploaded
|
||||
// file size and parameter 2 is the total file size
|
||||
//
|
||||
// on_success is called when the upload is done, the only parameter is the file
|
||||
// ID
|
||||
//
|
||||
// on_error is called when the upload has failed. The parameters are the error
|
||||
|
||||
import { fs_get_node, fs_mkdirall } from "../FilesystemAPI"
|
||||
import { fs_path_url, fs_split_path } from "../FilesystemUtil"
|
||||
|
||||
// code and an error message
|
||||
export const upload_file = async (file, bucket, path, on_progress, on_success, on_error) => {
|
||||
// Check the file size limit. For free accounts it's 20 GB
|
||||
if (window.user.subscription.file_size_limit === 0) {
|
||||
window.user.subscription.file_size_limit = 20e9
|
||||
}
|
||||
|
||||
if (file.size > window.user.subscription.file_size_limit) {
|
||||
on_failure(
|
||||
"file_too_large",
|
||||
"This file is too large. Check out the Pro subscription to increase the file size limit"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the parent directory exists
|
||||
try {
|
||||
await ensure_parent_dir(bucket, path)
|
||||
} catch (err) {
|
||||
if (err.value && err.message) {
|
||||
on_error(err.value, err.message)
|
||||
} else {
|
||||
on_error(err, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("PUT", fs_path_url(bucket, path), true);
|
||||
xhr.timeout = 86400000; // 24 hours, to account for slow connections
|
||||
|
||||
xhr.upload.addEventListener("progress", evt => {
|
||||
if (on_progress && evt.lengthComputable) {
|
||||
on_progress(evt.loaded, evt.total)
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
// readystate 4 means the upload is done
|
||||
if (xhr.readyState !== 4) {
|
||||
return
|
||||
}
|
||||
|
||||
if (xhr.status >= 100 && xhr.status < 400) {
|
||||
// Request is a success
|
||||
on_success(JSON.parse(xhr.response).id)
|
||||
} else if (xhr.status >= 400) {
|
||||
// Request failed
|
||||
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
|
||||
|
||||
let resp;
|
||||
if (xhr.status === 429) {
|
||||
resp = {
|
||||
value: "too_many_requests",
|
||||
message: "Too many requests. Please wait a few seconds",
|
||||
}
|
||||
} else {
|
||||
resp = JSON.parse(xhr.response)
|
||||
}
|
||||
|
||||
on_error(resp.value, resp.message)
|
||||
} else if (xhr.status === 0) {
|
||||
on_error("request_failed", "Your request did not arrive, check your network connection")
|
||||
} else {
|
||||
on_error(xhr.responseText, xhr.responseText)
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(file);
|
||||
}
|
||||
|
||||
const ensure_parent_dir = async (bucket, path) => {
|
||||
let parent = fs_split_path(path).parent
|
||||
|
||||
console.debug("Checking if parent directory exists", parent)
|
||||
|
||||
try {
|
||||
let node = await fs_get_node(bucket, parent)
|
||||
if (node.path[node.base_index].type !== "dir") {
|
||||
throw "Path " + path + " is not a directory"
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.value && err.value === "path_not_found") {
|
||||
// Directory does not exist. Create it
|
||||
await fs_mkdirall(bucket, parent)
|
||||
|
||||
console.debug("Created parent directory", parent)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
68
svelte/src/filesystem/upload_widget/UploadProgress.svelte
Normal file
68
svelte/src/filesystem/upload_widget/UploadProgress.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { upload_file } from "./UploadFunc";
|
||||
import ProgressBar from "../../util/ProgressBar.svelte";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
export let job = {
|
||||
file: null,
|
||||
name: "",
|
||||
id: "",
|
||||
status: "",
|
||||
}
|
||||
export let total = 0
|
||||
export let loaded = 0
|
||||
let error_code = ""
|
||||
let error_message = ""
|
||||
|
||||
export const start = () => {
|
||||
upload_file(
|
||||
job.file,
|
||||
job.bucket,
|
||||
job.path,
|
||||
(prog_loaded, prog_total) => {
|
||||
loaded = prog_loaded
|
||||
total = prog_total
|
||||
},
|
||||
async () => {
|
||||
job.status = "finished"
|
||||
dispatch("finished")
|
||||
},
|
||||
(code, message) => {
|
||||
console.log("error", code, message)
|
||||
error_code = code
|
||||
error_message = message
|
||||
job.status = "error"
|
||||
|
||||
// Wait with reporting so the user can read the error message
|
||||
setTimeout(() => dispatch("finished"), 60000)
|
||||
},
|
||||
)
|
||||
|
||||
job.status = "uploading"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="upload_progress" transition:fade={{duration: 200}} class:error={job.status === "error"}>
|
||||
{job.file.name}<br/>
|
||||
{#if error_code !== ""}
|
||||
{error_message}<br/>
|
||||
{error_code}<br/>
|
||||
{/if}
|
||||
<ProgressBar total={total} used={loaded}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.upload_progress {
|
||||
display: block;
|
||||
padding: 2px 4px 1px 4px;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.error {
|
||||
background: var(--danger_color);
|
||||
color: var(--highlight_text_color);
|
||||
}
|
||||
</style>
|
202
svelte/src/filesystem/upload_widget/UploadWidget.svelte
Normal file
202
svelte/src/filesystem/upload_widget/UploadWidget.svelte
Normal file
@@ -0,0 +1,202 @@
|
||||
<script>
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import DropUpload from "./DropUpload.svelte";
|
||||
import UploadProgress from "./UploadProgress.svelte";
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
export let fs_state
|
||||
|
||||
let file_input_field;
|
||||
let file_input_change = e => {
|
||||
// Start uploading the files async
|
||||
upload_files(e.target.files)
|
||||
|
||||
// This resets the file input field
|
||||
file_input_field.nodeValue = ""
|
||||
}
|
||||
export const pick_files = () => {
|
||||
file_input_field.click()
|
||||
}
|
||||
|
||||
export let drop_upload = false
|
||||
let visible = false
|
||||
let upload_queue = [];
|
||||
let task_id_counter = 0
|
||||
|
||||
export const upload_files = async (files) => {
|
||||
if (files.length === 0) {
|
||||
return
|
||||
}
|
||||
if (current_dir.type !== "dir") {
|
||||
alert("Can only upload to directory")
|
||||
return
|
||||
}
|
||||
|
||||
// Add files to the queue
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await upload_file(files[i])
|
||||
}
|
||||
}
|
||||
|
||||
export const upload_file = async file => {
|
||||
if (fs_state.base.type !== "dir") {
|
||||
alert("Can only upload to directory")
|
||||
return
|
||||
}
|
||||
if (file.type === "" && file.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
upload_queue.push({
|
||||
task_id: task_id_counter,
|
||||
file: file,
|
||||
bucket: fs_state.root.id,
|
||||
path: fs_state.base.path + "/" + file.webkitRelativePath,
|
||||
component: null,
|
||||
status: "queued",
|
||||
total_size: file.size,
|
||||
loaded_size: 0,
|
||||
})
|
||||
task_id_counter++
|
||||
|
||||
// Reassign array and wait for tick to complete. After the tick is completed
|
||||
// each upload progress bar will have bound itself to its array item
|
||||
upload_queue = upload_queue
|
||||
|
||||
if (active_uploads === 0 && state !== "uploading") {
|
||||
state = "uploading"
|
||||
visible = true
|
||||
await tick()
|
||||
await start_upload()
|
||||
}
|
||||
}
|
||||
|
||||
let active_uploads = 0
|
||||
let state = "idle"
|
||||
|
||||
const start_upload = async () => {
|
||||
for (let i = 0; i < upload_queue.length && active_uploads < 3; i++) {
|
||||
if (upload_queue[i]) {
|
||||
if (upload_queue[i].status === "queued") {
|
||||
active_uploads++
|
||||
upload_queue[i].component.start()
|
||||
upload_queue[i].status = "uploading"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (active_uploads === 0) {
|
||||
state = "finished"
|
||||
|
||||
let file_ids = []
|
||||
upload_queue.forEach(job => {
|
||||
if (job.status === "finished" && job.id !== "") {
|
||||
file_ids.push(job.id)
|
||||
}
|
||||
})
|
||||
|
||||
dispatch("uploads_finished", file_ids)
|
||||
|
||||
upload_queue = []
|
||||
setTimeout(() => {
|
||||
if (state === "finished") {
|
||||
visible = false
|
||||
}
|
||||
}, 10000)
|
||||
} else {
|
||||
state = "uploading"
|
||||
}
|
||||
}
|
||||
|
||||
const finish_upload = (e) => {
|
||||
active_uploads--
|
||||
upload_queue = upload_queue
|
||||
start_upload()
|
||||
}
|
||||
|
||||
const leave_confirmation = e => {
|
||||
if (state === "uploading") {
|
||||
e.preventDefault()
|
||||
e.returnValue = "If you close this page your files will stop uploading. Do you want to continue?"
|
||||
return e.returnValue
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={leave_confirmation} />
|
||||
|
||||
<input
|
||||
bind:this={file_input_field}
|
||||
on:change={file_input_change}
|
||||
class="upload_input" type="file" name="file" multiple="multiple"
|
||||
/>
|
||||
|
||||
{#if visible}
|
||||
<div class="upload_widget" transition:fade={{duration: 200}}>
|
||||
<div class="header">
|
||||
{#if state === "idle"}
|
||||
Waiting for files
|
||||
{:else if state === "uploading"}
|
||||
Uploading files...
|
||||
{:else if state === "finished"}
|
||||
Done
|
||||
{/if}
|
||||
</div>
|
||||
<div class="body">
|
||||
{#each upload_queue as job}
|
||||
{#if job.status !== "finished"}
|
||||
<UploadProgress bind:this={job.component} job={job} on:finished={finish_upload}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if drop_upload}
|
||||
<DropUpload on:upload_files={e => upload_files(e.detail)} on:upload_file={e => upload_file(e.detail)}/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.upload_input {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.upload_widget {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
max-width: 80%;
|
||||
height: auto;
|
||||
max-height: 50%;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
border-radius: 20px 20px 8px 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 1px 1px 8px var(--shadow_color);
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
background: var(--background_color);
|
||||
color: var(--background_text_color);
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
padding: 4px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
background: var(--body_color);
|
||||
color: var(--body_text_color);
|
||||
overflow-y: auto;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
@@ -1,10 +0,0 @@
|
||||
import App from './modal/App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {
|
||||
name: 'world'
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
@@ -1,45 +0,0 @@
|
||||
<script>
|
||||
import Modal from '../util/Modal.svelte';
|
||||
let modal1;
|
||||
let modal2;
|
||||
let modal3;
|
||||
</script>
|
||||
|
||||
<button on:click={modal1.show}>
|
||||
show modal
|
||||
</button>
|
||||
|
||||
<br/>
|
||||
|
||||
<Modal bind:this={modal3} title="Third modal" width="700px">
|
||||
<img src="https://pixeldrain.com/res/img/header_orbitron_wide.png" alt="logo" style="max-width: 100%;"/>
|
||||
</Modal>
|
||||
<Modal bind:this={modal2} title="Wat!" width="1000px">
|
||||
<ol class="definition-list">
|
||||
<li>of or relating to modality in logic</li>
|
||||
<li>containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy</li>
|
||||
<li>of or relating to a musical mode</li>
|
||||
<li>of or relating to structure as opposed to substance</li>
|
||||
<li><button on:click={modal3.show}>third modal</button></li>
|
||||
<li>of, relating to, or constituting a grammatical form or category characteristically indicating predication</li>
|
||||
<li>of or relating to a statistical mode</li>
|
||||
</ol>
|
||||
|
||||
<a href="https://www.merriam-webster.com/dictionary/modal">merriam-webster.com</a>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal1} title="Hello!">
|
||||
<ol class="definition-list">
|
||||
<li>of or relating to modality in logic</li>
|
||||
<li>containing provisions as to the mode of procedure or the manner of taking effect —used of a contract or legacy</li>
|
||||
<li>of or relating to a musical mode</li>
|
||||
<li>of or relating to structure as opposed to substance</li>
|
||||
<li>of, relating to, or constituting a grammatical form or category characteristically indicating predication</li>
|
||||
<li>of or relating to a statistical mode</li>
|
||||
</ol>
|
||||
|
||||
<a href="https://www.merriam-webster.com/dictionary/modal">merriam-webster.com</a>
|
||||
<br/>
|
||||
|
||||
<button on:click={modal2.show}>show modal</button>
|
||||
</Modal>
|
@@ -108,11 +108,6 @@ let handle_errors = (response) => {
|
||||
</script>
|
||||
|
||||
<form method="POST" on:submit={submit}>
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner></Spinner>
|
||||
</div>
|
||||
{/if}
|
||||
{#if submitted}
|
||||
{#if submit_result.messages}
|
||||
<div id="submit_result" class:highlight_green={submit_result.success} class:highlight_red={!submit_result.success}>
|
||||
@@ -239,15 +234,20 @@ let handle_errors = (response) => {
|
||||
<button type="submit" class="button_highlight">{@html config.submit_label}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="spinner_container">
|
||||
<Spinner></Spinner>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.spinner_container {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
@@ -17,6 +17,5 @@ export let loading = false
|
||||
right: 10px;
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
@@ -56,11 +56,11 @@ export const upload_file = (file, name, on_progress, on_success, on_error) => {
|
||||
resp = JSON.parse(xhr.response)
|
||||
}
|
||||
|
||||
on_failure(resp.value, resp.message)
|
||||
on_error(resp.value, resp.message)
|
||||
} else if (xhr.status === 0) {
|
||||
on_failure("request_failed", "Your request did not arrive, check your network connection")
|
||||
on_error("request_failed", "Your request did not arrive, check your network connection")
|
||||
} else {
|
||||
on_failure(xhr.responseText, xhr.responseText)
|
||||
on_error(xhr.responseText, xhr.responseText)
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { fade } from "svelte/transition";
|
||||
import { fade } from "svelte/transition";
|
||||
import ProgressBar from "../ProgressBar.svelte";
|
||||
import { upload_file } from "./UploadFunc";
|
||||
|
||||
|
Reference in New Issue
Block a user