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
77 lines
2.4 KiB
JavaScript
77 lines
2.4 KiB
JavaScript
/**
|
|
* `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)
|
|
}
|
|
}
|