From 30a485f5d960f103f5d8f101921fffa72f8c4405 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Thu, 8 Oct 2026 23:28:23 +0200 Subject: [PATCH] Mark the file the folder download is on During "Download all to a folder" the recipient saw only the file name under the progress bar. The file's row in the list now shows the loading indicator while it is on its way, and the list scrolls that row into view, so a run through hundreds of files can be followed in the list. The row is scrolled rather than the indicator, which is still hidden when the run moves on to the file. Co-Authored-By: Claude Opus 5.5 --- resources/js/share-downloader.js | 6 ++- .../views/livewire/share-download.blade.php | 11 +++++- tests/Browser/ShareFlowTest.php | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/resources/js/share-downloader.js b/resources/js/share-downloader.js index a496a34..88012a7 100644 --- a/resources/js/share-downloader.js +++ b/resources/js/share-downloader.js @@ -68,9 +68,11 @@ document.addEventListener('alpine:init', () => { /** How many of the run's files are in the folder. */ saved: 0, - /** The file on its way. */ + /** The file on its way: its path for the status line, its id for its row in the list. */ current: null, + currentId: null, + /** The files that failed, kept for the Retry button. */ failed: [], @@ -140,6 +142,7 @@ document.addEventListener('alpine:init', () => { for (const file of list) { const before = this.received this.current = file.path + this.currentId = file.id try { await this.save(file, controller.signal) @@ -162,6 +165,7 @@ document.addEventListener('alpine:init', () => { this.running = false this.current = null + this.currentId = null this.finished = ! controller.signal.aborted }, diff --git a/resources/views/livewire/share-download.blade.php b/resources/views/livewire/share-download.blade.php index fdc1909..f36dede 100644 --- a/resources/views/livewire/share-download.blade.php +++ b/resources/views/livewire/share-download.blade.php @@ -105,7 +105,16 @@ {{ Number::fileSize($file->file_size) }} - + {{-- The file the folder download is on; its row, always shown, is brought into view in the list as it starts. --}} + + + + diff --git a/tests/Browser/ShareFlowTest.php b/tests/Browser/ShareFlowTest.php index 3e31a9e..a57a82f 100644 --- a/tests/Browser/ShareFlowTest.php +++ b/tests/Browser/ShareFlowTest.php @@ -313,3 +313,40 @@ test('a long file list scrolls on its own and keeps the download buttons on the ->assertScript("[...document.querySelectorAll('[data-md-button]')].filter((button) => button.offsetParent !== null && button.textContent.trim().toLowerCase().startsWith('download all')).every((button) => button.getBoundingClientRect().bottom <= window.innerHeight)") ->assertNoJavaScriptErrors(); }); + +test('the folder download marks the file it is on and scrolls the list to it', function () { + $share = app(ShareService::class)->createShare(collect(range(1, 40))->map(fn (int $number): array => [ + 'file' => UploadedFile::fake()->createWithContent("file-{$number}.txt", "file {$number}"), + 'relativePath' => null, + ])->all()); + $last = $share->files->last(); + + $page = ready(visit(route('share.download', $share, false))->resize(1280, 800)); + + // Every file but the last is already in the folder, and the last one's request never answers, + // so the run stays on it. + $page->script("(async () => { + const root = await navigator.storage.getDirectory(); + for await (const name of root.keys()) { await root.removeEntry(name, { recursive: true }); } + for (let number = 1; number < 40; number++) { + const writable = await (await root.getFileHandle(`file-\${number}.txt`, { create: true })).createWritable(); + await writable.write(`file \${number}`); + await writable.close(); + } + window.showDirectoryPicker = () => Promise.resolve(root); + window.fetch = () => new Promise(() => {}); + Alpine.\$data(document.querySelector('[data-test=download-to-folder]')).supported = true; + })()"); + + $page->click('[data-test="download-to-folder"]'); + + $page->assertScript("[...document.querySelectorAll('[data-test=file-downloading]')].filter((mark) => mark.offsetParent !== null).length === 1") + ->assertScript("(() => { + const mark = [...document.querySelectorAll('[data-test=file-downloading]')].find((mark) => mark.offsetParent !== null); + const row = mark.closest('[data-md-list-item]'); + const list = document.querySelector('[data-test=share-file-list]').getBoundingClientRect(); + const rect = row.getBoundingClientRect(); + return row.textContent.includes('{$last->original_name}') && rect.top >= list.top - 1 && rect.bottom <= list.bottom + 1; + })()") + ->assertNoJavaScriptErrors(); +});