Add file upload widget to file manager
This commit is contained in:
@@ -26,6 +26,7 @@
|
|||||||
window.api_endpoint = '{{.APIEndpoint}}';
|
window.api_endpoint = '{{.APIEndpoint}}';
|
||||||
window.viewer_data = {{.Other}};
|
window.viewer_data = {{.Other}};
|
||||||
window.user_authenticated = {{.Authenticated}};
|
window.user_authenticated = {{.Authenticated}};
|
||||||
|
window.user = {{.User}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel='stylesheet' href='/res/svelte/file_viewer.css?v{{cacheID}}'>
|
<link rel='stylesheet' href='/res/svelte/file_viewer.css?v{{cacheID}}'>
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.api_endpoint = '{{.APIEndpoint}}';
|
window.api_endpoint = '{{.APIEndpoint}}';
|
||||||
|
window.user = {{.User}};
|
||||||
</script>
|
</script>
|
||||||
<link rel='stylesheet' href='/res/svelte/user_file_manager.css?v{{cacheID}}'>
|
<link rel='stylesheet' href='/res/svelte/user_file_manager.css?v{{cacheID}}'>
|
||||||
<script defer src='/res/svelte/user_file_manager.js?v{{cacheID}}'></script>
|
<script defer src='/res/svelte/user_file_manager.js?v{{cacheID}}'></script>
|
||||||
|
@@ -331,7 +331,7 @@ const keydown = (e) => {
|
|||||||
on:keydown={keydown}
|
on:keydown={keydown}
|
||||||
on:beforeunload={leave_confirmation} />
|
on:beforeunload={leave_confirmation} />
|
||||||
|
|
||||||
<Konami></Konami>
|
<Konami/>
|
||||||
|
|
||||||
<!-- If the user is logged in and has used more than 50% of their storage space we will show a progress bar -->
|
<!-- If the user is logged in and has used more than 50% of their storage space we will show a progress bar -->
|
||||||
{#if window.user.username !== "" && window.user.storage_space_used/window.user.subscription.storage_space > 0.5}
|
{#if window.user.username !== "" && window.user.storage_space_used/window.user.subscription.storage_space > 0.5}
|
||||||
|
@@ -3,6 +3,7 @@ import { onMount } from "svelte";
|
|||||||
import { formatDataVolume } from "../util/Formatting.svelte";
|
import { formatDataVolume } from "../util/Formatting.svelte";
|
||||||
import Modal from "../util/Modal.svelte";
|
import Modal from "../util/Modal.svelte";
|
||||||
import Spinner from "../util/Spinner.svelte";
|
import Spinner from "../util/Spinner.svelte";
|
||||||
|
import UploadWidget from "../util/upload_widget/UploadWidget.svelte";
|
||||||
import DirectoryElement from "./DirectoryElement.svelte"
|
import DirectoryElement from "./DirectoryElement.svelte"
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
@@ -12,6 +13,7 @@ let directoryElement
|
|||||||
let downloadFrame
|
let downloadFrame
|
||||||
let help_modal
|
let help_modal
|
||||||
let help_modal_visible = false
|
let help_modal_visible = false
|
||||||
|
let upload_widget
|
||||||
|
|
||||||
let getUserFiles = () => {
|
let getUserFiles = () => {
|
||||||
loading = true
|
loading = true
|
||||||
@@ -251,28 +253,40 @@ onMount(() => {
|
|||||||
initialized = true
|
initialized = true
|
||||||
hashChange()
|
hashChange()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={keydown} on:hashchange={hashChange} />
|
<svelte:window on:keydown={keydown} on:hashchange={hashChange} />
|
||||||
|
|
||||||
|
<UploadWidget bind:this={upload_widget} drop_upload on:uploads_finished={hashChange}/>
|
||||||
|
|
||||||
<div id="file_manager" class="file_manager">
|
<div id="file_manager" class="file_manager">
|
||||||
<div id="nav_bar" class="nav_bar">
|
<div id="nav_bar" class="nav_bar">
|
||||||
<button id="btn_menu" onclick="toggleMenu()"><i class="icon">menu</i></button>
|
<button id="btn_menu" onclick="toggleMenu()"><i class="icon">menu</i></button>
|
||||||
<button on:click={toggleSelecting} id="btn_select" class:button_highlight={selecting}>
|
<button on:click={toggleSelecting} id="btn_select" class:button_highlight={selecting}>
|
||||||
<i class="icon">select_all</i> Select
|
<i class="icon">select_all</i> Select
|
||||||
</button>
|
</button>
|
||||||
<input bind:this={inputSearch} on:keyup={searchHandler} id="input_search" class="input_search" type="text" placeholder="press / to search"/>
|
<input
|
||||||
<button on:click={() => help_modal.toggle()} class:button_highlight={help_modal_visible}>
|
bind:this={inputSearch}
|
||||||
<i class="icon">info</i>
|
on:keyup={searchHandler}
|
||||||
</button>
|
id="input_search"
|
||||||
<button on:click={hashChange} id="btn_reload">
|
class="input_search"
|
||||||
|
type="text"
|
||||||
|
placeholder="press / to search"
|
||||||
|
/>
|
||||||
|
{#if contentType === "files"}
|
||||||
|
<button on:click={upload_widget.pick_files} id="btn_upload" title="Upload files">
|
||||||
|
<i class="icon">cloud_upload</i>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button on:click={hashChange} id="btn_reload" title="Refresh file list">
|
||||||
<i class="icon">refresh</i>
|
<i class="icon">refresh</i>
|
||||||
</button>
|
</button>
|
||||||
|
<button on:click={() => help_modal.toggle()} class:button_highlight={help_modal_visible} title="Help">
|
||||||
|
<i class="icon">info</i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if selecting}
|
{#if selecting}
|
||||||
<div class="nav_bar">
|
<div class="nav_bar">
|
||||||
<!-- Buttons to make a list and bulk delete files will show up here soon. Stay tuned ;-) -->
|
|
||||||
{#if contentType === "files"}
|
{#if contentType === "files"}
|
||||||
<button on:click={createList}><i class="icon">list</i> Make list</button>
|
<button on:click={createList}><i class="icon">list</i> Make list</button>
|
||||||
<button on:click={downloadFiles}><i class="icon">download</i> Download</button>
|
<button on:click={downloadFiles}><i class="icon">download</i> Download</button>
|
||||||
|
53
svelte/src/util/upload_widget/DropUpload.svelte
Normal file
53
svelte/src/util/upload_widget/DropUpload.svelte
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
let dragging = false
|
||||||
|
|
||||||
|
const drop = (e) => {
|
||||||
|
dragging = false;
|
||||||
|
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
dispatch("upload", e.dataTransfer.files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const paste = (e) => {
|
||||||
|
if (e.clipboardData.files[0]) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dispatch("upload", e.dataTransfer.files)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
height: auto;
|
||||||
|
margin: auto;
|
||||||
|
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>
|
68
svelte/src/util/upload_widget/UploadFunc.js
Normal file
68
svelte/src/util/upload_widget/UploadFunc.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// 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
|
||||||
|
// code and an error message
|
||||||
|
export const upload_file = (file, name, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("PUT", window.api_endpoint + "/file/" + encodeURIComponent(name), 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_failure(resp.value, resp.message)
|
||||||
|
} else if (xhr.status === 0) {
|
||||||
|
on_failure("request_failed", "Your request did not arrive, check your network connection")
|
||||||
|
} else {
|
||||||
|
on_failure(xhr.responseText, xhr.responseText)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(file);
|
||||||
|
}
|
66
svelte/src/util/upload_widget/UploadProgress.svelte
Normal file
66
svelte/src/util/upload_widget/UploadProgress.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import ProgressBar from "../ProgressBar.svelte";
|
||||||
|
import { upload_file } from "./UploadFunc";
|
||||||
|
|
||||||
|
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.name,
|
||||||
|
(prog_loaded, prog_total) => {
|
||||||
|
loaded = prog_loaded
|
||||||
|
total = prog_total
|
||||||
|
},
|
||||||
|
async (id) => {
|
||||||
|
console.log("finsished", id)
|
||||||
|
job.status = "finished"
|
||||||
|
job.id = id
|
||||||
|
dispatch("finished")
|
||||||
|
},
|
||||||
|
(code, message) => {
|
||||||
|
console.log("error", code, message)
|
||||||
|
error_code = code
|
||||||
|
error_message = message
|
||||||
|
job.status = "error"
|
||||||
|
dispatch("finished")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
job.status = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="upload_progress" class:error={job.status === "error"}>
|
||||||
|
{job.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>
|
170
svelte/src/util/upload_widget/UploadWidget.svelte
Normal file
170
svelte/src/util/upload_widget/UploadWidget.svelte
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher, tick } from "svelte";
|
||||||
|
import DropUpload from "./DropUpload.svelte";
|
||||||
|
import UploadProgress from "./UploadProgress.svelte";
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files to the queue
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
if (files[i].type === "" && files[i].size === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_queue.push({
|
||||||
|
task_id: task_id_counter,
|
||||||
|
file: files[i],
|
||||||
|
name: files[i].name,
|
||||||
|
component: null,
|
||||||
|
id: "",
|
||||||
|
status: "queued",
|
||||||
|
total_size: files[i].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
|
||||||
|
visible = true
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
start_upload()
|
||||||
|
}
|
||||||
|
|
||||||
|
let active_uploads = 0
|
||||||
|
let state = "idle"
|
||||||
|
|
||||||
|
const start_upload = () => {
|
||||||
|
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 = []
|
||||||
|
visible = false
|
||||||
|
} else {
|
||||||
|
state = "uploading"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const finish_upload = (e) => {
|
||||||
|
active_uploads--
|
||||||
|
upload_queue = upload_queue
|
||||||
|
start_upload()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={file_input_field}
|
||||||
|
on:change={file_input_change}
|
||||||
|
class="upload_input" type="file" name="file" multiple="multiple"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if drop_upload}
|
||||||
|
<DropUpload on:upload={e => upload_files(e.detail)}/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="upload_widget">
|
||||||
|
<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}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload_input {
|
||||||
|
visibility: hidden;
|
||||||
|
position: static;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.upload_widget {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
border-radius: 20px 20px 8px 8px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 1px 1px 10px -2px 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);
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user