Files
fnx_web/svelte/src/filesystem/upload_widget/FSUploadWidget.svelte

224 lines
4.9 KiB
Svelte
Raw Normal View History

2025-10-13 16:05:50 +02:00
<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">
2024-08-30 17:03:43 +02:00
import { tick } from "svelte";
2023-05-27 15:50:44 +02:00
import UploadProgress from "./UploadProgress.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
2023-05-27 15:50:44 +02:00
2025-10-13 16:05:50 +02:00
let { nav }: {
nav: FSNavigator;
} = $props();
2023-05-27 15:50:44 +02:00
2025-10-13 16:05:50 +02:00
let file_input_field: HTMLInputElement = $state()
let file_input_change = (e: Event) => {
2023-05-27 15:50:44 +02:00
// Start uploading the files async
upload_files((e.target as HTMLInputElement).files)
2023-05-27 15:50:44 +02:00
// This resets the file input field
file_input_field.nodeValue = ""
}
export const pick_files = () => {
file_input_field.click()
}
2025-10-13 16:05:50 +02:00
let visible = $state(false)
let upload_queue: UploadJob[] = $state([]);
2023-05-27 15:50:44 +02:00
let task_id_counter = 0
export const upload_files = async (files: File[]|FileList) => {
2023-05-27 15:50:44 +02:00
if (files.length === 0) {
return
} else if (nav.base.type !== "dir") {
2023-05-27 15:50:44 +02:00
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") {
2023-05-27 15:50:44 +02:00
alert("Can only upload to directory")
return
2023-05-29 22:15:46 +02:00
} else if (file.type === "" && file.size === 0) {
2023-05-27 15:50:44 +02:00
return
}
let path = nav.base.path + "/"
2023-05-29 18:01:23 +02:00
if (file.webkitRelativePath) {
path += file.webkitRelativePath
} else {
path += file.name
}
2023-05-27 15:50:44 +02:00
upload_queue.push({
task_id: task_id_counter,
file: file,
2023-05-29 18:01:23 +02:00
path: path,
2023-05-27 15:50:44 +02:00
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
2025-10-13 16:05:50 +02:00
if (active_uploads === 0 && status !== "uploading") {
status = "uploading"
2023-05-27 15:50:44 +02:00
visible = true
await tick()
await start_upload()
}
}
let active_uploads = 0
2025-10-13 16:05:50 +02:00
let status = $state("idle")
2023-05-27 15:50:44 +02:00
const start_upload = async () => {
2025-10-09 15:48:23 +02:00
active_uploads = 0
let uploading_size = 0
for (let i = 0; i < upload_queue.length; i++) {
2023-05-27 15:50:44 +02:00
if (upload_queue[i]) {
2025-10-09 15:48:23 +02:00
// If this file is queued, start the upload
2023-05-27 15:50:44 +02:00
if (upload_queue[i].status === "queued") {
upload_queue[i].component.start()
upload_queue[i].status = "uploading"
}
2025-10-09 15:48:23 +02:00
// 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 >= 2) || active_uploads >= 10) {
console.debug("Current uploads", active_uploads, "uploads size", uploading_size)
break
}
2023-05-27 15:50:44 +02:00
}
}
if (active_uploads === 0) {
2025-10-13 16:05:50 +02:00
status = "finished"
nav.reload()
2023-05-27 15:50:44 +02:00
// Empty the queue to free any references to lingering components
2023-05-27 15:50:44 +02:00
upload_queue = []
// In ten seconds we close the popup
2023-05-27 15:50:44 +02:00
setTimeout(() => {
2025-10-13 16:05:50 +02:00
if (status === "finished") {
2023-05-27 15:50:44 +02:00
visible = false
}
}, 10000)
} else {
2025-10-13 16:05:50 +02:00
status = "uploading"
2023-05-27 15:50:44 +02:00
}
}
2024-08-30 17:03:43 +02:00
const finish_upload = () => {
// Update the queue so the status updates are properly rendered
2023-05-27 15:50:44 +02:00
upload_queue = upload_queue
start_upload()
}
const leave_confirmation = (e: BeforeUnloadEvent) => {
2025-10-13 16:05:50 +02:00
if (status === "uploading") {
2023-05-27 15:50:44 +02:00
e.preventDefault()
2024-08-30 17:03:43 +02:00
return "If you close this page your files will stop uploading. Do you want to continue?"
2023-05-27 15:50:44 +02:00
} else {
return null
}
}
</script>
2025-10-13 16:05:50 +02:00
<svelte:window onbeforeunload={leave_confirmation} />
2023-05-27 15:50:44 +02:00
<input
bind:this={file_input_field}
2025-10-13 16:05:50 +02:00
onchange={file_input_change}
2024-08-30 17:03:43 +02:00
class="upload_input" type="file" name="file" multiple
2023-05-27 15:50:44 +02:00
/>
{#if visible}
2025-10-09 15:48:23 +02:00
<div class="upload_widget">
2023-05-27 15:50:44 +02:00
<div class="header">
2025-10-13 16:05:50 +02:00
{#if status === "idle"}
2023-05-27 15:50:44 +02:00
Waiting for files
2025-10-13 16:05:50 +02:00
{:else if status === "uploading"}
2023-05-27 15:50:44 +02:00
Uploading files...
2025-10-13 16:05:50 +02:00
{:else if status === "finished"}
2023-05-27 15:50:44 +02:00
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: fixed;
width: 0;
height: 0;
}
.upload_widget {
position: fixed;
2023-05-27 15:50:44 +02:00
display: flex;
flex-direction: column;
2025-10-09 15:48:23 +02:00
width: auto;
min-width: 400px;
2023-05-27 15:50:44 +02:00
max-width: 80%;
height: auto;
max-height: 50%;
right: 20px;
bottom: 20px;
2023-11-16 14:48:15 +01:00
border-radius: 8px;
2023-05-27 15:50:44 +02:00
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>