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

${repositoryUrl}

-

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;