This commit is contained in:
sharevb 2024-08-08 18:20:11 +05:30 committed by GitHub
commit 205760a042
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 425 additions and 54 deletions

6
components.d.ts vendored
View file

@ -127,24 +127,26 @@ declare module '@vue/runtime-core' {
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode'] NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
NLabel: typeof import('naive-ui')['NLabel']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpin: typeof import('naive-ui')['NSpin']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']

View file

@ -80,7 +80,9 @@
"pdf-signature-reader": "^1.4.2", "pdf-signature-reader": "^1.4.2",
"pinia": "^2.0.34", "pinia": "^2.0.34",
"plausible-tracker": "^0.3.8", "plausible-tracker": "^0.3.8",
"pp-qr-code": "^0.6.3",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"qrcode-terminal-nooctal": "^0.12.1",
"sql-formatter": "^13.0.0", "sql-formatter": "^13.0.0",
"ua-parser-js": "^1.0.35", "ua-parser-js": "^1.0.35",
"ulid": "^2.3.0", "ulid": "^2.3.0",

40
pnpm-lock.yaml generated
View file

@ -140,9 +140,15 @@ dependencies:
plausible-tracker: plausible-tracker:
specifier: ^0.3.8 specifier: ^0.3.8
version: 0.3.8 version: 0.3.8
pp-qr-code:
specifier: ^0.6.3
version: 0.6.4
qrcode: qrcode:
specifier: ^1.5.1 specifier: ^1.5.1
version: 1.5.1 version: 1.5.1
qrcode-terminal-nooctal:
specifier: ^0.12.1
version: 0.12.1
sql-formatter: sql-formatter:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -3354,7 +3360,7 @@ packages:
dependencies: dependencies:
'@unhead/dom': 0.5.1 '@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1 '@unhead/schema': 0.5.1
'@vueuse/shared': 10.8.0(vue@3.3.4) '@vueuse/shared': 10.11.0(vue@3.3.4)
unhead: 0.5.1 unhead: 0.5.1
vue: 3.3.4 vue: 3.3.4
transitivePeerDependencies: transitivePeerDependencies:
@ -3987,19 +3993,19 @@ packages:
- vue - vue
dev: false dev: false
/@vueuse/shared@10.3.0(vue@3.3.4): /@vueuse/shared@10.11.0(vue@3.3.4):
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
dependencies: dependencies:
vue-demi: 0.14.5(vue@3.3.4) vue-demi: 0.14.8(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
dev: false dev: false
/@vueuse/shared@10.8.0(vue@3.3.4): /@vueuse/shared@10.3.0(vue@3.3.4):
resolution: {integrity: sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==} resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
dependencies: dependencies:
vue-demi: 0.14.7(vue@3.3.4) vue-demi: 0.14.5(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -7470,6 +7476,12 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
/pp-qr-code@0.6.4:
resolution: {integrity: sha512-6OhfzsFgZClbBh3XzfwysSwH4injljeMreKaZyxWydthkhXAdjSbFfEtrvfHt1SN65PmAHEOux+dOVvJRiOaDw==}
dependencies:
qrcode-generator: 1.4.4
dev: false
/prelude-ls@1.2.1: /prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -7680,6 +7692,15 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/qrcode-generator@1.4.4:
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
dev: false
/qrcode-terminal-nooctal@0.12.1:
resolution: {integrity: sha512-jy/kkD0iIMDjTucB+5T6KBsnirlhegDH47vHgrj5MejchSQmi/EAMM0xMFeePgV9CJkkAapNakpVUWYgHvtdKg==}
hasBin: true
dev: false
/qrcode@1.5.1: /qrcode@1.5.1:
resolution: {integrity: sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==} resolution: {integrity: sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -9158,8 +9179,8 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-demi@0.14.7(vue@3.3.4): /vue-demi@0.14.8(vue@3.3.4):
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
@ -9449,6 +9470,7 @@ packages:
/workbox-google-analytics@7.0.0: /workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
dependencies: dependencies:
workbox-background-sync: 7.0.0 workbox-background-sync: 7.0.0
workbox-core: 7.0.0 workbox-core: 7.0.0

View file

@ -1,6 +1,7 @@
import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types'; import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
import type { Ref } from 'vue'; import type { MaybeRef, Ref } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
import { get } from '@vueuse/core';
export { export {
getMimeTypeFromBase64, getMimeTypeFromBase64,
@ -75,21 +76,11 @@ function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
} }
function useDownloadFileFromBase64( function useDownloadFileFromBase64(
{ source, filename, extension, fileMimeType }:
{ source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
},
};
}
function useDownloadFileFromBase64Refs(
{ source, filename, extension }: { source, filename, extension }:
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) { { source: MaybeRef<string>; filename?: MaybeRef<string>; extension?: MaybeRef<string> }) {
return { return {
download() { download() {
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value }); downloadFromBase64({ sourceValue: get(source), filename: get(filename), extension: get(extension) });
}, },
}; };
} }
@ -116,3 +107,13 @@ function previewImageFromBase64(base64String: string): HTMLImageElement {
return img; return img;
} }
function useDownloadFileFromBase64Refs(
{ source, filename, extension }:
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
return {
download() {
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
},
};
}

View file

@ -1,7 +1,8 @@
import { useRouteQuery } from '@vueuse/router'; import { useRouteQuery } from '@vueuse/router';
import { computed } from 'vue'; import { computed } from 'vue';
import { useStorage } from '@vueuse/core';
export { useQueryParam }; export { useQueryParam, useQueryParamOrStorage };
const transformers = { const transformers = {
number: { number: {
@ -16,6 +17,12 @@ const transformers = {
fromQuery: (value: string) => value.toLowerCase() === 'true', fromQuery: (value: string) => value.toLowerCase() === 'true',
toQuery: (value: boolean) => (value ? 'true' : 'false'), toQuery: (value: boolean) => (value ? 'true' : 'false'),
}, },
object: {
fromQuery: (value: string) => {
return JSON.parse(value);
},
toQuery: (value: object) => JSON.stringify(value),
},
}; };
function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) { function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
@ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
}, },
}); });
} }
function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
const type = typeof defaultValue;
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
const storageRef = useStorage(storageName, defaultValue);
const proxyDefaultValue = transformer.toQuery(defaultValue as never);
const proxy = useRouteQuery(name, proxyDefaultValue);
const r = ref(defaultValue);
watch(r,
(value) => {
proxy.value = transformer.toQuery(value as never);
storageRef.value = value as never;
},
{ deep: true });
r.value = (proxy.value && proxy.value !== proxyDefaultValue
? transformer.fromQuery(proxy.value) as unknown as T
: storageRef.value as T) as never;
return r;
}

View file

@ -1,26 +1,89 @@
<script setup lang="ts"> <script setup lang="ts">
import type { QRCodeErrorCorrectionLevel } from 'qrcode'; import type {
import { useQRCode } from './useQRCode'; CornerDotType,
CornerSquareType,
DotType,
ErrorCorrectionLevel,
FileExtension,
} from 'pp-qr-code';
import qrcodeConsole from 'qrcode-terminal-nooctal';
import { useQRCodeStyling } from './useQRCode';
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
import { useQueryParamOrStorage } from '@/composable/queryParams';
const foreground = ref('#000000ff'); const foreground = useQueryParamOrStorage({ name: 'fg', storageName: 'qr-code-gen:fg', defaultValue: '#000000ff' });
const background = ref('#ffffffff'); const background = useQueryParamOrStorage({ name: 'bg', storageName: 'qr-code-gen:bg', defaultValue: '#ffffffff' });
const errorCorrectionLevel = ref<QRCodeErrorCorrectionLevel>('medium'); const errorCorrectionLevelSelectValue = useQueryParamOrStorage<string>({ name: 'level', storageName: 'qr-code-gen:level', defaultValue: 'medium' });
const errorCorrectionLevel = computed(() => errorCorrectionLevelSelectValue.value.toString()[0].toUpperCase() as ErrorCorrectionLevel);
const width = useQueryParamOrStorage({ name: 'width', storageName: 'qr-code-gen:width', defaultValue: 1024 });
const margin = useQueryParamOrStorage({ name: 'margin', storageName: 'qr-code-gen:margin', defaultValue: 10 });
const imageSize = useQueryParamOrStorage({ name: 'imgsize', storageName: 'qr-code-gen:imsz', defaultValue: 0.4 });
const imageMargin = useQueryParamOrStorage({ name: 'imgmargin', storageName: 'qr-code-gen:immg', defaultValue: 20 });
const outputType = useQueryParamOrStorage<FileExtension>({ name: 'out', storageName: 'qr-code-gen:out', defaultValue: 'png' });
const dotType = useQueryParamOrStorage<DotType>({ name: 'dot', storageName: 'qr-code-gen:dot', defaultValue: 'square' });
const dotColor = useQueryParamOrStorage<string>({ name: 'dotc', storageName: 'qr-code-gen:dotc', defaultValue: '#ffffffff' });
const cornersDotType = useQueryParamOrStorage<CornerDotType>({ name: 'cdt', storageName: 'qr-code-gen:cdt', defaultValue: 'square' });
const cornersDotColor = useQueryParamOrStorage<string>({ name: 'cdtc', storageName: 'qr-code-gen:cdtc', defaultValue: '#ffffffff' });
const cornersSquareType = useQueryParamOrStorage<CornerSquareType>({ name: 'cst', storageName: 'qr-code-gen:cst', defaultValue: 'square' });
const cornersSquareColor = useQueryParamOrStorage<string>({ name: 'cstc', storageName: 'qr-code-gen:cstc', defaultValue: '#ffffffff' });
const smallTerminal = useQueryParamOrStorage<boolean>({ name: 'sml', storageName: 'qr-code-gen:sml', defaultValue: false });
const fileInput = ref() as Ref<File>;
const { base64: imageBase64 } = useBase64(fileInput);
async function onUpload(file: File) {
if (file) {
fileInput.value = file;
}
}
const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high']; const errorCorrectionLevels = ['low', 'medium', 'quartile', 'high'];
const outputTypes = ['svg', 'png', 'jpeg', 'webp'];
const dotTypes = ['dots',
'random-dots',
'rounded',
'vertical-lines',
'horizontal-lines',
'classy',
'classy-rounded',
'square',
'extra-rounded'];
const cornersDotTypes = ['dot', 'square', 'heart'];
const cornersSquareTypes = ['dot', 'square', 'extra-rounded'];
const text = ref('https://it-tools.tech'); const text = ref('https://it-tools.tech');
const { qrcode } = useQRCode({ const { qrcode } = useQRCodeStyling({
text, text,
color: { color: { background, foreground },
background,
foreground,
},
errorCorrectionLevel, errorCorrectionLevel,
options: { width: 1024 }, imageBase64,
imageOptions: { imageSize, margin: imageMargin },
dotOptions: { type: dotType, color: dotColor },
cornersSquareOptions: { type: cornersSquareType, color: cornersSquareColor },
cornersDotOptions: { type: cornersDotType, color: cornersDotColor },
outputType,
width,
margin,
}); });
const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' }); const qrcodeTerminal = computedAsync(() => {
const textValue = text.value;
const level = errorCorrectionLevel.value;
const small = smallTerminal.value;
return new Promise<string>((resolve, _reject) => {
try {
qrcodeConsole.setErrorLevel(level);
qrcodeConsole.generate(textValue, { small }, (qrcode: string) => {
resolve(qrcode);
});
}
catch (_) {
resolve('');
}
});
});
const filename = ref('qr-code');
const extension = computed(() => outputType.value.toString());
const { download } = useDownloadFileFromBase64({ source: qrcode, filename, extension });
</script> </script>
<template> <template>
@ -46,8 +109,14 @@ const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-c
<n-form-item label="Background color:"> <n-form-item label="Background color:">
<n-color-picker v-model:value="background" :modes="['hex']" /> <n-color-picker v-model:value="background" :modes="['hex']" />
</n-form-item> </n-form-item>
<n-form-item label="Width:">
<n-input-number v-model:value="width" :min="0" />
</n-form-item>
<n-form-item label="Margin:">
<n-input-number v-model:value="margin" :min="0" />
</n-form-item>
<c-select <c-select
v-model:value="errorCorrectionLevel" v-model:value="errorCorrectionLevelSelectValue"
label="Error resistance:" label="Error resistance:"
label-position="left" label-position="left"
label-width="130px" label-width="130px"
@ -55,14 +124,104 @@ const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-c
:options="errorCorrectionLevels.map((value) => ({ label: value, value }))" :options="errorCorrectionLevels.map((value) => ({ label: value, value }))"
/> />
</n-form> </n-form>
<c-card title="Image" mt-3>
<c-file-upload title="Drag and drop an image here, or click to select an image" @file-upload="onUpload" />
<n-form label-width="130" label-placement="left" mt-3>
<n-form-item label="Size:">
<n-input-number v-model:value="imageSize" :min="0" step="0.1" />
</n-form-item>
<n-form-item label="Margin:">
<n-input-number v-model:value="imageMargin" :min="0" />
</n-form-item>
</n-form>
</c-card>
<c-card mt-3>
<details>
<summary>Dots Options</summary>
<n-form label-width="130" label-placement="left">
<n-form-item label="Color:">
<n-color-picker v-model:value="dotColor" :modes="['hex']" />
</n-form-item>
<c-select
v-model:value="dotType"
label="Type:"
label-position="left"
label-width="130px"
label-align="right"
:options="dotTypes.map((value) => ({ label: value, value }))"
/>
</n-form>
</details>
</c-card>
<c-card mt-3>
<details>
<summary>Corners Dots Options</summary>
<n-form label-width="130" label-placement="left">
<n-form-item label="Color:">
<n-color-picker v-model:value="cornersDotColor" :modes="['hex']" />
</n-form-item>
<c-select
v-model:value="cornersDotType"
label="Type:"
label-position="left"
label-width="130px"
label-align="right"
:options="cornersDotTypes.map((value) => ({ label: value, value }))"
/>
</n-form>
</details>
</c-card>
<c-card mt-3>
<details>
<summary>Corners Square Options</summary>
<n-form label-width="130" label-placement="left">
<n-form-item label="Color:">
<n-color-picker v-model:value="cornersSquareColor" :modes="['hex']" />
</n-form-item>
<c-select
v-model:value="cornersSquareType"
label="Type:"
label-position="left"
label-width="130px"
label-align="right"
:options="cornersSquareTypes.map((value) => ({ label: value, value }))"
/>
</n-form>
</details>
</c-card>
<c-select
v-model:value="outputType"
mt-3
label="Output format:"
label-position="left"
label-width="130px"
label-align="right"
:options="outputTypes.map((value) => ({ label: value.toUpperCase(), value }))"
/>
</n-gi> </n-gi>
<n-gi> <n-gi>
<div flex flex-col items-center gap-3> <div flex flex-col items-center gap-3>
<n-image :src="qrcode" width="200" /> <n-image :src="qrcode" width="200" />
<c-button @click="download"> <c-button @click="download">
Download qr-code Download qr-code ({{ outputType.toString().toUpperCase() }})
</c-button> </c-button>
</div> </div>
<n-divider />
<n-checkbox v-model:checked="smallTerminal">
Small Terminal
</n-checkbox>
<n-form-item label="Terminal output:" mt-1>
<TextareaCopyable
:value="qrcodeTerminal"
multiline
rows="5"
mb-1 mt-1
copy-placement="outside"
/>
</n-form-item>
</n-gi> </n-gi>
</n-grid> </n-grid>
</c-card> </c-card>

View file

@ -0,0 +1,5 @@
declare module 'qrcode-terminal-nooctal' {
export const error: 0 | 1 | 2 | 3;
export function generate(input: string, opts?: { small: boolean }, callback?: (qrcode: string) => void): void;
export function setErrorLevel(error: "L" | "M" | "Q" | "H"): void;
}

View file

@ -1,33 +1,182 @@
import { type MaybeRef, get } from '@vueuse/core'; import { type MaybeRef, get } from '@vueuse/core';
import QRCode, { type QRCodeErrorCorrectionLevel, type QRCodeToDataURLOptions } from 'qrcode'; import QRCode, { type QRCodeDataURLType, type QRCodeErrorCorrectionLevel, type QRCodeRenderersOptions, type QRCodeStringType } from 'qrcode';
import { isRef, ref, watch } from 'vue'; import { isRef, ref, watch } from 'vue';
import type {
CornerDotType,
CornerSquareType,
DotType,
DrawType,
ErrorCorrectionLevel,
FileExtension,
Mode,
TypeNumber,
} from 'pp-qr-code';
import QRCodeStyling from 'pp-qr-code';
export function useQRCode({ function blobToBase64(blob: Blob | null): Promise<string> {
if (blob === null) {
return Promise.resolve('');
}
return new Promise((resolve, _reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
}
export function useQRCodeStyling({
text, text,
color: { background, foreground }, color: { background, foreground },
errorCorrectionLevel, errorCorrectionLevel,
options, imageBase64,
imageOptions: { imageSize, margin: imageMargin },
dotOptions: { type: dotType, color: dotColor },
cornersSquareOptions: { type: cornersSquareType, color: cornersSquareColor },
cornersDotOptions: { type: cornersDotType, color: cornersDotColor },
outputType,
width,
margin,
}: { }: {
text: MaybeRef<string> text: MaybeRef<string>
color: { foreground: MaybeRef<string>; background: MaybeRef<string> } color: { foreground: MaybeRef<string>; background: MaybeRef<string> }
errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel> outputType: MaybeRef<FileExtension>
options?: QRCodeToDataURLOptions errorCorrectionLevel?: MaybeRef<ErrorCorrectionLevel>
imageBase64?: MaybeRef<string>
imageOptions: { imageSize: MaybeRef<number>; margin: MaybeRef<number> }
dotOptions: { type: MaybeRef<DotType>; color: MaybeRef<string> }
cornersSquareOptions: { type: MaybeRef<CornerSquareType>; color: MaybeRef<string> }
cornersDotOptions: { type: MaybeRef<CornerDotType>; color: MaybeRef<string> }
width?: MaybeRef<number>
margin?: MaybeRef<number>
}) { }) {
const qrcode = ref(''); const qrcode = ref('');
watch( watch(
[text, background, foreground, errorCorrectionLevel].filter(isRef), [text, background, foreground, errorCorrectionLevel,
imageBase64, imageSize, imageMargin,
dotType, dotColor, cornersSquareType, cornersSquareColor,
cornersDotType, cornersDotColor,
outputType, width, margin].filter(isRef),
async () => { async () => {
if (get(text)) { const qrCodeText = get(text)?.trim();
qrcode.value = await QRCode.toDataURL(get(text).trim(), { const qrCodeOutputType = get(outputType) || 'png';
color: { if (qrCodeText) {
dark: get(foreground), const getOrForeground = (colorRef: MaybeRef<string>) => {
light: get(background), const color = get(colorRef) || get(foreground);
...options?.color, if (color?.toLowerCase() === '#ffffffff') {
return get(foreground);
}
return color;
};
const qrCodeOptions = {
width: get(width) ?? 1024,
height: get(width) ?? 1024,
type: 'svg' as DrawType,
data: qrCodeText,
image: get(imageBase64),
margin: get(margin) || 10,
qrOptions: {
typeNumber: 0 as TypeNumber,
mode: 'Byte' as Mode,
errorCorrectionLevel: get(errorCorrectionLevel) || 'Q',
}, },
errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M', imageOptions: {
...options, hideBackgroundDots: true,
}); imageSize: get(imageSize) || 0.4,
margin: get(imageMargin) || 20,
crossOrigin: 'anonymous',
},
dotsOptions: {
color: getOrForeground(dotColor),
// gradient: {
// type: 'linear', // 'radial'
// rotation: 0,
// colorStops: [{ offset: 0, color: '#8688B2' }, { offset: 1, color: '#77779C' }]
// },
type: get(dotType) || 'square',
},
backgroundOptions: {
color: get(background),
// gradient: {
// type: 'linear', // 'radial'
// rotation: 0,
// colorStops: [{ offset: 0, color: '#ededff' }, { offset: 1, color: '#e6e7ff' }]
// },
},
cornersSquareOptions: {
color: getOrForeground(cornersSquareColor),
type: get(cornersSquareType) || 'extra-rounded',
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#25456e' }, { offset: 1, color: '#4267b2' }]
// },
},
cornersDotOptions: {
color: getOrForeground(cornersDotColor),
type: get(cornersDotType) || 'dot',
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#00266e' }, { offset: 1, color: '#4060b3' }]
// },
},
};
const qrGenerator = new QRCodeStyling(qrCodeOptions);
qrcode.value = await blobToBase64(await qrGenerator.getRawData(qrCodeOutputType));
}
},
{ immediate: true },
);
return { qrcode };
}
export function useQRCode<QRCodeOptions extends QRCodeRenderersOptions>({
text,
color: { background, foreground },
errorCorrectionLevel,
outputType,
width,
options,
}: {
text: MaybeRef<string>
color: { foreground: MaybeRef<string>; background: MaybeRef<string> }
errorCorrectionLevel?: MaybeRef<QRCodeErrorCorrectionLevel>
outputType?: MaybeRef<QRCodeDataURLType | QRCodeStringType>
width?: MaybeRef<number>
options?: QRCodeOptions
}) {
const qrcode = ref('');
watch(
[text, background, foreground, errorCorrectionLevel, outputType, width].filter(isRef),
async () => {
const qrCodeText = get(text)?.trim();
const qrCodeType = get(outputType) || '';
if (qrCodeText) {
const qrCodeOptions = {
color: {
dark: get(foreground),
light: get(background),
...options?.color,
},
errorCorrectionLevel: get(errorCorrectionLevel) ?? 'M',
width: get(width) ?? 1024,
...options,
};
if (['utf8', 'svg', 'terminal'].includes(qrCodeType)) {
qrcode.value = await QRCode.toString(qrCodeText, {
type: qrCodeType as QRCodeStringType,
});
}
else {
qrcode.value = await QRCode.toDataURL(qrCodeText, {
...qrCodeOptions,
type: qrCodeType as QRCodeDataURLType,
});
}
} }
}, },
{ immediate: true }, { immediate: true },