diff --git a/_site/components/file_drop_target.js b/_site/components/file_drop_target.js
new file mode 100644
index 0000000..7439b8a
--- /dev/null
+++ b/_site/components/file_drop_target.js
@@ -0,0 +1,106 @@
+/*
+ * JPEG Sawmill - A viewer for JPEG progressive scans
+ * Copyright (C) 2024 Rob Cowsill
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { html } from "../external/preact-htm-3.1.1.js";
+
+
+const mainClass = "file-drop-target";
+const activeClass = "active";
+const dragoverClass = "dragover";
+
+(function initModule() {
+ const root = document;
+ root.addEventListener("dragenter", handleRootDragEnter);
+ root.addEventListener("dragleave", handleRootDragClear);
+ root.addEventListener("dragover", handleRootDragOver);
+ root.addEventListener("drop", handleRootDragClear);
+})();
+
+function handleRootDragEnter(e) {
+ const root = e.currentTarget;
+ for (const el of root.querySelectorAll(`.${mainClass}`)) {
+ el.classList.add(activeClass);
+ }
+
+ handleRootDragOver(e);
+}
+
+function handleRootDragClear(e) {
+ // Deactivate drop targets when drag leaves without entering another element
+ if (e.relatedTarget === null) {
+ const root = e.currentTarget;
+ for (const el of root.querySelectorAll(`.${mainClass}`)) {
+ el.classList.remove(activeClass);
+ }
+ }
+}
+
+function handleRootDragOver(e) {
+ const items = e.dataTransfer.items;
+ if (items.length > 0) {
+ // Chrome workaround: preventDefault makes file input's button non-dropzone
+ const targetIsInput = e.target instanceof HTMLInputElement;
+ const targetIsFileInput = (targetIsInput && e.target.type === "file");
+ if (!targetIsFileInput || items[0].kind !== "file") {
+ e.preventDefault();
+ }
+ }
+}
+
+function handleTargetDragOver(e) {
+ const items = e.dataTransfer.items;
+ if (items.length === 1 && items[0].kind === "file") {
+ e.currentTarget.classList.add(dragoverClass);
+ } else {
+ e.dataTransfer.dropEffect = "none";
+ }
+
+ e.preventDefault();
+}
+
+function handleTargetDragLeave(e) {
+ e.currentTarget.classList.remove(dragoverClass);
+}
+
+
+function FileDropTarget({ children, onFileDrop }) {
+ function handleTargetDrop(e) {
+ handleTargetDragLeave(e);
+
+ const items = e.dataTransfer.items;
+ if (items.length === 1 && items[0].kind === "file") {
+ onFileDrop(e);
+ }
+
+ e.preventDefault();
+ }
+
+ const targetEvents = {
+ onDragOver: handleTargetDragOver,
+ onDragLeave: handleTargetDragLeave,
+ onDrop: handleTargetDrop
+ };
+
+ return html`
+
+ ${children}
+
+ `;
+}
+
+export default FileDropTarget;
diff --git a/_site/components/sawmill_about_box.js b/_site/components/sawmill_about_box.js
index f56d647..bbd3f10 100644
--- a/_site/components/sawmill_about_box.js
+++ b/_site/components/sawmill_about_box.js
@@ -21,12 +21,15 @@ import { html } from "../external/preact-htm-3.1.1.js";
const repositoryUrl = "https://github.com/rcowsill/JPEGSawmill";
-function SawmillAboutBox(){
+function SawmillAboutBox() {
return html`
JPEG Sawmill
-
Load a JPEG file using the "Browse..." button in the toolbar
+
+ Load a JPEG file using drag & drop or
+ the file selector in the toolbar
+
`;
}
diff --git a/_site/components/sawmill_app.js b/_site/components/sawmill_app.js
index d8c9d47..ef53693 100644
--- a/_site/components/sawmill_app.js
+++ b/_site/components/sawmill_app.js
@@ -29,8 +29,21 @@ function SawmillApp() {
loadJPEG(e.target.files, setFileData);
}
+ function onFileDrop(e) {
+ // Apply dropped file to file input
+ const fileInput = document.querySelector(".sawmill-toolbar .input-file");
+ fileInput.files = e.dataTransfer.files;
+
+ loadJPEG(e.dataTransfer.files, setFileData);
+ }
+
+ const uiEvents = {
+ onFileInputChange,
+ onFileDrop
+ };
+
return html`
- <${SawmillUI} ...${fileData} onFileInputChange=${onFileInputChange} />
+ <${SawmillUI} ...${fileData} uiEvents=${uiEvents} />
`;
}
diff --git a/_site/components/sawmill_file_drop_target.js b/_site/components/sawmill_file_drop_target.js
new file mode 100644
index 0000000..6b3b728
--- /dev/null
+++ b/_site/components/sawmill_file_drop_target.js
@@ -0,0 +1,40 @@
+/*
+ * JPEG Sawmill - A viewer for JPEG progressive scans
+ * Copyright (C) 2024 Rob Cowsill
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { html } from "../external/preact-htm-3.1.1.js";
+import FileDropTarget from "./file_drop_target.js";
+
+
+function SawmillFileDropTarget({ onFileDrop }) {
+ return html`
+
+ <${FileDropTarget} onFileDrop=${onFileDrop}>
+
+
+
+
+
+
Drop a JPEG file here to open it
+
Open file...
+
+ />
+
+ `;
+}
+
+export default SawmillFileDropTarget;
diff --git a/_site/components/sawmill_ui.js b/_site/components/sawmill_ui.js
index 1ce18fc..18d99dd 100644
--- a/_site/components/sawmill_ui.js
+++ b/_site/components/sawmill_ui.js
@@ -128,7 +128,7 @@ function handleWheelEvent(e, { zoomLevel }, setZoom) {
}
-function SawmillUI({ onFileInputChange, uint8Array, scanEndOffsets }) {
+function SawmillUI({ uint8Array, scanEndOffsets, uiEvents }) {
const [brightness, onBrightnessSet] = useValueState(0);
const [diffView, onDiffViewSet, setDiffView] = useCheckedState(false);
const [duration, onDurationSet] = useValueState(10);
@@ -188,12 +188,13 @@ function SawmillUI({ onFileInputChange, uint8Array, scanEndOffsets }) {
onDiffViewSet,
onBrightnessSet,
onScanlinesSet,
- onFileInputChange
+ onFileInputChange: uiEvents.onFileInputChange
};
const viewerEvents = {
onAnimationEnd: (e) => handleAnimationEvent(e, animationIndex, scanData, playbackSetters),
- onWheel: (e) => handleWheelEvent(e, zoom, setZoom)
+ onWheel: (e) => handleWheelEvent(e, zoom, setZoom),
+ onFileDrop: uiEvents.onFileDrop
};
return html`
diff --git a/_site/components/sawmill_viewer.js b/_site/components/sawmill_viewer.js
index d8e92dd..820d20c 100644
--- a/_site/components/sawmill_viewer.js
+++ b/_site/components/sawmill_viewer.js
@@ -18,6 +18,7 @@
import { html } from "../external/preact-htm-3.1.1.js";
import useTargetedZoom from "../hooks/targeted_zoom.js";
+import SawmillFileDropTarget from "./sawmill_file_drop_target.js";
import SawmillMeter from "./sawmill_meter.js";
import SawmillViewerFilter from "./sawmill_viewer_filter.js";
@@ -74,6 +75,7 @@ function SawmillViewer({ playback, scanData=[], selected=0, settings, viewerEven
<${SawmillViewerFilter} ...${{ scanData, selected, settings }}/>
+ <${SawmillFileDropTarget} onFileDrop=${viewerEvents.onFileDrop} />
`;
}
diff --git a/_site/css/jpeg_sawmill.css b/_site/css/jpeg_sawmill.css
index 503ed78..c08b557 100644
--- a/_site/css/jpeg_sawmill.css
+++ b/_site/css/jpeg_sawmill.css
@@ -16,6 +16,12 @@
* along with this program. If not, see .
*/
+button[disabled],
+fieldset[disabled],
+input[disabled] {
+ pointer-events: none;
+}
+
.sawmill-ui {
display: grid;
grid-template-areas:
@@ -29,6 +35,10 @@
margin-block: 1.2em;
}
+.sawmill-ui h1 {
+ font-family: sans-serif;
+}
+
.sawmill-ui .sawmill-toolbar {
grid-area: toolbar;
}
@@ -211,7 +221,7 @@
clip-path: inset(0 100% 0 0);
}
to {
- clip-path: inset(0)
+ clip-path: inset(0);
}
}
@@ -222,26 +232,102 @@
animation-duration: var(--anim-total-duration);
}
+.sawmill-file-drop-target {
+ grid-area: scans;
+ pointer-events: none;
+}
+
+.sawmill-file-drop-overlay {
+ --dragover-transition-duration: 300ms;
+
+ display: grid;
+ grid-template-areas: "stack";
+ height: 100%;
+ position: relative;
+ z-index: 3;
+}
+
+.sawmill-file-drop-overlay > * {
+ grid-area: stack;
+ position: relative;
+}
+
+.sawmill-file-drop-overlay .drop-filter {
+ backdrop-filter: saturate(40%) brightness(70%) blur(6px);
+}
+
+.sawmill-file-drop-overlay .drop-background {
+ background-color: darkslateblue;
+ opacity: 0.5;
+ transition: opacity var(--dragover-transition-duration);
+}
+
+.sawmill-file-drop-overlay .drop-vignette {
+ box-shadow: inset 0 0 24px 20px black;
+ opacity: 0.8;
+ transition: box-shadow var(--dragover-transition-duration);
+}
+
+.sawmill-file-drop-overlay .drop-outline {
+ border: 6px dashed blue;
+}
+
+.sawmill-file-drop-overlay :is(.drop-active-label, .drop-dragover-label) {
+ margin: 0;
+ padding-top: 1.5em;
+ color: white;
+ text-align: center;
+ text-shadow: 0 0 6px black;
+ transition: opacity var(--dragover-transition-duration);
+}
+
+.file-drop-target:not(.active) .sawmill-file-drop-overlay {
+ visibility: hidden;
+}
+
+.file-drop-target.dragover .sawmill-file-drop-overlay .drop-active-label,
+.file-drop-target:not(.dragover) .sawmill-file-drop-overlay .drop-dragover-label {
+ opacity: 0;
+}
+
+.file-drop-target.dragover .sawmill-file-drop-overlay .drop-background {
+ opacity: 0.9;
+}
+
+.file-drop-target.dragover .sawmill-file-drop-overlay .drop-vignette {
+ box-shadow: inset 0 0 24px 6px black;
+}
+
.sawmill-about-box {
background-color: gainsboro;
padding: 1.2em 2em;
border-radius: 6px;
border: 1px solid white;
+ width: min-content;
}
.sawmill-about-box > * {
margin: 0;
}
-.sawmill-about-box h1 {
- font-family: sans-serif;
-}
-
.sawmill-about-box h2 {
margin-bottom: 1.5em;
font-size: 1em;
}
+.sawmill-about-box a {
+ white-space: nowrap;
+}
+
+.file-drop-target {
+ height: 100%;
+ pointer-events: none;
+}
+
+.file-drop-target.active {
+ pointer-events: auto;
+}
+
.graduated-meter {
position: relative;
display: grid;