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 <noreply@anthropic.com>
190 lines
13 KiB
PHP
190 lines
13 KiB
PHP
{{-- The page a recipient opens. No anchored components (menus, tooltips) on it: it has to work on
|
|
iOS before Safari 18.4, which cannot position them. --}}
|
|
|
|
<x-page brand>
|
|
{{-- Under the page's h1, what the recipient acts on is in cards: the password, or the share's
|
|
private text and its files, each in its own card, with the share's expiry and limit under them. --}}
|
|
@if (! $authenticated)
|
|
<x-card :title="__('Password Required')" :subtitle="__('Enter the password to open this share')" heading="h2" variant="outlined">
|
|
<x-form wire:submit="verifyPassword">
|
|
<x-password
|
|
full
|
|
wire:model="password"
|
|
:label="__('Password')"
|
|
required
|
|
autocomplete="off"
|
|
:placeholder="__('Enter share password')"
|
|
/>
|
|
|
|
<x-slot:actions>
|
|
<x-button type="submit" :label="__('Unlock')" variant="filled" icon="lock_open" spinner="verifyPassword" />
|
|
</x-slot:actions>
|
|
</x-form>
|
|
</x-card>
|
|
@else
|
|
{{-- A download link does not render the page again: the download limit's note switches on
|
|
the first press of any download or "Show text", and the server draws the open window on
|
|
the next visit. --}}
|
|
<x-stack gap="space200" x-data="{ downloaded: false }">
|
|
{{-- The text is fetched only when the recipient asks for it, never with the page: a link
|
|
preview must not use up a share limited to one download. It stays in Alpine and is
|
|
drawn as plain text. --}}
|
|
@if ($hasText)
|
|
<x-card :title="__('Private text')" heading="h2" variant="outlined">
|
|
<x-stack
|
|
gap="space200"
|
|
x-data="{
|
|
text: null,
|
|
async reveal() {
|
|
const text = await $wire.revealText();
|
|
|
|
if (text === null) {
|
|
window.materialToast({{ \Illuminate\Support\Js::from(__('This text is no longer available.')) }}, { type: 'error' });
|
|
|
|
return;
|
|
}
|
|
|
|
this.text = text;
|
|
downloaded = true;
|
|
},
|
|
}"
|
|
>
|
|
<template x-if="text !== null">
|
|
<x-stack gap="space200">
|
|
<div class="share-text md-type-body-lg" x-text="text" data-test="shared-text"></div>
|
|
|
|
<x-row justify="end">
|
|
<x-button
|
|
:label="__('Copy')"
|
|
icon="content_copy"
|
|
variant="tonal"
|
|
x-on:click="navigator.clipboard.writeText(text).then(() => window.materialToast({{ \Illuminate\Support\Js::from(__('Copied to the clipboard')) }}, { type: 'success' }))"
|
|
data-test="copy-text"
|
|
/>
|
|
</x-row>
|
|
</x-stack>
|
|
</template>
|
|
|
|
<x-row justify="end" x-show="text === null">
|
|
<x-button :label="__('Show text')" icon="visibility" variant="filled" x-on:click="reveal()" spinner="revealText" data-test="show-text" />
|
|
</x-row>
|
|
</x-stack>
|
|
</x-card>
|
|
@endif
|
|
|
|
@if ($files->isNotEmpty())
|
|
{{-- Which files this browser has downloaded is remembered per share (resources/js/share-downloader.js),
|
|
and where the browser can write to a folder, "Download all to a folder" fetches them one at a
|
|
time, each resumable, which a single ZIP of a large share is not. --}}
|
|
<x-card
|
|
:title="__('Shared Files')"
|
|
:subtitle="trans_choice('{1} :count file, :size|[2,*] :count files, :size', $files->count(), ['size' => Number::fileSize($files->sum('file_size'))])"
|
|
heading="h2"
|
|
variant="outlined"
|
|
>
|
|
<x-stack
|
|
gap="space200"
|
|
x-data="shareDownloader({
|
|
token: {{ \Illuminate\Support\Js::from($share->token) }},
|
|
files: {{ \Illuminate\Support\Js::from($downloads) }},
|
|
messages: {{ \Illuminate\Support\Js::from([
|
|
'progress' => __(':saved of :count files, :received of :total'),
|
|
'gone' => __('This share is no longer available.'),
|
|
]) }},
|
|
})"
|
|
x-on:beforeunload.window="warnBeforeLeaving($event)"
|
|
>
|
|
<div class="share-file-list" data-test="share-file-list">
|
|
<x-list :label="__('Shared Files')">
|
|
@foreach ($files as $file)
|
|
<x-list-item
|
|
:title="$file->relative_path ?: $file->original_name"
|
|
icon="description"
|
|
wire:key="file-{{ $file->id }}"
|
|
>
|
|
<x-slot:description><span class="md-tabular">{{ Number::fileSize($file->file_size) }}</span></x-slot:description>
|
|
<x-slot:end>
|
|
<x-row gap="space100">
|
|
{{-- The file the folder download is on; its row, always shown, is brought into view in the list as it starts. --}}
|
|
<span
|
|
x-show="currentId === {{ $file->id }}"
|
|
x-effect="if (currentId === {{ $file->id }}) $el.closest('[data-md-list-item]').scrollIntoView({ block: 'nearest' })"
|
|
x-cloak
|
|
data-test="file-downloading"
|
|
>
|
|
<x-loading size="24" :label="__('Downloading')" />
|
|
</span>
|
|
<span x-show="isDone({{ $file->id }}) && currentId !== {{ $file->id }}" x-cloak data-test="file-downloaded">
|
|
<x-icon name="download_done" class="md-ink-success" :label="__('Downloaded')" />
|
|
</span>
|
|
<x-button icon="download" :link="route('share.download.file', [$share, $file])" no-wire-navigate :aria-label="__('Download :name', ['name' => $file->original_name])" x-on:click="downloaded = true; markDone({{ $file->id }})" />
|
|
</x-row>
|
|
</x-slot:end>
|
|
</x-list-item>
|
|
@endforeach
|
|
</x-list>
|
|
</div>
|
|
|
|
<div x-show="running || finished" x-cloak data-test="folder-download">
|
|
<x-stack gap="space100">
|
|
<x-row justify="between">
|
|
<span class="md-type-label-lg md-tabular" x-text="status"></span>
|
|
<x-button :label="__('Cancel')" size="xs" x-show="running" x-on:click="cancel()" />
|
|
</x-row>
|
|
<x-progress bind="progress" :label="__('Downloading')" />
|
|
<p class="md-type-body-sm md-ink-variant" x-show="running" x-text="current"></p>
|
|
<p class="md-type-body-sm md-ink-success" x-show="finished && failed.length === 0">{{ __('All files are in the folder.') }}</p>
|
|
</x-stack>
|
|
</div>
|
|
|
|
<div x-show="error" x-cloak>
|
|
<x-alert color="error" assertive><span x-text="error"></span></x-alert>
|
|
</div>
|
|
|
|
<div x-show="! running && failed.length > 0" x-cloak>
|
|
<x-alert color="error" assertive>
|
|
{{ __('Files that could not be downloaded:') }} <span class="md-tabular" x-text="failed.length"></span>
|
|
<x-slot:actions><x-button :label="__('Retry')" x-on:click="retry()" /></x-slot:actions>
|
|
</x-alert>
|
|
</div>
|
|
|
|
<x-row justify="end" gap="space100" wrap x-show="! running">
|
|
@if ($files->count() > 1)
|
|
<x-button :label="__('Download All as ZIP')" icon="folder_zip" variant="tonal" :link="route('share.download.all', $share)" no-wire-navigate x-show="supported" x-cloak x-on:click="downloaded = true" />
|
|
<x-button :label="__('Download all to a folder')" icon="download" variant="filled" x-show="supported" x-cloak x-on:click="downloadAll()" data-test="download-to-folder" />
|
|
<x-button :label="__('Download All as ZIP')" icon="download" variant="filled" :link="route('share.download.all', $share)" no-wire-navigate x-show="! supported" x-on:click="downloaded = true" />
|
|
@else
|
|
<x-button :label="__('Download')" icon="download" variant="filled" :link="route('share.download.file', [$share, $files->first()])" no-wire-navigate x-on:click="downloaded = true; markDone({{ $files->first()->id }})" />
|
|
@endif
|
|
</x-row>
|
|
</x-stack>
|
|
</x-card>
|
|
@endif
|
|
|
|
@if ($share->expires_at || $share->max_downloads)
|
|
<x-stack gap="space100">
|
|
@if ($share->expires_at)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('Expires') }}: {{ $share->expires_at->diffForHumans() }}</p>
|
|
@endif
|
|
|
|
@if ($share->max_downloads)
|
|
@if ($hasText)
|
|
@if ($downloadWindowEndsAt)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('You can open this share for another :time.', ['time' => $downloadWindowEndsAt->diffForHumans(syntax: \Carbon\CarbonInterface::DIFF_ABSOLUTE)]) }}</p>
|
|
@elseif ($remainingDownloads > 0)
|
|
<p class="md-type-body-sm md-ink-variant" x-show="! downloaded">{{ trans_choice('{1} Showing the text or downloading uses the last remaining download. You then have :window to open this share.|[2,*] Showing the text or downloading uses 1 of :count remaining downloads. You then have :window to open this share.', $remainingDownloads, ['window' => $downloadWindow]) }}</p>
|
|
<p class="md-type-body-sm md-ink-variant" x-show="downloaded" x-cloak>{{ __('You have :window to open this share.', ['window' => $downloadWindow]) }}</p>
|
|
@endif
|
|
@elseif ($downloadWindowEndsAt)
|
|
<p class="md-type-body-sm md-ink-variant">{{ __('You can download these files for another :time.', ['time' => $downloadWindowEndsAt->diffForHumans(syntax: \Carbon\CarbonInterface::DIFF_ABSOLUTE)]) }}</p>
|
|
@elseif ($remainingDownloads > 0)
|
|
<p class="md-type-body-sm md-ink-variant" x-show="! downloaded">{{ trans_choice('{1} Downloading uses the last remaining download. You then have :window to download the files.|[2,*] Downloading uses 1 of :count remaining downloads. You then have :window to download the files.', $remainingDownloads, ['window' => $downloadWindow]) }}</p>
|
|
<p class="md-type-body-sm md-ink-variant" x-show="downloaded" x-cloak>{{ __('You have :window to download the files.', ['window' => $downloadWindow]) }}</p>
|
|
@endif
|
|
@endif
|
|
</x-stack>
|
|
@endif
|
|
</x-stack>
|
|
@endif
|
|
</x-page>
|