Offer a new share as a QR code and through the share sheet
The share created page gets "Show QR code", a dialog with the link as a QR code (black on white, full screen on a phone) that downloads as a PNG drawn in the browser, with a password reminder for protected shares; and "Share…", which opens the device's share sheet where there is one. Both carry only the link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
fbe358b2e5
commit
1352533667
@@ -1,2 +1,3 @@
|
||||
// Livewire Material. Alpine is bundled and started by Livewire 4: never import it here as well.
|
||||
import '../../vendor/nonameweb/livewire-material/resources/js/material.js'
|
||||
import './share-created.js'
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* `shareActions`: the ways the share created page hands a share over besides copying its link —
|
||||
* the device's share sheet, where the browser has one, and the QR code in its dialog saved as a
|
||||
* PNG. Both carry only the link.
|
||||
*
|
||||
* The PNG is drawn in the browser from the dialog's SVG, which is 1024 pixels square, so the
|
||||
* server needs no image extension and every browser rasterises it at full size.
|
||||
*/
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.data('shareActions', ({ url, title, filename, messages }) => ({
|
||||
open: false,
|
||||
|
||||
canShare: typeof navigator.share === 'function',
|
||||
|
||||
async share() {
|
||||
try {
|
||||
await navigator.share({ title, url })
|
||||
} catch (error) {
|
||||
if (error?.name !== 'AbortError') {
|
||||
window.materialToast(messages.shareFailed, { type: 'error' })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async downloadQrCode(svg) {
|
||||
try {
|
||||
const png = await rasterise(svg)
|
||||
const link = document.createElement('a')
|
||||
const href = URL.createObjectURL(png)
|
||||
|
||||
link.href = href
|
||||
link.download = filename
|
||||
link.click()
|
||||
|
||||
setTimeout(() => URL.revokeObjectURL(href), 0)
|
||||
} catch {
|
||||
window.materialToast(messages.downloadFailed, { type: 'error' })
|
||||
}
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* The SVG element as a PNG blob, at the SVG's own width and height, on white.
|
||||
*/
|
||||
async function rasterise(svg) {
|
||||
const width = Number(svg.getAttribute('width'))
|
||||
const height = Number(svg.getAttribute('height'))
|
||||
const source = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(svg)], { type: 'image/svg+xml' }))
|
||||
|
||||
try {
|
||||
const image = new Image()
|
||||
image.src = source
|
||||
await image.decode()
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
context.imageSmoothingEnabled = false
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, width, height)
|
||||
context.drawImage(image, 0, 0, width, height)
|
||||
|
||||
const png = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'))
|
||||
|
||||
if (!png) {
|
||||
throw new Error('The canvas gave no image.')
|
||||
}
|
||||
|
||||
return png
|
||||
} finally {
|
||||
URL.revokeObjectURL(source)
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,51 @@
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4">
|
||||
<x-input
|
||||
:label="__('Share Link')"
|
||||
:value="route('share.download', $share)"
|
||||
readonly
|
||||
copyable
|
||||
icon="link"
|
||||
data-test="share-link"
|
||||
/>
|
||||
{{-- Besides the link: a QR code in a dialog, saved as a PNG in the browser, and the device's
|
||||
share sheet where there is one (resources/js/share-created.js). Both carry the link only. --}}
|
||||
<div
|
||||
x-data="shareActions({
|
||||
url: @js($shareUrl),
|
||||
title: @js($siteTitle),
|
||||
filename: @js('share-'.$share->token.'.png'),
|
||||
messages: @js(['shareFailed' => __('The share sheet could not open.'), 'downloadFailed' => __('The QR code could not be saved.')]),
|
||||
})"
|
||||
class="grid gap-3"
|
||||
data-test="share-actions"
|
||||
>
|
||||
<x-input
|
||||
:label="__('Share Link')"
|
||||
:value="$shareUrl"
|
||||
readonly
|
||||
copyable
|
||||
icon="link"
|
||||
data-test="share-link"
|
||||
/>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-button :label="__('Show QR code')" icon="qr_code_2" variant="tonal" x-on:click="open = true" data-test="show-qr-code" />
|
||||
|
||||
<span x-show="canShare" x-cloak class="inline-flex">
|
||||
<x-button :label="__('Share…')" icon="share" variant="tonal" x-on:click="share()" data-test="share-sheet" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-modal fullscreen :title="__('Scan to open the share')" data-test="qr-code-dialog">
|
||||
{{-- White in either theme: a scanner needs the contrast. The SVG is drawn from the app's own URL. --}}
|
||||
<div data-qr-code class="mx-auto aspect-square w-full max-w-80 rounded-corner-lg bg-white p-2 [&>svg]:size-full">{!! $qrCodeSvg !!}</div>
|
||||
|
||||
@if ($share->isPasswordProtected())
|
||||
<div class="mt-4">
|
||||
<x-alert color="info" icon="lock" :title="__('Recipients also need the password.')" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-slot:actions>
|
||||
<x-button :label="__('Close')" x-on:click="close()" />
|
||||
<x-button :label="__('Download')" icon="download" variant="tonal" x-on:click="downloadQrCode($el.closest('dialog').querySelector('[data-qr-code] svg'))" data-test="download-qr-code" />
|
||||
</x-slot:actions>
|
||||
</x-modal>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<x-stat :title="__('Files')" :value="$share->files->count()" icon="description" />
|
||||
|
||||
Reference in New Issue
Block a user