diff --git a/TESTING.md b/TESTING.md index 57723f3..74a49ac 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1500,6 +1500,31 @@ Ereignisse, `transform: none`. programmgesteuerter Zeiger prüft die Überfahrung nicht bei jedem Bild neu, eine echte Maus schon. Bitte mit der Maus gegenprüfen. +## Wischen holt die Seitenleiste (25.08.2026) + +Vom linken Rand nach rechts öffnet, nach links schließt – nur an Geräten mit +grobem Zeiger (`pointer: coarse`), mit der Maus passiert nichts. + +Gemessen bei 412×900 mit Touch: + +| Geste | Erwartet | Gemessen | +|---|---|---| +| vom Rand (x=10) 150 px nach rechts | öffnet | ✅ | +| offen, 130 px nach links | schließt | ✅ | +| aus der Mitte (x=200) nach rechts | nichts | ✅ | +| vom Rand, nur 30 px | nichts | ✅ | +| vom Rand, 60 px rechts bei 100 px runter | nichts | ✅ | +| vom Rand, 150 px in 1,2 s | nichts | ✅ | +| in der Vollbild-Vorschau vom Rand | nichts | ✅ | + +Die Grenzen: 44 px Randstreifen, mindestens 60 px Weg, doppelt so weit +waagerecht wie senkrecht, höchstens 800 ms. + +- [ ] **Auf Android gegenprüfen.** Die Zurück-Geste des Systems belegt die + äußersten ~20 px; was dort beginnt, erreicht die Seite nie. Deshalb 44 px + statt 20 – ob das in der Praxis reicht, kann nur ein echtes Gerät sagen. +- [ ] Im Querformat gegenprüfen (Aussparung links) + ## Automatisiert geprüft (22.08.2026) Diese Punkte hat Claude per API/`curl` gegen Wegwerf-Konten durchgespielt; sie diff --git a/frontend/src/components/FilePreview.tsx b/frontend/src/components/FilePreview.tsx index 5ed740e..5298cb2 100644 --- a/frontend/src/components/FilePreview.tsx +++ b/frontend/src/components/FilePreview.tsx @@ -171,6 +171,10 @@ export default function FilePreview({ file, onPrev, onNext, onClose, onSaved, se return (
diff --git a/frontend/src/components/Shell.tsx b/frontend/src/components/Shell.tsx index 2a6f89b..bed1d68 100644 --- a/frontend/src/components/Shell.tsx +++ b/frontend/src/components/Shell.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import { filesApi } from '../api/client' import type { FolderItem, User } from '../api/client' import { useNavHistory, type View } from '../hooks/useNavHistory' +import { useEdgeSwipe } from '../hooks/useEdgeSwipe' import Sidebar from './Sidebar' import FileBrowser from '../pages/FileBrowser' import TrashPage from '../pages/TrashPage' @@ -25,6 +26,14 @@ export default function Shell({ user, onLogout, onUserChanged }: Props) { const nav = useNavHistory() const { view, folderPath } = nav const [sidebarOpen, setSidebarOpen] = useState(false) + + // Am Finger: vom linken Rand nach rechts wischen holt die Leiste, nach + // links wischen schickt sie zurück. Mit Maus passiert nichts. + useEdgeSwipe({ + open: sidebarOpen, + onOpen: useCallback(() => setSidebarOpen(true), []), + onClose: useCallback(() => setSidebarOpen(false), []), + }) // Hochgezählt, wenn etwas auf den Papierkorb gezogen wurde – die Dateiliste // hängt daran und holt sich daraufhin neu. const [trashBump, setTrashBump] = useState(0) diff --git a/frontend/src/hooks/useEdgeSwipe.ts b/frontend/src/hooks/useEdgeSwipe.ts new file mode 100644 index 0000000..37ba418 --- /dev/null +++ b/frontend/src/hooks/useEdgeSwipe.ts @@ -0,0 +1,97 @@ +import { useEffect } from 'react' + +/** + * Wischen vom linken Rand öffnet die Seitenleiste, Wischen nach links + * schließt sie wieder. + * + * Bisher gab es dafür nur den Knopf oben links. Auf einem Telefon ist das die + * Ecke, die man einhändig am schlechtesten erreicht – gerade auf einem + * aufgeklappten Flip. + * + * Drei Bedingungen, damit die Geste nicht in die Quere kommt: + * + * * **Nur am Rand.** Sonst öffnete jedes Wischen über eine Kachel die + * Leiste, und waagerecht scrollbare Bereiche (Reiterleisten, breite + * Tabellen, der PDF-Betrachter im Zoom) wären nicht mehr zu bedienen. + * * **Deutlich waagerecht.** Beim Blättern durch eine lange Liste wandert + * der Finger fast nie exakt senkrecht; ohne diese Bedingung öffnete sich + * die Leiste beim Scrollen. + * * **Zügig.** Ein langsames Ziehen ist meistens etwas anderes – ein + * Auswählen, ein Verschieben, ein Halten. + * + * **Auf Android hat das System das erste Wort:** Die Zurück-Geste belegt die + * äußersten ~20 Pixel beider Ränder, und was dort beginnt, erreicht die Seite + * gar nicht. Die Zone ist deshalb 44 Pixel breit – ein Wisch, der knapp hinter + * dem Systembereich ansetzt, wird noch erkannt. + */ + +/** Breite des Streifens am linken Rand, in dem eine Geste beginnen darf. */ +const edgeZone = 44 +/** So weit muss der Finger waagerecht wandern. */ +const minDistance = 60 +/** Länger gilt als „gezogen", nicht als „gewischt". */ +const maxDuration = 800 + +interface Options { + open: boolean + onOpen: () => void + onClose: () => void +} + +export function useEdgeSwipe({ open, onOpen, onClose }: Options) { + useEffect(() => { + if (typeof matchMedia !== 'function' || !matchMedia('(pointer: coarse)').matches) return + + let startX = 0 + let startY = 0 + let startedAt = 0 + let valid = false + + function onStart(e: TouchEvent) { + valid = false + if (e.touches.length !== 1) return // zwei Finger heißt zoomen + const t = e.touches[0] + + // Nicht aus einer bildschirmfüllenden Ansicht heraus: Dort läge die + // Leiste hinter dem Betrachter, und man wischt dort durch Seiten. + const el = e.target instanceof Element ? e.target : null + if (el?.closest('[data-fullscreen]')) return + + // Geschlossen: nur am Rand. Offen: überall dort, wo die Leiste und ihr + // Schatten liegen – weiter rechts hat ein Wisch nichts mit ihr zu tun. + if (!open && t.clientX > edgeZone) return + if (open && t.clientX > 320) return + + startX = t.clientX + startY = t.clientY + startedAt = Date.now() + valid = true + } + + function onEnd(e: TouchEvent) { + if (!valid) return + valid = false + const t = e.changedTouches[0] + if (!t) return + if (Date.now() - startedAt > maxDuration) return + + const dx = t.clientX - startX + const dy = t.clientY - startY + // Doppelt so weit zur Seite wie nach oben oder unten: Das trennt ein + // Wischen sauber vom Blättern, ohne einen genauen Winkel zu verlangen. + if (Math.abs(dx) < minDistance || Math.abs(dx) < Math.abs(dy) * 2) return + + if (!open && dx > 0) onOpen() + else if (open && dx < 0) onClose() + } + + // Passiv: Die Geste verhindert nichts, sie sieht nur zu. Ein nicht-passiver + // Zuhörer würde das Scrollen der ganzen Anwendung ausbremsen. + document.addEventListener('touchstart', onStart, { passive: true }) + document.addEventListener('touchend', onEnd, { passive: true }) + return () => { + document.removeEventListener('touchstart', onStart) + document.removeEventListener('touchend', onEnd) + } + }, [open, onOpen, onClose]) +}