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">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { useQrCode, type QrOptions } from './composables/useQrCode'
|
import { useQrCode, type QrOptions } from './composables/useQrCode'
|
||||||
|
|
||||||
const preview = ref<HTMLDivElement | null>(null)
|
const preview = ref<HTMLDivElement | null>(null)
|
||||||
@@ -20,7 +20,25 @@ const options = reactive<QrOptions>({
|
|||||||
hideBackgroundDots: true,
|
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 dotTypes = ['square', 'dots', 'rounded', 'classy', 'classy-rounded', 'extra-rounded'] as const
|
||||||
const cornerSquareTypes = ['square', 'dot', 'extra-rounded'] as const
|
const cornerSquareTypes = ['square', 'dot', 'extra-rounded'] as const
|
||||||
@@ -146,6 +164,14 @@ function clearLogo() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="download">
|
<div class="download">
|
||||||
<button type="button" @click="download('png')">Download PNG</button>
|
<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>
|
<button type="button" @click="download('svg')">Download SVG</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -40,7 +40,14 @@ export function useQrCode(container: Ref<HTMLElement | null>, options: QrOptions
|
|||||||
qr.value?.download({ name: 'qrcode', extension })
|
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) {
|
function toLibOptions(o: QrOptions) {
|
||||||
|
|||||||
@@ -203,10 +203,16 @@ input[type="file"] {
|
|||||||
|
|
||||||
.download {
|
.download {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.download {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -240,3 +246,25 @@ button.ghost:hover {
|
|||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #1e293b;
|
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