From b5a83e40578e7aa7f3557de7ce037b189a871801 Mon Sep 17 00:00:00 2001 From: marc Date: Mon, 24 Aug 2026 17:24:22 +0200 Subject: [PATCH] fix(upload): gezogenes Vorschaubild wurde erneut hochgeladen Zieht man das angezeigte Bild ein Stueck zur Seite, legt Chrome es als echte Datei in den Ziehvorgang. dataTransfer.types enthaelt dann "Files" - nicht zu unterscheiden von einer Datei aus dem Dateimanager. Die Ablageflaeche lud das eigene Bild daraufhin ein zweites Mal hoch. Zwei Riegel: draggable={false} an allen Bildern, die kein eigenes Ziehen brauchen (die Kachel bleibt ziehbar und verschiebt die Datei), und useInternalDrag - was in dieser Seite losgezogen ist, wird nie als Upload behandelt. dragstart feuert nur bei eigener Quelle. Gemessen: vorher 1 Upload, nachher 0. Ein Zug von aussen laedt unveraendert hoch. Co-Authored-By: Claude Opus 5 --- TESTING.md | 26 +++++++++++++++ frontend/src/components/FileCover.tsx | 5 +++ frontend/src/components/FilePreview.tsx | 3 ++ frontend/src/components/SearchBox.tsx | 2 +- frontend/src/components/Sidebar.tsx | 1 + frontend/src/components/UserMenu.tsx | 1 + frontend/src/hooks/useInternalDrag.ts | 44 +++++++++++++++++++++++++ frontend/src/pages/FileBrowser.tsx | 9 ++++- frontend/src/pages/LoginPage.tsx | 1 + frontend/src/pages/TagsPage.tsx | 2 +- frontend/src/pages/TrashPage.tsx | 1 + 11 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 frontend/src/hooks/useInternalDrag.ts diff --git a/TESTING.md b/TESTING.md index 1b1d8f8..e1826a2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1376,6 +1376,32 @@ hätte keine Media-Query sie erreicht. - [ ] Auf dem Flip 7 selbst gegenprüfen, auf beiden Schirmen +## Bild ziehen lud es erneut hoch (24.08.2026) + +Zieht man das angezeigte Bild in der Vorschau ein Stück zur Seite, legt Chrome +es als **echte Datei** in den Ziehvorgang. `dataTransfer.types` enthält dann +`Files` – nicht zu unterscheiden von einer Datei aus dem Dateimanager. Die +Ablagefläche lud das Bild daraufhin ein zweites Mal hoch. + +Nachgestellt und gemessen: + +| Fall | vorher | nachher | +|---|---|---| +| Vorschaubild ziehen und ablegen | `draggable=true`, **1 Upload** | `draggable=false`, **0 Uploads** | +| Datei von außen ablegen | 1 Upload, Overlay sichtbar | unverändert 1 Upload, Overlay sichtbar | + +Zwei Riegel, weil einer allein zu leicht wieder aufgeht: + +1. `draggable={false}` an allen Bildern, die kein eigenes Ziehen brauchen – + Kachel-Vorschauen, Vorschaufenster, Suchtreffer, Papierkorb, Etiketten, + Logo, Profilbild. Die **Kachel** bleibt ziehbar und verschiebt die Datei. +2. `hooks/useInternalDrag.ts`: Was in dieser Seite losgezogen ist, wird nie + als Upload behandelt. `dragstart` feuert nur bei eigener Quelle; von außen + gibt es keins. + +- [ ] Auf dem Handy gegenprüfen: Ein Bild lässt sich mit dem Finger nicht mehr + aus der Vorschau ziehen (dort war das Wischen ohnehin gemeint) + ## Automatisiert geprüft (22.08.2026) Diese Punkte hat Claude per API/`curl` gegen Wegwerf-Konten durchgespielt; sie diff --git a/frontend/src/components/FileCover.tsx b/frontend/src/components/FileCover.tsx index 36671ea..ca3a50c 100644 --- a/frontend/src/components/FileCover.tsx +++ b/frontend/src/components/FileCover.tsx @@ -22,6 +22,11 @@ export default function FileCover({ file }: { file: FileItem }) { return ( <> setFailed(true)} diff --git a/frontend/src/components/FilePreview.tsx b/frontend/src/components/FilePreview.tsx index 395f432..59559cf 100644 --- a/frontend/src/components/FilePreview.tsx +++ b/frontend/src/components/FilePreview.tsx @@ -280,6 +280,9 @@ export default function FilePreview({ file, onPrev, onNext, onClose, onSaved, se {kind === 'image' && ( {file.name} )} diff --git a/frontend/src/components/SearchBox.tsx b/frontend/src/components/SearchBox.tsx index 0a47ba0..e54953e 100644 --- a/frontend/src/components/SearchBox.tsx +++ b/frontend/src/components/SearchBox.tsx @@ -186,7 +186,7 @@ export default function SearchBox({ onOpenFolder, onOpenFile }: Props) { {hit.kind === 'folder' ? ( ) : hit.has_thumbnail ? ( - ) : ( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 744ab86..29cd424 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -379,6 +379,7 @@ export default function Sidebar({ user, view, activeTagId, onOpenTag,
{logoUrl ? ( boolean { + const active = useRef(false) + + useEffect(() => { + const start = () => { active.current = true } + // Der Rückweg über zwei Ereignisse: `dragend` feuert an der Quelle, + // `drop` am Ziel. Fällt eines aus (abgebrochener Zug außerhalb des + // Fensters), setzt das andere zurück. + const stop = () => { active.current = false } + // Setzen in der Erfassungsphase, damit die Marke steht, bevor irgendein + // Handler sie liest. **Zurücksetzen dagegen in der Blasenphase**: Ein + // `drop`-Listener am Dokument mit `capture` liefe *vor* der Ablagefläche, + // hätte die Marke also schon gelöscht, wenn sie gebraucht wird. + document.addEventListener('dragstart', start, true) + document.addEventListener('dragend', stop) + document.addEventListener('drop', stop) + return () => { + document.removeEventListener('dragstart', start, true) + document.removeEventListener('dragend', stop) + document.removeEventListener('drop', stop) + } + }, []) + + return () => active.current +} diff --git a/frontend/src/pages/FileBrowser.tsx b/frontend/src/pages/FileBrowser.tsx index 6a0138c..9aac3d9 100644 --- a/frontend/src/pages/FileBrowser.tsx +++ b/frontend/src/pages/FileBrowser.tsx @@ -21,6 +21,7 @@ import MoveTo from '../components/MoveTo' import TagDialog from '../components/TagDialog' import OfficeEditor from '../components/OfficeEditor' import { useAppSettings } from '../hooks/useAppSettings' +import { useInternalDrag } from '../hooks/useInternalDrag' import { DownloadIcon, ShareIcon, HistoryIcon, RenameIcon, MoveIcon, TrashIcon, FolderOpenIcon, StarIcon, CheckIcon, PeopleIcon, TagIcon, OfficeIcon, InfoIcon } from '../components/icons' interface Props { @@ -135,6 +136,7 @@ export default function FileBrowser({ const [ctxMenu, setCtxMenu] = useState(null) const [dialog, setDialog] = useState(null) const [dragging, setDragging] = useState(false) + const isInternalDrag = useInternalDrag() const [dragOverFolder, setDragOverFolder] = useState(null) const [viewMode, setViewMode] = useState(() => loadPref('aether:view', 'grid')) const [sort, setSort] = useState(() => loadPref('aether:sort', 'name-asc')) @@ -414,6 +416,10 @@ export default function FileBrowser({ setDragging(false) const t = e.dataTransfer.types if (t.includes('aether-file-id') || t.includes('aether-folder-id')) return // internal move, not upload + // Alles, was in dieser Seite losgezogen ist, ist kein Upload – auch ein + // vom Browser begonnener Bild-Zug, der `Files` mitbringt, aber keine + // unserer Kennungen. Siehe hooks/useInternalDrag. + if (isInternalDrag()) return // Capture FileSystemEntries synchronously – the DataTransferItemList is only // valid during the drop event itself. @@ -718,7 +724,8 @@ export default function FileBrowser({ onDragOver={e => { e.preventDefault() const t = e.dataTransfer.types - if (t.includes('Files') && !t.includes('aether-file-id') && !t.includes('aether-folder-id')) setDragging(true) + if (t.includes('Files') && !t.includes('aether-file-id') && !t.includes('aether-folder-id') + && !isInternalDrag()) setDragging(true) }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragging(false) }} onDrop={onDrop} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index d8d5862..4ee846d 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -44,6 +44,7 @@ export default function LoginPage({ onLogin }: Props) { aus. */} {config.logo_url ? ( {config.cloud_name} ) : ( diff --git a/frontend/src/pages/TrashPage.tsx b/frontend/src/pages/TrashPage.tsx index a7661e9..f2d4b30 100644 --- a/frontend/src/pages/TrashPage.tsx +++ b/frontend/src/pages/TrashPage.tsx @@ -106,6 +106,7 @@ export default function TrashPage({ onUserChanged }: Props) { }}> {file.has_thumbnail ? (