A share with hundreds of files made the download page as tall as its list, so the recipient scrolled far past the buttons. The list now sits in .share-file-list, capped at min(32rem, 60dvh) with its own scroll, as the upload page's selected-files list already is; the download buttons stay on the first screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
12 KiB
PHP
181 lines
12 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">
|
|
<span x-show="isDone({{ $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>
|