234 lines
5.0 KiB
Svelte
234 lines
5.0 KiB
Svelte
<script lang="ts" module>
|
|
export type UploadJob = {
|
|
task_id: number,
|
|
file: File,
|
|
path: string,
|
|
component: UploadProgress,
|
|
status: string,
|
|
total_size: number,
|
|
loaded_size: number,
|
|
};
|
|
</script>
|
|
<script lang="ts">
|
|
import { tick } from "svelte";
|
|
import UploadProgress from "./UploadProgress.svelte";
|
|
import type { FSNavigator } from "filesystem/FSNavigator";
|
|
|
|
let { nav }: {
|
|
nav: FSNavigator;
|
|
} = $props();
|
|
|
|
let file_input_field: HTMLInputElement = $state()
|
|
let file_input_change = (e: Event) => {
|
|
// Start uploading the files async
|
|
upload_files((e.target as HTMLInputElement).files)
|
|
|
|
// This resets the file input field
|
|
file_input_field.nodeValue = ""
|
|
}
|
|
export const pick_files = () => {
|
|
file_input_field.click()
|
|
}
|
|
|
|
let visible = $state(false)
|
|
let upload_queue: UploadJob[] = $state([]);
|
|
let task_id_counter = 0
|
|
|
|
export const upload_files = async (files: File[]|FileList) => {
|
|
if (files.length === 0) {
|
|
return
|
|
} else if (nav.base.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: File) => {
|
|
if (nav.base.type !== "dir") {
|
|
alert("Can only upload to directory")
|
|
return
|
|
} else if (file.type === "" && file.size === 0) {
|
|
return
|
|
}
|
|
|
|
let path = nav.base.path + "/"
|
|
if (file.webkitRelativePath) {
|
|
path += file.webkitRelativePath
|
|
} else {
|
|
path += file.name
|
|
}
|
|
|
|
upload_queue.push({
|
|
task_id: task_id_counter,
|
|
file: file,
|
|
path: path,
|
|
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 && status !== "uploading") {
|
|
status = "uploading"
|
|
visible = true
|
|
await tick()
|
|
await start_upload()
|
|
}
|
|
}
|
|
|
|
const min_uploads = 2
|
|
const max_uploads = 20
|
|
|
|
let active_uploads = 0
|
|
let status = $state("idle")
|
|
|
|
const start_upload = async () => {
|
|
active_uploads = 0
|
|
let uploading_size = 0
|
|
for (let i = 0; i < upload_queue.length; i++) {
|
|
if (upload_queue[i]) {
|
|
// If this file is queued, start the upload
|
|
if (upload_queue[i].status === "queued") {
|
|
upload_queue[i].component.start()
|
|
upload_queue[i].status = "uploading"
|
|
}
|
|
|
|
// If this file is already uploading (or just started), count it
|
|
if (upload_queue[i].status === "uploading") {
|
|
uploading_size += upload_queue[i].total_size
|
|
active_uploads++
|
|
}
|
|
|
|
// If the size threshold or the concurrent upload limit is reached
|
|
// we break the loop. The system tries to keep an upload queue of
|
|
// 100 MB and a minimum of two concurrent uploads.
|
|
if (
|
|
(uploading_size >= 100e6 && active_uploads >= min_uploads) ||
|
|
active_uploads >= max_uploads
|
|
) {
|
|
console.debug("Current uploads", active_uploads, "uploads size", uploading_size)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (active_uploads === 0) {
|
|
status = "finished"
|
|
nav.reload()
|
|
|
|
// Empty the queue to free any references to lingering components
|
|
upload_queue = []
|
|
|
|
// In ten seconds we close the popup
|
|
setTimeout(() => {
|
|
if (status === "finished") {
|
|
visible = false
|
|
}
|
|
}, 10000)
|
|
} else {
|
|
status = "uploading"
|
|
}
|
|
}
|
|
|
|
const finish_upload = () => {
|
|
// Update the queue so the status updates are properly rendered
|
|
upload_queue = upload_queue
|
|
start_upload()
|
|
}
|
|
|
|
const leave_confirmation = (e: BeforeUnloadEvent) => {
|
|
if (status === "uploading") {
|
|
e.preventDefault()
|
|
return "If you close this page your files will stop uploading. Do you want to continue?"
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onbeforeunload={leave_confirmation} />
|
|
|
|
<input
|
|
bind:this={file_input_field}
|
|
onchange={file_input_change}
|
|
class="upload_input" type="file" name="file" multiple
|
|
/>
|
|
|
|
{#if visible}
|
|
<div class="upload_widget">
|
|
<div class="header">
|
|
{#if status === "idle"}
|
|
Waiting for files
|
|
{:else if status === "uploading"}
|
|
Uploading files...
|
|
{:else if status === "finished"}
|
|
Done
|
|
{/if}
|
|
</div>
|
|
<div class="body">
|
|
{#each upload_queue as job, i}
|
|
{#if job.status !== "finished"}
|
|
<UploadProgress
|
|
bind:this={upload_queue[i].component}
|
|
bind:job={upload_queue[i]}
|
|
finish={finish_upload}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.upload_input {
|
|
visibility: hidden;
|
|
position: fixed;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.upload_widget {
|
|
position: fixed;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: auto;
|
|
min-width: 400px;
|
|
max-width: 80%;
|
|
height: auto;
|
|
max-height: 50%;
|
|
right: 20px;
|
|
bottom: 20px;
|
|
border-radius: 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>
|