Allow drag-and-drop uploading in album viewer

This commit is contained in:
2023-03-21 23:37:46 +01:00
parent b2101b3243
commit 236e282777
7 changed files with 195 additions and 96 deletions

View File

@@ -21,6 +21,7 @@ import CustomBanner from "./CustomBanner.svelte";
import LoadingIndicator from "../util/LoadingIndicator.svelte"; import LoadingIndicator from "../util/LoadingIndicator.svelte";
import TransferLimit from "./TransferLimit.svelte"; import TransferLimit from "./TransferLimit.svelte";
import ListStats from "./ListStats.svelte"; import ListStats from "./ListStats.svelte";
import ListUpdater from "./ListUpdater.svelte";
let loading = true let loading = true
let embedded = false let embedded = false
@@ -73,6 +74,7 @@ let toolbar_toggle = () => {
} }
let downloader let downloader
let list_updater
let details_window let details_window
let details_visible = false let details_visible = false
let qr_window let qr_window
@@ -354,6 +356,10 @@ const keyboard_event = evt => {
case "q": // Q to close the window case "q": // Q to close the window
window.close() window.close()
break break
case "u": // U to upload new files
if (list_updater) {
list_updater.pick_files()
}
} }
} }
@@ -561,22 +567,20 @@ const keyboard_event = evt => {
on:prev={() => { if (list_navigator) { list_navigator.prev() }}} on:prev={() => { if (list_navigator) { list_navigator.prev() }}}
on:next={() => { if (list_navigator) { list_navigator.next() }}} on:next={() => { if (list_navigator) { list_navigator.next() }}}
on:loading={e => {loading = e.detail}} on:loading={e => {loading = e.detail}}
on:reload={reload}> on:reload={reload}
</FilePreview> />
{:else if view === "gallery"} {:else if view === "gallery"}
<GalleryView <GalleryView
list={list} list={list}
on:reload={reload} on:reload={reload}
on:loading={e => {loading = e.detail}}> on:update_list={e => list_updater.update(e.detail)}
</GalleryView> on:pick_files={() => list_updater.pick_files()}
on:upload_files={e => list_updater.upload_files(e.detail)}
/>
{/if} {/if}
</div> </div>
<Sharebar bind:this={sharebar}></Sharebar> <Sharebar bind:this={sharebar}></Sharebar>
<!-- {#if ads_enabled}
<AdSkyscraper on:visibility={e => {skyscraper_visible = e.detail}}></AdSkyscraper>
{/if} -->
</div> </div>
{#if ads_enabled} {#if ads_enabled}
@@ -611,6 +615,15 @@ const keyboard_event = evt => {
{/if} {/if}
<Downloader bind:this={downloader} file={file} list={list}></Downloader> <Downloader bind:this={downloader} file={file} list={list}></Downloader>
{#if is_list && list.can_edit}
<ListUpdater
bind:this={list_updater}
list={list}
on:reload={reload}
on:loading={e => {loading = e.detail}}
/>
{/if}
</div> </div>
<style> <style>

View File

@@ -6,74 +6,28 @@ import { file_type } from "./FileUtilities.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let list = { export let list = {
id: "",
title: "",
files: [], files: [],
download_href: "",
info_href: "",
can_edit: false, can_edit: false,
} }
let file_picker; let file_picker;
const update_list = async new_files => {
dispatch("loading", true)
// If the list is empty we simply delete it
if (list.files.length === 0) {
try {
let resp = await fetch(list.info_href, {method: "DELETE"})
if (resp.status >= 400) {
throw (await resp.json()).message
}
window.close()
} catch (err) {
alert("Failed to delete album: "+err)
} finally {
dispatch("loading", false)
}
return
}
let listjson = {
title: list.title,
files: [],
}
list.files.forEach(f => {
listjson.files.push({
id: f.id,
})
})
try {
const resp = await fetch(
list.info_href,
{ method: "PUT", body: JSON.stringify(listjson) },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
} catch (err) {
alert("Failed to update album: "+err)
} finally {
dispatch("loading", false)
}
}
const add_files = async files => { const add_files = async files => {
let list_files = list.files; let list_files = list.files;
files.forEach(f => { files.forEach(f => {
list_files.push(f) list_files.push(f)
}) })
await update_list(list_files)
dispatch("reload") list.files = list_files // Update the view (and play animation)
dispatch("update_list", list_files)
} }
const delete_file = async index => { const delete_file = async index => {
let list_files = list.files let list_files = list.files
list_files.splice(index, 1) list_files.splice(index, 1)
await update_list(list_files)
list.files = list_files list.files = list_files // Update the view (and play animation)
dispatch("update_list", list_files)
} }
const move_left = async index => { const move_left = async index => {
@@ -82,8 +36,9 @@ const move_left = async index => {
} }
let f = list.files; let f = list.files;
[f[index], f[index-1]] = [f[index-1], f[index]]; [f[index], f[index-1]] = [f[index-1], f[index]];
await update_list(f)
list.files = f list.files = f // Update the view (and play animation)
dispatch("update_list", f)
} }
const move_right = async index => { const move_right = async index => {
if (index >= list.files.length-1) { if (index >= list.files.length-1) {
@@ -91,17 +46,33 @@ const move_right = async index => {
} }
let f = list.files; let f = list.files;
[f[index], f[index+1]] = [f[index+1], f[index]]; [f[index], f[index+1]] = [f[index+1], f[index]];
await update_list(f)
list.files = f list.files = f // Update the view (and play animation)
dispatch("update_list", f)
} }
// Index of the file which is being hovered over. -1 is nothing and -2 is the
// Add files button
let hovering = -1 let hovering = -1
let dragging = false
const drag = (e, index) => { const drag = (e, index) => {
dragging = true
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
e.dataTransfer.setData('text/plain', index); e.dataTransfer.setData('text/plain', index);
} }
const drop = (e, index) => { const drop = (e, index) => {
hovering = -1
dragging = false
if (e.dataTransfer.files.length !== 0) {
// This is not a rearrangement, this is a file upload
dispatch("upload_files", e.dataTransfer.files)
return
} else if (index === -2) {
return
}
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = 'move';
let start = parseInt(e.dataTransfer.getData("text/plain")); let start = parseInt(e.dataTransfer.getData("text/plain"));
let list_files = list.files let list_files = list.files
@@ -116,11 +87,32 @@ const drop = (e, index) => {
return; // Nothing changed return; // Nothing changed
} }
update_list(list_files) dispatch("update_list", list_files)
} }
</script> </script>
<div class="gallery"> <div class="gallery">
{#if list.can_edit}
<div class="add_button"
on:drop|preventDefault={e => drop(e, -2)}
on:dragover|preventDefault|stopPropagation
on:dragenter={() => hovering = -2}
on:dragend={() => {hovering = -1}}
class:highlight={!dragging && hovering === -2}
>
<button on:click={e => dispatch("pick_files")} style="font-size: 1.5em; cursor: pointer;">
<i class="icon">cloud_upload</i>
<br/>
Upload files
</button>
<button on:click={file_picker.open} style="font-size: 1.5em; cursor: pointer;">
<i class="icon">add</i>
<br/>
Add files
</button>
</div>
{/if}
{#each list.files as file, index (file)} {#each list.files as file, index (file)}
<a <a
href="#item={index}" href="#item={index}"
@@ -129,10 +121,10 @@ const drop = (e, index) => {
on:dragstart={e => drag(e, index)} on:dragstart={e => drag(e, index)}
on:drop|preventDefault={e => drop(e, index)} on:drop|preventDefault={e => drop(e, index)}
on:dragover|preventDefault|stopPropagation on:dragover|preventDefault|stopPropagation
on:dragenter={() => {hovering = index}} on:dragenter={() => hovering = index}
on:dragend={() => {hovering = -1}} on:dragend={() => {hovering = -1; dragging = false}}
class:highlight={hovering === index} class:highlight={dragging && hovering === index}
animate:flip={{duration: 500}}> animate:flip={{duration: 400}}>
<div <div
class="icon_container" class="icon_container"
class:editing={list.can_edit} class:editing={list.can_edit}
@@ -159,14 +151,6 @@ const drop = (e, index) => {
{file.name} {file.name}
</a> </a>
{/each} {/each}
{#if list.can_edit}
<button class="file" on:click={file_picker.open} style="font-size: 1.5em; cursor: pointer;">
<i class="icon">add</i>
<br/>
Add files
</button>
{/if}
</div> </div>
<FilePicker <FilePicker
@@ -242,4 +226,23 @@ const drop = (e, index) => {
.button_row>.separator { .button_row>.separator {
flex: 1 1 auto; flex: 1 1 auto;
} }
.add_button{
width: 200px;
max-width: 42%;
height: 200px;
margin: 8px;
border-radius: 8px;
background: var(--body_color);
text-align: center;
line-height: 1.2em;
display: inline-block;
vertical-align: top;
color: var(--body_text_color);
display: flex;
flex-direction: column;
}
.add_button > * {
flex: 1 1 auto;
}
</style> </style>

View File

@@ -0,0 +1,82 @@
<script>
import { createEventDispatcher } from "svelte";
import UploadWidget from "../util/upload_widget/UploadWidget.svelte";
let dispatch = createEventDispatcher()
export let list = {
title: "",
files: [],
info_href: "",
}
export const update = async new_files => {
dispatch("loading", true)
// If the list is empty we simply delete it
if (list.files.length === 0) {
try {
let resp = await fetch(list.info_href, {method: "DELETE"})
if (resp.status >= 400) {
throw (await resp.json()).message
}
window.close()
} catch (err) {
alert("Failed to delete album: "+err)
} finally {
dispatch("loading", false)
}
return
}
let listjson = {
title: list.title,
files: [],
}
new_files.forEach(f => {
listjson.files.push({
id: f.id,
})
})
try {
const resp = await fetch(
list.info_href,
{ method: "PUT", body: JSON.stringify(listjson) },
);
if (resp.status >= 400) {
throw (await resp.json()).message
}
} catch (err) {
alert("Failed to update album: "+err)
} finally {
dispatch("loading", false)
dispatch("reload")
}
}
let upload_widget
export const pick_files = () => upload_widget.pick_files()
export const upload_files = files => upload_widget.upload_files(files)
const uploads_finished = async (file_ids) => {
let list_files = list.files;
file_ids.forEach(id => {
list_files.push({id: id})
})
await update(list_files)
}
const paste = (e) => {
if (e.clipboardData.files.length !== 0) {
e.preventDefault();
e.stopPropagation();
upload_widget.upload_files(e.clipboardData.files)
}
}
</script>
<svelte:window on:paste={paste}/>
<UploadWidget bind:this={upload_widget} on:uploads_finished={e => uploads_finished(e.detail)}/>

View File

@@ -257,8 +257,6 @@ onMount(() => {
<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>
@@ -340,6 +338,8 @@ onMount(() => {
<iframe bind:this={downloadFrame} title="File download frame" style="display: none; width: 1px; height: 1px;"></iframe> <iframe bind:this={downloadFrame} title="File download frame" style="display: none; width: 1px; height: 1px;"></iframe>
</div> </div>
<UploadWidget bind:this={upload_widget} drop_upload on:uploads_finished={hashChange}/>
<style> <style>
:global(#page_body) { :global(#page_body) {
height: 100vh; height: 100vh;

View File

@@ -14,10 +14,10 @@ const drop = (e) => {
} }
} }
const paste = (e) => { const paste = (e) => {
if (e.clipboardData.files[0]) { if (e.clipboardData.files.length !== 0) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
dispatch("upload", e.dataTransfer.files) dispatch("upload", e.clipboardData.files)
} }
} }
</script> </script>
@@ -39,8 +39,6 @@ const paste = (e) => {
<style> <style>
.drag_target { .drag_target {
position: fixed; position: fixed;
height: auto;
margin: auto;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);

View File

@@ -1,5 +1,6 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import ProgressBar from "../ProgressBar.svelte"; import ProgressBar from "../ProgressBar.svelte";
import { upload_file } from "./UploadFunc"; import { upload_file } from "./UploadFunc";
@@ -43,7 +44,7 @@ export const start = () => {
</script> </script>
<div class="upload_progress" class:error={job.status === "error"}> <div class="upload_progress" transition:fade={{duration: 200}} class:error={job.status === "error"}>
{job.name}<br/> {job.name}<br/>
{#if error_code !== ""} {#if error_code !== ""}
{error_message}<br/> {error_message}<br/>

View File

@@ -1,5 +1,6 @@
<script> <script>
import { createEventDispatcher, tick } from "svelte"; import { createEventDispatcher, tick } from "svelte";
import { fade } from "svelte/transition";
import DropUpload from "./DropUpload.svelte"; import DropUpload from "./DropUpload.svelte";
import UploadProgress from "./UploadProgress.svelte"; import UploadProgress from "./UploadProgress.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@@ -103,12 +104,8 @@ const finish_upload = (e) => {
class="upload_input" type="file" name="file" multiple="multiple" class="upload_input" type="file" name="file" multiple="multiple"
/> />
{#if drop_upload}
<DropUpload on:upload={e => upload_files(e.detail)}/>
{/if}
{#if visible} {#if visible}
<div class="upload_widget"> <div class="upload_widget" transition:fade={{duration: 200}}>
<div class="header"> <div class="header">
{#if state === "idle"} {#if state === "idle"}
Waiting for files Waiting for files
@@ -128,10 +125,14 @@ const finish_upload = (e) => {
</div> </div>
{/if} {/if}
{#if drop_upload}
<DropUpload on:upload={e => upload_files(e.detail)}/>
{/if}
<style> <style>
.upload_input { .upload_input {
visibility: hidden; visibility: hidden;
position: static; position: fixed;
width: 0; width: 0;
height: 0; height: 0;
} }
@@ -139,16 +140,15 @@ const finish_upload = (e) => {
position: fixed; position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 400px; width: 500px;
max-width: 90%; max-width: 80%;
height: auto; height: auto;
max-height: 500px; max-height: 50%;
right: 20px; right: 20px;
bottom: 20px; bottom: 20px;
border-radius: 20px 20px 8px 8px; border-radius: 20px 20px 8px 8px;
overflow-x: hidden; overflow: hidden;
overflow-y: auto; box-shadow: 1px 1px 8px var(--shadow_color);
box-shadow: 1px 1px 10px -2px var(--shadow_color);
} }
.header { .header {
@@ -166,5 +166,7 @@ const finish_upload = (e) => {
flex: 1 1 auto; flex: 1 1 auto;
background: var(--body_color); background: var(--body_color);
color: var(--body_text_color); color: var(--body_text_color);
overflow-y: auto;
text-align: left;
} }
</style> </style>