Centralize drag-and-drop upload code

This commit is contained in:
2024-09-10 18:51:13 +02:00
parent 75b6c304b2
commit c5ddc20ce2
13 changed files with 219 additions and 235 deletions

View File

@@ -7,14 +7,14 @@ import Breadcrumbs from './Breadcrumbs.svelte';
import DetailsWindow from './DetailsWindow.svelte'; import DetailsWindow from './DetailsWindow.svelte';
import FilePreview from './viewers/FilePreview.svelte'; import FilePreview from './viewers/FilePreview.svelte';
import SearchView from './SearchView.svelte'; import SearchView from './SearchView.svelte';
import UploadWidget from './upload_widget/UploadWidget.svelte'; import FSUploadWidget from './upload_widget/FSUploadWidget.svelte';
import { fs_path_url } from './FilesystemAPI'; import { fs_path_url } from './FilesystemAPI';
import { branding_from_path } from './edit_window/Branding.js'
import Menu from './Menu.svelte'; import Menu from './Menu.svelte';
import { FSNavigator } from "./FSNavigator" import { FSNavigator } from "./FSNavigator"
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import TransferLimit from '../file_viewer/TransferLimit.svelte'; import TransferLimit from '../file_viewer/TransferLimit.svelte';
import { stats } from "src/util/StatsSocket.js" import { stats } from "src/util/StatsSocket.js"
import { css_from_path } from './edit_window/Branding';
let file_viewer let file_viewer
let file_preview let file_preview
@@ -41,7 +41,7 @@ onMount(() => {
} }
// Custom CSS rules for the whole viewer // Custom CSS rules for the whole viewer
document.documentElement.style = branding_from_path(nav.path) document.documentElement.style = css_from_path(nav.path)
loading.set(false) loading.set(false)
}) })
@@ -169,10 +169,10 @@ const search = async () => {
<FilePreview <FilePreview
bind:this={file_preview} bind:this={file_preview}
nav={nav} nav={nav}
upload_widget={upload_widget}
edit_window={edit_window} edit_window={edit_window}
on:open_sibling={e => nav.open_sibling(e.detail)} on:open_sibling={e => nav.open_sibling(e.detail)}
on:download={download} on:download={download}
on:upload_picker={() => upload_widget.pick_files()}
/> />
{:else if view === "search"} {:else if view === "search"}
<SearchView nav={nav} on:done={() => {view = "file"}} /> <SearchView nav={nav} on:done={() => {view = "file"}} />
@@ -206,7 +206,9 @@ const search = async () => {
<EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} /> <EditWindow nav={nav} bind:this={edit_window} bind:visible={edit_visible} />
<UploadWidget nav={nav} bind:this={upload_widget} drop_upload /> <!-- This one is included at the highest level so uploads can keep running
even when the user navigates to a different directory -->
<FSUploadWidget nav={nav} bind:this={upload_widget} />
<LoadingIndicator loading={$loading}/> <LoadingIndicator loading={$loading}/>
</div> </div>

View File

@@ -10,7 +10,7 @@ export const branding_from_path = path => {
add_styles(style, node.properties) add_styles(style, node.properties)
} }
last_generated_style = style last_generated_style = style
return gen_css(style) return style
} }
// The last style which was generated is cached, when we don't have a complete // The last style which was generated is cached, when we don't have a complete
@@ -21,6 +21,10 @@ export const branding_from_node = node => {
return gen_css(last_generated_style) return gen_css(last_generated_style)
} }
export const css_from_path = path => {
return gen_css(branding_from_path(path))
}
const gen_css = style => { const gen_css = style => {
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';'); return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
} }

View File

@@ -1,15 +1,16 @@
<script> <script>
import { fs_delete_all, fs_rename } from './../FilesystemAPI.ts' import { fs_delete_all, fs_rename } from './../FilesystemAPI.ts'
import { createEventDispatcher, onMount } from 'svelte' import { onMount } from 'svelte'
import CreateDirectory from './CreateDirectory.svelte' import CreateDirectory from './CreateDirectory.svelte'
import ListView from './ListView.svelte' import ListView from './ListView.svelte'
import GalleryView from './GalleryView.svelte' import GalleryView from './GalleryView.svelte'
import Button from '../../layout/Button.svelte'; import Button from '../../layout/Button.svelte';
import FileImporter from './FileImporter.svelte'; import FileImporter from './FileImporter.svelte';
import { formatDate } from '../../util/Formatting.svelte'; import { formatDate } from '../../util/Formatting.svelte';
let dispatch = createEventDispatcher() import { drop_target } from "src/util/DropTarget.ts"
export let nav export let nav
export let upload_widget
export let edit_window export let edit_window
export let directory_view = "" export let directory_view = ""
let large_icons = false let large_icons = false
@@ -256,7 +257,13 @@ onMount(() => {
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} /> <svelte:window on:keydown={detect_shift} on:keyup={detect_shift} />
<div class="container"> <div
class="container"
use:drop_target={{
upload: (files) => upload_widget.upload_files(files),
shadow: "var(--highlight_color) 0 0 10px 2px inset",
}}
>
<div class="width_container"> <div class="width_container">
{#if mode === "viewing"} {#if mode === "viewing"}
<div class="toolbar"> <div class="toolbar">
@@ -296,7 +303,7 @@ onMount(() => {
<div class="toolbar_spacer"></div> <div class="toolbar_spacer"></div>
{#if $nav.permissions.update} {#if $nav.permissions.update}
<button on:click={() => dispatch("upload_picker")} title="Upload files to this directory"> <button on:click={() => upload_widget.pick_files()} title="Upload files to this directory">
<i class="icon">cloud_upload</i> <i class="icon">cloud_upload</i>
</button> </button>

View File

@@ -1,105 +0,0 @@
<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 can_upload = e => {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
return true
}
for (let i = 0; i < e.dataTransfer.items.length; i++) {
if (e.dataTransfer.items[i].kind === "file") {
return true
}
}
return false
}
const dragover = e => {
if (can_upload(e)) {
e.stopPropagation();
e.preventDefault();
dragging = true
console.log(e)
}
}
const dragleave = e => {
dragging = false
}
const drop = async e => {
dragging = false;
if (can_upload(e)) {
e.stopPropagation();
e.preventDefault();
} else {
return
}
// 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) {
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={dragover}
on:dragenter={dragover}
on:dragleave={dragleave}
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>

View File

@@ -1,7 +1,6 @@
<script> <script>
import { tick } from "svelte"; import { tick } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import DropUpload from "./DropUpload.svelte";
import UploadProgress from "./UploadProgress.svelte"; import UploadProgress from "./UploadProgress.svelte";
export let nav export let nav
@@ -18,7 +17,6 @@ export const pick_files = () => {
file_input_field.click() file_input_field.click()
} }
export let drop_upload = false
let visible = false let visible = false
let upload_queue = []; let upload_queue = [];
let task_id_counter = 0 let task_id_counter = 0
@@ -161,10 +159,6 @@ const leave_confirmation = (e) => {
</div> </div>
{/if} {/if}
{#if drop_upload}
<DropUpload on:upload_files={e => upload_files(e.detail)} on:upload_file={e => upload_file(e.detail)}/>
{/if}
<style> <style>
.upload_input { .upload_input {
visibility: hidden; visibility: hidden;

View File

@@ -78,15 +78,19 @@ const cancel = () => {
.prog { .prog {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%;
overflow: hidden;
} }
.bar { .bar {
flex: 1 1 auto; flex: 1 1 auto;
padding: 2px 4px 1px 4px; padding: 2px 4px 1px 4px;
margin: 4px; margin: 4px;
border-radius: 4px; border-radius: 4px;
overflow: hidden;
line-break: anywhere;
} }
.cancel { .cancel {
flex: 0 0 auto; flex: 0 0 content;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; /* Stop stretching the button */ align-items: center; /* Stop stretching the button */

View File

@@ -16,6 +16,7 @@ import { stats } from "src/util/StatsSocket.js"
import SlowDown from "src/layout/SlowDown.svelte"; import SlowDown from "src/layout/SlowDown.svelte";
export let nav export let nav
export let upload_widget
export let edit_window export let edit_window
let viewer let viewer
@@ -59,7 +60,7 @@ export const seek = delta => {
<Spinner></Spinner> <Spinner></Spinner>
</div> </div>
{:else if viewer_type === "dir"} {:else if viewer_type === "dir"}
<FileManager nav={nav} edit_window={edit_window} on:upload_picker> <FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>
</FileManager> </FileManager>
{:else if $nav.context.premium_transfer === false && $stats.limits.transfer_limit_used > $stats.limits.transfer_limit} {:else if $nav.context.premium_transfer === false && $stats.limits.transfer_limit_used > $stats.limits.transfer_limit}

View File

@@ -2,16 +2,26 @@
import Login from "../login/Login.svelte"; import Login from "../login/Login.svelte";
import Register from "../login/Register.svelte"; import Register from "../login/Register.svelte";
import UploadWidget from "./UploadWidget.svelte"; import UploadWidget from "./UploadWidget.svelte";
import { drop_target } from "src/util/DropTarget.ts"
const finish_login = async e => { const finish_login = async e => {
location.reload() location.reload()
} }
let upload_widget
let page = "login" let page = "login"
</script> </script>
{#if window.user && window.user.username && window.user.username !== ""} {#if window.user && window.user.username && window.user.username !== ""}
<UploadWidget/> <div
class="drop_target"
use:drop_target={{
upload: (files) => upload_widget.upload_files(files),
shadow: "var(--highlight_color) 0 0 10px 2px inset",
}}
>
<UploadWidget bind:this={upload_widget}/>
</div>
{:else} {:else}
<section> <section>
<p> <p>
@@ -51,6 +61,9 @@ let page = "login"
{/if} {/if}
<style> <style>
.drop_target {
border-radius: 8px;
}
.tab_bar > button { .tab_bar > button {
width: 40%; width: 40%;
max-width: 10em; max-width: 10em;

View File

@@ -21,15 +21,6 @@ const file_input_change = (event) => {
// This resets the file input field // This resets the file input field
file_input_field.nodeValue = "" file_input_field.nodeValue = ""
} }
let dragging = false
const drop = (e) => {
dragging = false;
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
e.preventDefault()
e.stopPropagation()
upload_files(e.dataTransfer.files)
}
}
const paste = (e) => { const paste = (e) => {
if (e.clipboardData.files[0]) { if (e.clipboardData.files[0]) {
e.preventDefault(); e.preventDefault();
@@ -43,7 +34,7 @@ let upload_queue = []
let state = "idle" // idle, uploading, finished let state = "idle" // idle, uploading, finished
let upload_stats let upload_stats
const upload_files = async (files) => { export const upload_files = async (files) => {
if (files.length === 0) { if (files.length === 0) {
return return
} }
@@ -279,14 +270,7 @@ const keydown = (e) => {
</script> </script>
<svelte:window <svelte:window on:paste={paste} on:keydown={keydown} on:beforeunload={leave_confirmation} />
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
on:drop={drop}
on:paste={paste}
on:keydown={keydown}
on:beforeunload={leave_confirmation} />
<Konami/> <Konami/>
@@ -327,11 +311,6 @@ const keydown = (e) => {
<br/> <br/>
<UploadStats bind:this={upload_stats} upload_queue={upload_queue}/> <UploadStats bind:this={upload_stats} upload_queue={upload_queue}/>
<div id="file_drop_highlight" class="highlight_green" class:hide={!dragging}>
Gimme gimme gimme!<br/>
Drop your files to upload them
</div>
</section> </section>
{#each upload_queue as file} {#each upload_queue as file}

View File

@@ -4,7 +4,8 @@ import { FSNavigator } from "src/filesystem/FSNavigator.ts"
import { fs_encode_path, fs_node_icon } from "src/filesystem/FilesystemAPI.ts"; import { fs_encode_path, fs_node_icon } from "src/filesystem/FilesystemAPI.ts";
import Button from "src/layout/Button.svelte"; import Button from "src/layout/Button.svelte";
import CreateDirectory from "src/filesystem/filemanager/CreateDirectory.svelte"; import CreateDirectory from "src/filesystem/filemanager/CreateDirectory.svelte";
import UploadWidget from "src/filesystem/upload_widget/UploadWidget.svelte"; import FSUploadWidget from "src/filesystem/upload_widget/FSUploadWidget.svelte";
import { drop_target } from "src/util/DropTarget.ts"
const nav = new FSNavigator(false) const nav = new FSNavigator(false)
let upload_widget let upload_widget
@@ -14,6 +15,7 @@ var creating_dir = false
onMount(() => nav.navigate("/me", false)) onMount(() => nav.navigate("/me", false))
</script> </script>
<div class="wrapper" use:drop_target={{upload: (files) => upload_widget.upload_files(files)}}>
<div class="toolbar"> <div class="toolbar">
{#if $nav.permissions.update} {#if $nav.permissions.update}
<Button <Button
@@ -72,10 +74,14 @@ onMount(() => nav.navigate("/me", false))
</a> </a>
{/each} {/each}
</div> </div>
</div>
<UploadWidget nav={nav} bind:this={upload_widget} drop_upload /> <FSUploadWidget nav={nav} bind:this={upload_widget} />
<style> <style>
.wrapper {
border-radius: 4px;
}
.toolbar { .toolbar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -1,9 +1,11 @@
<script> <script>
import UploadLib from "./UploadLib.svelte"; import UploadLib from "./UploadLib.svelte";
import { drop_target } from "src/util/DropTarget.ts"
let upload_widget let upload_widget
</script> </script>
<div class="wrapper" use:drop_target={{upload: (files) => upload_widget.upload_files(files)}}>
<div class="upload_buttons"> <div class="upload_buttons">
<button on:click={() => upload_widget.pick_files() } class="big_button button_highlight"> <button on:click={() => upload_widget.pick_files() } class="big_button button_highlight">
<i class="icon small">cloud_upload</i> <i class="icon small">cloud_upload</i>
@@ -18,8 +20,12 @@ let upload_widget
<div class="center"> <div class="center">
<UploadLib bind:this={upload_widget}/> <UploadLib bind:this={upload_widget}/>
</div> </div>
</div>
<style> <style>
.wrapper {
border-radius: 4px;
}
.center { .center {
text-align: center; text-align: center;
} }

View File

@@ -17,15 +17,6 @@ const file_input_change = (event) => {
// This resets the file input field // This resets the file input field
file_input_field.nodeValue = "" file_input_field.nodeValue = ""
} }
let dragging = false
const drop = (e) => {
dragging = false;
if (e.dataTransfer && e.dataTransfer.items.length > 0) {
e.preventDefault()
e.stopPropagation()
upload_files(e.dataTransfer.files)
}
}
const paste = (e) => { const paste = (e) => {
if (e.clipboardData.files[0]) { if (e.clipboardData.files[0]) {
e.preventDefault(); e.preventDefault();
@@ -39,7 +30,7 @@ let upload_queue = []
let state = "idle" // idle, uploading, finished let state = "idle" // idle, uploading, finished
let upload_stats let upload_stats
const upload_files = async (files) => { export const upload_files = async (files) => {
if (files.length === 0) { if (files.length === 0) {
return return
} }
@@ -88,7 +79,6 @@ const start_upload = () => {
if (active_uploads === 0 && finished_count != 0) { if (active_uploads === 0 && finished_count != 0) {
state = "finished" state = "finished"
upload_stats.finish() upload_stats.finish()
// uploads_finished()
} else { } else {
state = "uploading" state = "uploading"
upload_stats.start() upload_stats.start()
@@ -180,17 +170,9 @@ const keydown = (e) => {
case "m": share_tumblr(); break case "m": share_tumblr(); break
} }
} }
</script> </script>
<svelte:window <svelte:window on:paste={paste} on:keydown={keydown} on:beforeunload={leave_confirmation} />
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
on:drop={drop}
on:paste={paste}
on:keydown={keydown}
on:beforeunload={leave_confirmation} />
<input bind:this={file_input_field} on:change={file_input_change} type="file" name="file" multiple="multiple" class="hide"/> <input bind:this={file_input_field} on:change={file_input_change} type="file" name="file" multiple="multiple" class="hide"/>
@@ -209,10 +191,6 @@ const keydown = (e) => {
</div> </div>
{/if} {/if}
<div id="file_drop_highlight" class="highlight_green" class:hide={!dragging}>
Drop your files to upload them
</div>
{#each upload_queue as file} {#each upload_queue as file}
<UploadProgressBar bind:this={file.component} job={file}></UploadProgressBar> <UploadProgressBar bind:this={file.component} job={file}></UploadProgressBar>
{/each} {/each}

View File

@@ -0,0 +1,95 @@
export const drop_target = (
node: HTMLElement,
args: {
upload: (files: File[]) => void,
shadow: string | undefined,
},
) => {
const can_upload = (e: DragEvent) => {
// Check for files
if (
e.dataTransfer &&
e.dataTransfer.files &&
e.dataTransfer.files.length > 0
) {
return true
}
if (e.dataTransfer && e.dataTransfer.items) {
for (let i = 0; i < e.dataTransfer.items.length; i++) {
if (e.dataTransfer.items[i].kind === "file") {
return true
}
}
}
return false
}
const dragover = (e: DragEvent) => {
if (can_upload(e)) {
e.stopPropagation();
e.preventDefault();
if (args.shadow === undefined) {
node.style.boxShadow = "var(--highlight_color) 0 0 2px 2px"
} else {
node.style.boxShadow = args.shadow
}
}
}
const dragleave = (e: DragEvent) => {
node.style.boxShadow = ""
}
const drop = async (e: DragEvent) => {
node.style.boxShadow = ""
if (can_upload(e)) {
e.stopPropagation();
e.preventDefault();
} else {
return
}
// 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++) {
const entry: FileSystemEntry | null = e.dataTransfer.items[i].webkitGetAsEntry();
if (entry !== null) {
read_dir_recursive(entry);
}
}
} else if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const files: File[] = []
for (let i = 0; i < e.dataTransfer.files.length; i++) {
files.push(e.dataTransfer.files[i])
}
args.upload(files)
}
}
const read_dir_recursive = (item: FileSystemEntry) => {
if (item.isDirectory) {
(item as FileSystemDirectoryEntry).createReader().readEntries(entries => {
entries.forEach(entry => {
read_dir_recursive(entry);
});
});
} else {
(item as FileSystemFileEntry).file(file => {
args.upload([file])
});
}
}
node.addEventListener("dragover", dragover)
node.addEventListener("dragenter", dragover)
node.addEventListener("dragleave", dragleave)
node.addEventListener("drop", drop)
return {
destroy() {
node.removeEventListener("dragover", dragover)
node.removeEventListener("dragenter", dragover)
node.removeEventListener("dragleave", dragleave)
node.removeEventListener("drop", drop)
}
}
}