feat(uploads): Fortschritt in MB, Tempo und Restzeit; Abbruchkreuz rot
Das Kreuz faerbt sich beim Darueberfahren rot -- aber nur, solange der Upload laeuft. Dasselbe Zeichen tut zwei Dinge: eine Meldung wegraeumen ist folgenlos, einen laufenden Vorgang fallen zu lassen nicht. Unter dem Balken steht jetzt "4.2 MB / 18.0 MB · 1.8 MB/s · 8 s left". Die Zahlen kamen schon immer vom XMLHttpRequest, sie wurden nur weggeworfen. Neu gerechnet wird nur das Tempo -- der Browser meldet Bytes, nicht deren Ableitung. Zwei Daempfungen, aus zwei Gruenden: * Das Tempo aus einem einzelnen Abstand springt zwischen dem Doppelten und der Haelfte. Gleitender Mittelwert (0,7/0,3) ueber Messpunkte von mindestens 0,2 s. * Die Anzeige wird hoechstens alle 150 ms nachgezogen. Der Browser meldet im 50-ms-Takt; bei drei Dateien waeren das sechzig Neuaufbauten der Liste je Sekunde fuer eine Zahl, die niemand so schnell liest. Jeder Teil erscheint erst, wenn er stimmt: Groesse sofort, Tempo nach dem zweiten Messpunkt, Restzeit erst mit beidem. Eine Schaetzung aus dem Nichts waere schlimmer als keine -- man richtet sich danach. formatDuration in src/format.ts, 5 Tests (37 gesamt). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
511213b40d
commit
e9f69f7d7f
4 files changed
+175
-14
No files matched your search
@@ -65,6 +65,17 @@ export function useUploads({ folderId, onFinished }: Options) {
|
||||
*/
|
||||
const canceled = useRef(new Set<string>())
|
||||
|
||||
/**
|
||||
* Was für Tempo und Restzeit gemerkt werden muss.
|
||||
*
|
||||
* Der Browser meldet nur „so viele Bytes sind draußen". Wie schnell das
|
||||
* geht, steht nirgends – das ist die Ableitung, und dafür braucht es den
|
||||
* vorigen Messpunkt.
|
||||
*/
|
||||
const rates = useRef(new Map<string, {
|
||||
lastAt: number; lastLoaded: number; speed: number; lastPatch: number
|
||||
}>())
|
||||
|
||||
/**
|
||||
* Der Zielordner in einem Ref – **nicht** in der Abhängigkeitsliste.
|
||||
*
|
||||
@@ -87,6 +98,42 @@ export function useUploads({ folderId, onFinished }: Options) {
|
||||
const patchRef = useRef(patch)
|
||||
patchRef.current = patch
|
||||
|
||||
/**
|
||||
* Ein Fortschrittsbericht: Tempo mitrechnen und die Zeile nachführen.
|
||||
*
|
||||
* Zwei Dinge werden hier gedämpft, aus zwei verschiedenen Gründen:
|
||||
*
|
||||
* * **Das Tempo** aus einem einzelnen Abstand gerechnet springt zwischen
|
||||
* dem Doppelten und der Hälfte – die Pakete kommen nun einmal nicht
|
||||
* gleichmäßig heraus. Ein gleitender Mittelwert macht daraus eine Zahl,
|
||||
* die man ablesen kann, ohne dass sie träge wird.
|
||||
* * **Die Anzeige.** Der Browser meldet Fortschritt im 50-Millisekunden-
|
||||
* Takt; bei drei Dateien gleichzeitig wären das sechzig Neuaufbauten der
|
||||
* Liste je Sekunde für eine Zahl, die sich niemand so schnell ansieht.
|
||||
*/
|
||||
const onProgress = useCallback((id: string, loaded: number, total: number, percent: number) => {
|
||||
const r = rates.current.get(id)
|
||||
if (!r) return
|
||||
const now = performance.now()
|
||||
|
||||
// Erst ab einem Fünftel Sekunde messen: Darunter ist der Abstand so klein,
|
||||
// dass die Rechnung mehr Zufall als Tempo enthält.
|
||||
const dt = (now - r.lastAt) / 1000
|
||||
if (r.lastAt === 0) {
|
||||
r.lastAt = now
|
||||
r.lastLoaded = loaded
|
||||
} else if (dt >= 0.2) {
|
||||
const jetzt = (loaded - r.lastLoaded) / dt
|
||||
r.speed = r.speed === 0 ? jetzt : r.speed * 0.7 + jetzt * 0.3
|
||||
r.lastAt = now
|
||||
r.lastLoaded = loaded
|
||||
}
|
||||
|
||||
if (now - r.lastPatch < 150 && percent < 100) return
|
||||
r.lastPatch = now
|
||||
patch(id, { progress: percent, loaded, total, speed: r.speed })
|
||||
}, [patch])
|
||||
|
||||
/**
|
||||
* Den Vorgang einer Zeile abbrechen.
|
||||
*
|
||||
@@ -107,10 +154,12 @@ export function useUploads({ folderId, onFinished }: Options) {
|
||||
if (canceled.current.has(id)) return
|
||||
const controller = new AbortController()
|
||||
running.current.set(id, controller)
|
||||
rates.current.set(id, { lastAt: 0, lastLoaded: 0, speed: 0, lastPatch: 0 })
|
||||
try {
|
||||
await uploadWithProgress(file, target,
|
||||
({ percent }) => patch(id, { progress: percent }), controller.signal)
|
||||
patch(id, { progress: 100, status: 'done' })
|
||||
({ loaded, total, percent }) => onProgress(id, loaded, total, percent),
|
||||
controller.signal)
|
||||
patch(id, { progress: 100, loaded: file.size, total: file.size, speed: 0, status: 'done' })
|
||||
} catch (err) {
|
||||
// Ein Abbruch ist kein Fehler: Die Zeile steht schon auf „abgebrochen",
|
||||
// und eine rote Meldung darüber wäre ein Vorwurf für eine Entscheidung,
|
||||
@@ -119,8 +168,9 @@ export function useUploads({ folderId, onFinished }: Options) {
|
||||
patch(id, { status: 'error', error: err instanceof APIError ? err.message : 'Upload failed' })
|
||||
} finally {
|
||||
running.current.delete(id)
|
||||
rates.current.delete(id)
|
||||
}
|
||||
}, [patch])
|
||||
}, [patch, onProgress])
|
||||
|
||||
/** Einzelne Dateien – aus dem Auswahlfenster oder per Ablage. */
|
||||
const uploadFiles = useCallback(async (fileList: File[]) => {
|
||||
|
||||
Reference in new issue
Block a user