feat: add copy PNG to clipboard button
New button next to Download PNG writes the rendered QR as a PNG Blob to the system clipboard via the async Clipboard API, with a brief in-button confirmation (Copied!) or error state.
This commit is contained in:
30
src/App.vue
30
src/App.vue
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useQrCode, type QrOptions } from './composables/useQrCode'
|
||||
|
||||
const preview = ref<HTMLDivElement | null>(null)
|
||||
@@ -20,7 +20,25 @@ const options = reactive<QrOptions>({
|
||||
hideBackgroundDots: true,
|
||||
})
|
||||
|
||||
const { download } = useQrCode(preview, options)
|
||||
const { download, copyPng } = useQrCode(preview, options)
|
||||
|
||||
const copyState = ref<'idle' | 'copied' | 'error'>('idle')
|
||||
let copyTimer: number | undefined
|
||||
|
||||
async function onCopyPng() {
|
||||
try {
|
||||
await copyPng()
|
||||
copyState.value = 'copied'
|
||||
} catch {
|
||||
copyState.value = 'error'
|
||||
}
|
||||
window.clearTimeout(copyTimer)
|
||||
copyTimer = window.setTimeout(() => (copyState.value = 'idle'), 1800)
|
||||
}
|
||||
|
||||
const copyLabel = computed(() =>
|
||||
copyState.value === 'copied' ? 'Copied!' : copyState.value === 'error' ? 'Copy failed' : 'Copy PNG',
|
||||
)
|
||||
|
||||
const dotTypes = ['square', 'dots', 'rounded', 'classy', 'classy-rounded', 'extra-rounded'] as const
|
||||
const cornerSquareTypes = ['square', 'dot', 'extra-rounded'] as const
|
||||
@@ -146,6 +164,14 @@ function clearLogo() {
|
||||
</div>
|
||||
<div class="download">
|
||||
<button type="button" @click="download('png')">Download PNG</button>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:class="{ copied: copyState === 'copied', errored: copyState === 'error' }"
|
||||
@click="onCopyPng"
|
||||
>
|
||||
{{ copyLabel }}
|
||||
</button>
|
||||
<button type="button" @click="download('svg')">Download SVG</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -40,7 +40,14 @@ export function useQrCode(container: Ref<HTMLElement | null>, options: QrOptions
|
||||
qr.value?.download({ name: 'qrcode', extension })
|
||||
}
|
||||
|
||||
return { download }
|
||||
const copyPng = async () => {
|
||||
if (!qr.value) throw new Error('QR not ready')
|
||||
const data = await qr.value.getRawData('png')
|
||||
if (!(data instanceof Blob)) throw new Error('Unexpected PNG output')
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': data })])
|
||||
}
|
||||
|
||||
return { download, copyPng }
|
||||
}
|
||||
|
||||
function toLibOptions(o: QrOptions) {
|
||||
|
||||
@@ -203,10 +203,16 @@ input[type="file"] {
|
||||
|
||||
.download {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.download {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
@@ -240,3 +246,25 @@ button.ghost:hover {
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #fff;
|
||||
color: #4f46e5;
|
||||
border-color: #c7d2fe;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
button.secondary.copied {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
border-color: #6ee7b7;
|
||||
}
|
||||
|
||||
button.secondary.errored {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user