Merge 880d937d33 into 67094980c9
				
					
				
			This commit is contained in:
		
						commit
						431f5d4965
					
				
							
								
								
									
										3
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -133,6 +133,7 @@ declare module '@vue/runtime-core' { | |||||||
|     NButton: typeof import('naive-ui')['NButton'] |     NButton: typeof import('naive-ui')['NButton'] | ||||||
|     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'] | ||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|     NForm: typeof import('naive-ui')['NForm'] |     NForm: typeof import('naive-ui')['NForm'] | ||||||
| @ -140,6 +141,7 @@ declare module '@vue/runtime-core' { | |||||||
|     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'] | ||||||
|     NLayout: typeof import('naive-ui')['NLayout'] |     NLayout: typeof import('naive-ui')['NLayout'] | ||||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] |     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||||
| @ -154,6 +156,7 @@ declare module '@vue/runtime-core' { | |||||||
|     PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default'] |     PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default'] | ||||||
|     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] |     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] | ||||||
|     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] |     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] | ||||||
|  |     QrCodeDecoder: typeof import('./src/tools/qr-code-decoder/qr-code-decoder.vue')['default'] | ||||||
|     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] |     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] | ||||||
|     RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] |     RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] | ||||||
|     ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default'] |     ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default'] | ||||||
|  | |||||||
| @ -67,6 +67,7 @@ | |||||||
|     "iarna-toml-esm": "^3.0.5", |     "iarna-toml-esm": "^3.0.5", | ||||||
|     "ibantools": "^4.3.3", |     "ibantools": "^4.3.3", | ||||||
|     "js-base64": "^3.7.6", |     "js-base64": "^3.7.6", | ||||||
|  |     "ical.js": "^2.0.1", | ||||||
|     "json5": "^2.2.3", |     "json5": "^2.2.3", | ||||||
|     "jwt-decode": "^3.1.2", |     "jwt-decode": "^3.1.2", | ||||||
|     "libphonenumber-js": "^1.10.28", |     "libphonenumber-js": "^1.10.28", | ||||||
| @ -84,6 +85,7 @@ | |||||||
|     "pinia": "^2.0.34", |     "pinia": "^2.0.34", | ||||||
|     "plausible-tracker": "^0.3.8", |     "plausible-tracker": "^0.3.8", | ||||||
|     "qrcode": "^1.5.1", |     "qrcode": "^1.5.1", | ||||||
|  |     "qrcode-parser": "^2.1.3", | ||||||
|     "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", | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -98,6 +98,9 @@ dependencies: | |||||||
|   ibantools: |   ibantools: | ||||||
|     specifier: ^4.3.3 |     specifier: ^4.3.3 | ||||||
|     version: 4.3.3 |     version: 4.3.3 | ||||||
|  |   ical.js: | ||||||
|  |     specifier: ^2.0.1 | ||||||
|  |     version: 2.0.1 | ||||||
|   js-base64: |   js-base64: | ||||||
|     specifier: ^3.7.6 |     specifier: ^3.7.6 | ||||||
|     version: 3.7.7 |     version: 3.7.7 | ||||||
| @ -152,6 +155,9 @@ dependencies: | |||||||
|   qrcode: |   qrcode: | ||||||
|     specifier: ^1.5.1 |     specifier: ^1.5.1 | ||||||
|     version: 1.5.1 |     version: 1.5.1 | ||||||
|  |   qrcode-parser: | ||||||
|  |     specifier: ^2.1.3 | ||||||
|  |     version: 2.1.3 | ||||||
|   sql-formatter: |   sql-formatter: | ||||||
|     specifier: ^13.0.0 |     specifier: ^13.0.0 | ||||||
|     version: 13.0.0 |     version: 13.0.0 | ||||||
| @ -6116,6 +6122,10 @@ packages: | |||||||
|     resolution: {integrity: sha512-RUTlGuFj3cU/Qfu5YIrsIZjW34/VDgKOz5fDr64Mc4NWP9b2i48vQ39r5xCl1yyFQeyEG/lASstIQHAUX18rRA==} |     resolution: {integrity: sha512-RUTlGuFj3cU/Qfu5YIrsIZjW34/VDgKOz5fDr64Mc4NWP9b2i48vQ39r5xCl1yyFQeyEG/lASstIQHAUX18rRA==} | ||||||
|     dev: false |     dev: false | ||||||
| 
 | 
 | ||||||
|  |   /ical.js@2.0.1: | ||||||
|  |     resolution: {integrity: sha512-uYYb1CwTXbd9NP/xTtgQZ5ivv6bpUjQu9VM98s3X78L3XRu00uJW5ZtmnLwyxhztpf5fSiRyDpFW7ZNCePlaPw==} | ||||||
|  |     dev: false | ||||||
|  | 
 | ||||||
|   /iconv-lite@0.6.3: |   /iconv-lite@0.6.3: | ||||||
|     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} |     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} | ||||||
|     engines: {node: '>=0.10.0'} |     engines: {node: '>=0.10.0'} | ||||||
| @ -6640,6 +6650,10 @@ packages: | |||||||
|     engines: {node: '>=0.10.0'} |     engines: {node: '>=0.10.0'} | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /jsqr@1.4.0: | ||||||
|  |     resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==} | ||||||
|  |     dev: false | ||||||
|  | 
 | ||||||
|   /jwt-decode@3.1.2: |   /jwt-decode@3.1.2: | ||||||
|     resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} |     resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} | ||||||
|     dev: false |     dev: false | ||||||
| @ -7729,6 +7743,12 @@ packages: | |||||||
|     engines: {node: '>=6'} |     engines: {node: '>=6'} | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /qrcode-parser@2.1.3: | ||||||
|  |     resolution: {integrity: sha512-tyakoHUQXCjH1+RGiqxH3/6XqbQuXuSaW0CkUp1AlYT0+XA4ndG7bxxyyWpdnr0Z2Vuv0GRwgKSq6sOzNiQfog==} | ||||||
|  |     dependencies: | ||||||
|  |       jsqr: 1.4.0 | ||||||
|  |     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'} | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import { tool as textToUnicode } from './text-to-unicode'; | |||||||
| import { tool as safelinkDecoder } from './safelink-decoder'; | import { tool as safelinkDecoder } from './safelink-decoder'; | ||||||
| import { tool as xmlToJson } from './xml-to-json'; | import { tool as xmlToJson } from './xml-to-json'; | ||||||
| import { tool as jsonToXml } from './json-to-xml'; | import { tool as jsonToXml } from './json-to-xml'; | ||||||
|  | import { tool as qrCodeDecoder } from './qr-code-decoder'; | ||||||
| import { tool as markdownToHtml } from './markdown-to-html'; | import { tool as markdownToHtml } from './markdown-to-html'; | ||||||
| import { tool as pdfSignatureChecker } from './pdf-signature-checker'; | import { tool as pdfSignatureChecker } from './pdf-signature-checker'; | ||||||
| import { tool as numeronymGenerator } from './numeronym-generator'; | import { tool as numeronymGenerator } from './numeronym-generator'; | ||||||
| @ -139,7 +140,13 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Images and videos', |     name: 'Images and videos', | ||||||
|     components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], |     components: [ | ||||||
|  |       qrCodeGenerator, | ||||||
|  |       wifiQrCodeGenerator, | ||||||
|  |       qrCodeDecoder, | ||||||
|  |       svgPlaceholderGenerator, | ||||||
|  |       cameraRecorder, | ||||||
|  |     ], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Development', |     name: 'Development', | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								src/tools/qr-code-decoder/ical.js.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/tools/qr-code-decoder/ical.js.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | declare module "ical.js" { | ||||||
|  |     export function parse(content: string): object; | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/tools/qr-code-decoder/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/qr-code-decoder/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { Qrcode } from '@vicons/tabler'; | ||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'QR Code decoder', | ||||||
|  |   path: '/qr-code-decoder', | ||||||
|  |   description: 'QR Code Reader', | ||||||
|  |   keywords: ['qrcode', 'qr-code', 'decoder', 'reader'], | ||||||
|  |   component: () => import('./qr-code-decoder.vue'), | ||||||
|  |   icon: Qrcode, | ||||||
|  |   createdAt: new Date('2024-09-01'), | ||||||
|  | }); | ||||||
							
								
								
									
										151
									
								
								src/tools/qr-code-decoder/qr-code-decoder.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/tools/qr-code-decoder/qr-code-decoder.service.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | |||||||
|  | import { describe, expect, test } from 'vitest'; | ||||||
|  | import { parseQRData } from './qr-code-decoder.service'; | ||||||
|  | 
 | ||||||
|  | describe('qr-code-decoder', () => { | ||||||
|  |   test('parseQRData should parse content correctly', () => { | ||||||
|  |     expect(parseQRData(null)).toEqual({ | ||||||
|  |       type: 'Unknown', | ||||||
|  |       value: '', | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('')).toEqual({ | ||||||
|  |       type: 'Unknown', | ||||||
|  |       value: '', | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('TEL:+33123456')).toEqual({ | ||||||
|  |       type: 'Phone', | ||||||
|  |       value: '+33123456', | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('MATMSG:TO: email@example.com;SUB:email subject;BODY:Email text;;')).toEqual({ | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         body: 'Email text', | ||||||
|  |         subject: 'email subject', | ||||||
|  |         to: 'email@example.com', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('mailto:email@example.com?subject=email subject&body=Email text')).toEqual({ | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         body: 'Email text', | ||||||
|  |         subject: 'email subject', | ||||||
|  |         to: 'email@example.com', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('SMTP:email@example.com:email subject:Email text')).toEqual({ | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         body: 'Email text', | ||||||
|  |         subject: 'email subject', | ||||||
|  |         to: 'email@example.com', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('smsto:+33315555:message')).toEqual({ | ||||||
|  |       type: 'SMS', | ||||||
|  |       value: { | ||||||
|  |         message: 'message', | ||||||
|  |         to: '+33315555', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('WIFI:T:nopass;S:ssid;H:true;')).toEqual({ | ||||||
|  |       type: 'Wifi', | ||||||
|  |       value: { | ||||||
|  |         authentication: 'nopass', | ||||||
|  |         hidden: 'true', | ||||||
|  |         name: 'ssid', | ||||||
|  |         password: undefined, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('WIFI:T:WPA;S:ssid;P:password;H:false;')).toEqual({ | ||||||
|  |       type: 'Wifi', | ||||||
|  |       value: { | ||||||
|  |         authentication: 'WPA', | ||||||
|  |         hidden: 'false', | ||||||
|  |         name: 'ssid', | ||||||
|  |         password: 'password', // NOSONAR
 | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     expect(parseQRData('BEGIN:VCALENDAR\nPRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN\nVERSION:2.0\nBEGIN:VEVENT\nDTSTAMP:19960704T120000Z\nUID:uid1@example.com\nORGANIZER:mailto:jsmith@example.com\nDTSTART:19960918T143000Z\nDTEND:19960920T220000Z\nSTATUS:CONFIRMED\nCATEGORIES:CONFERENCE\nSUMMARY:Networld+Interop Conference\nDESCRIPTION:Networld+Interop Conference\n  and Exhibit\\nAtlanta World Congress Center\\n\n Atlanta\\, Georgia\nEND:VEVENT\nEND:VCALENDAR')) | ||||||
|  |       .toEqual({ | ||||||
|  |         type: 'iCal', | ||||||
|  |         value: [ | ||||||
|  |           'vcalendar', | ||||||
|  |           [ | ||||||
|  |             [ | ||||||
|  |               'prodid', | ||||||
|  |               {}, | ||||||
|  |               'text', | ||||||
|  |               '-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN', | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |               'version', | ||||||
|  |               {}, | ||||||
|  |               'text', | ||||||
|  |               '2.0', | ||||||
|  |             ], | ||||||
|  |           ], | ||||||
|  |           [ | ||||||
|  |             [ | ||||||
|  |               'vevent', | ||||||
|  |               [ | ||||||
|  |                 [ | ||||||
|  |                   'dtstamp', | ||||||
|  |                   {}, | ||||||
|  |                   'date-time', | ||||||
|  |                   '1996-07-04T12:00:00Z', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'uid', | ||||||
|  |                   {}, | ||||||
|  |                   'text', | ||||||
|  |                   'uid1@example.com', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'organizer', | ||||||
|  |                   {}, | ||||||
|  |                   'cal-address', | ||||||
|  |                   'mailto:jsmith@example.com', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'dtstart', | ||||||
|  |                   {}, | ||||||
|  |                   'date-time', | ||||||
|  |                   '1996-09-18T14:30:00Z', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'dtend', | ||||||
|  |                   {}, | ||||||
|  |                   'date-time', | ||||||
|  |                   '1996-09-20T22:00:00Z', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'status', | ||||||
|  |                   {}, | ||||||
|  |                   'text', | ||||||
|  |                   'CONFIRMED', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'categories', | ||||||
|  |                   {}, | ||||||
|  |                   'text', | ||||||
|  |                   'CONFERENCE', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'summary', | ||||||
|  |                   {}, | ||||||
|  |                   'text', | ||||||
|  |                   'Networld+Interop Conference', | ||||||
|  |                 ], | ||||||
|  |                 [ | ||||||
|  |                   'description', | ||||||
|  |                   {}, | ||||||
|  |                   'text', | ||||||
|  |                   'Networld+Interop Conference and Exhibit\nAtlanta World Congress Center\nAtlanta, Georgia', | ||||||
|  |                 ], | ||||||
|  |               ], | ||||||
|  |               [], | ||||||
|  |             ], | ||||||
|  |           ], | ||||||
|  |         ], | ||||||
|  |       }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										83
									
								
								src/tools/qr-code-decoder/qr-code-decoder.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/tools/qr-code-decoder/qr-code-decoder.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | import ICAL from 'ical.js'; | ||||||
|  | 
 | ||||||
|  | export function parseQRData(qrContent: string | null) { | ||||||
|  |   if (!qrContent) { | ||||||
|  |     return { type: 'Unknown', value: '' }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('BEGIN:VCALENDAR')) { | ||||||
|  |     return { type: 'iCal', value: ICAL.parse(qrContent?.trim()) }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('TEL:')) { | ||||||
|  |     return { type: 'Phone', value: qrContent.substring(4)?.trim() }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('MATMSG:')) { | ||||||
|  |     // MATMSG:TO: email@example.com;SUB:email subject;BODY:Email text;;
 | ||||||
|  |     const parsing = /^MATMSG:(?:TO:([^;]*);)?(?:SUB:([^;]*);)?(?:BODY:([^;]*))?;;$/.exec(qrContent) || []; | ||||||
|  |     return { | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         to: parsing[1]?.trim(), | ||||||
|  |         subject: parsing[2]?.trim(), | ||||||
|  |         body: parsing[3]?.trim(), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('mailto:')) { | ||||||
|  |     // mailto:email@example.com?subject=email subject&body=Email text
 | ||||||
|  |     const parsing = /^mailto:([^\?]+)\?subject=([^\&]*)(?:&body=(.*))$/.exec(qrContent) || []; | ||||||
|  |     return { | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         to: parsing[1]?.trim(), | ||||||
|  |         subject: parsing[2]?.trim(), | ||||||
|  |         body: parsing[3]?.trim(), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('SMTP:')) { | ||||||
|  |     // SMTP:email@example.com:email subject:Email text
 | ||||||
|  |     const parsing = /^SMTP:([^:]+)(?::([^:]*))(?::([^:]*))?$/.exec(qrContent) || []; | ||||||
|  |     return { | ||||||
|  |       type: 'Email', | ||||||
|  |       value: { | ||||||
|  |         to: parsing[1]?.trim(), | ||||||
|  |         subject: parsing[2]?.trim(), | ||||||
|  |         body: parsing[3]?.trim(), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('smsto:')) { | ||||||
|  |     // smsto:${phoneNumber}:${message}
 | ||||||
|  |     const parsing = /^smsto:([^:]+)(?::(.+))$/.exec(qrContent) || []; | ||||||
|  |     return { | ||||||
|  |       type: 'SMS', | ||||||
|  |       value: { | ||||||
|  |         to: parsing[1]?.trim(), | ||||||
|  |         message: parsing[2]?.trim(), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   if (qrContent.startsWith('WIFI:')) { | ||||||
|  |     // WIFI:T:${authentication};S:${name};${authentication !== 'nopass' ? `P:${password};` : ''}H:${hidden};
 | ||||||
|  |     const parsing = /^WIFI:T:([^;]+);S:([^;]+);(?:P:([^;]+);)?(?:H:([^;]+);)?$/.exec(qrContent) || []; | ||||||
|  |     return { | ||||||
|  |       type: 'Wifi', | ||||||
|  |       value: { | ||||||
|  |         authentication: parsing[1]?.trim(), | ||||||
|  |         name: parsing[2]?.trim(), | ||||||
|  |         password: parsing[3]?.trim(), | ||||||
|  |         hidden: parsing[4]?.trim(), | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   if (/^(?:https?|ftp):\/\//.test(qrContent)) { | ||||||
|  |     return { | ||||||
|  |       type: 'Url', | ||||||
|  |       value: qrContent, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   return { | ||||||
|  |     type: 'Text', | ||||||
|  |     value: qrContent, | ||||||
|  |   }; | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								src/tools/qr-code-decoder/qr-code-decoder.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/tools/qr-code-decoder/qr-code-decoder.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { Ref } from 'vue'; | ||||||
|  | import qrcodeParser from 'qrcode-parser'; | ||||||
|  | import { parseQRData } from './qr-code-decoder.service'; | ||||||
|  | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
|  | 
 | ||||||
|  | const fileInput = ref() as Ref<File>; | ||||||
|  | const qrCode = computedAsync(async () => { | ||||||
|  |   try { | ||||||
|  |     return (await qrcodeParser(fileInput.value)); | ||||||
|  |   } | ||||||
|  |   catch (e: any) { | ||||||
|  |     return e.toString(); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | const qrCodeParsed = computed(() => { | ||||||
|  |   try { | ||||||
|  |     const parsed = parseQRData(qrCode.value); | ||||||
|  |     return `Type: ${parsed.type}\nValue:${JSON.stringify(parsed.value, null, 2)}`; | ||||||
|  |   } | ||||||
|  |   catch (e: any) { | ||||||
|  |     return e.toString(); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function onUpload(file: File) { | ||||||
|  |   if (file) { | ||||||
|  |     fileInput.value = file; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-file-upload | ||||||
|  |       title="Drag and drop a QR Code here, or click to select a file" | ||||||
|  |       :paste-image="true" | ||||||
|  |       @file-upload="onUpload" | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <n-divider /> | ||||||
|  | 
 | ||||||
|  |     <div> | ||||||
|  |       <h3>Decoded</h3> | ||||||
|  |       <TextareaCopyable | ||||||
|  |         :value="qrCode" | ||||||
|  |         :word-wrap="true" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |     <div> | ||||||
|  |       <h3>Parsed</h3> | ||||||
|  |       <TextareaCopyable | ||||||
|  |         :value="qrCodeParsed" | ||||||
|  |         :word-wrap="true" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style lang="less" scoped> | ||||||
|  | ::v-deep(.n-upload-trigger) { | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -5,10 +5,12 @@ const props = withDefaults(defineProps<{ | |||||||
|   multiple?: boolean |   multiple?: boolean | ||||||
|   accept?: string |   accept?: string | ||||||
|   title?: string |   title?: string | ||||||
|  |   pasteImage?: boolean | ||||||
| }>(), { | }>(), { | ||||||
|   multiple: false, |   multiple: false, | ||||||
|   accept: undefined, |   accept: undefined, | ||||||
|   title: 'Drag and drop files here, or click to select files', |   title: 'Drag and drop files here, or click to select files', | ||||||
|  |   pasteImage: false, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| @ -16,11 +18,31 @@ const emit = defineEmits<{ | |||||||
|   (event: 'fileUpload', file: File): void |   (event: 'fileUpload', file: File): void | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const { multiple } = toRefs(props); | const { multiple, pasteImage } = toRefs(props); | ||||||
| 
 | 
 | ||||||
| const isOverDropZone = ref(false); | const isOverDropZone = ref(false); | ||||||
| 
 | 
 | ||||||
|  | function toBase64(file: File) { | ||||||
|  |   return new Promise<string>((resolve, reject) => { | ||||||
|  |     const reader = new FileReader(); | ||||||
|  |     reader.readAsDataURL(file); | ||||||
|  |     reader.onload = () => resolve(reader.result?.toString() ?? ''); | ||||||
|  |     reader.onerror = error => reject(error); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| const fileInput = ref<HTMLInputElement | null>(null); | const fileInput = ref<HTMLInputElement | null>(null); | ||||||
|  | const imgPreview = ref<HTMLImageElement | null>(null); | ||||||
|  | async function handlePreview(image: File) { | ||||||
|  |   if (imgPreview.value) { | ||||||
|  |     imgPreview.value.src = await toBase64(image); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | function clearPreview() { | ||||||
|  |   if (imgPreview.value) { | ||||||
|  |     imgPreview.value.src = ''; | ||||||
|  |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| function triggerFileInput() { | function triggerFileInput() { | ||||||
|   fileInput.value?.click(); |   fileInput.value?.click(); | ||||||
| @ -39,7 +61,30 @@ function handleDrop(event: DragEvent) { | |||||||
|   handleUpload(files); |   handleUpload(files); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function handleUpload(files: FileList | null | undefined) { | async function onPasteImage(evt: ClipboardEvent) { | ||||||
|  |   if (!pasteImage.value) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const items = evt.clipboardData?.items; | ||||||
|  |   if (!items) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   for (let i = 0; i < items.length; i++) { | ||||||
|  |     if (items[i].type.includes('image')) { | ||||||
|  |       const imageFile = items[i].getAsFile(); | ||||||
|  |       if (imageFile) { | ||||||
|  |         await handlePreview(imageFile); | ||||||
|  |         emit('fileUpload', imageFile); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function handleUpload(files: FileList | null | undefined) { | ||||||
|  |   clearPreview(); | ||||||
|  | 
 | ||||||
|   if (_.isNil(files) || _.isEmpty(files)) { |   if (_.isNil(files) || _.isEmpty(files)) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @ -49,6 +94,7 @@ function handleUpload(files: FileList | null | undefined) { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   await handlePreview(files[0]); | ||||||
|   emit('fileUpload', files[0]); |   emit('fileUpload', files[0]); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| @ -60,6 +106,7 @@ function handleUpload(files: FileList | null | undefined) { | |||||||
|       'border-primary border-opacity-100': isOverDropZone, |       'border-primary border-opacity-100': isOverDropZone, | ||||||
|     }" |     }" | ||||||
|     @click="triggerFileInput" |     @click="triggerFileInput" | ||||||
|  |     @paste.prevent="onPasteImage" | ||||||
|     @drop.prevent="handleDrop" |     @drop.prevent="handleDrop" | ||||||
|     @dragover.prevent |     @dragover.prevent | ||||||
|     @dragenter="isOverDropZone = true" |     @dragenter="isOverDropZone = true" | ||||||
| @ -73,6 +120,7 @@ function handleUpload(files: FileList | null | undefined) { | |||||||
|       :accept="accept" |       :accept="accept" | ||||||
|       @change="handleFileInput" |       @change="handleFileInput" | ||||||
|     > |     > | ||||||
|  | 
 | ||||||
|     <slot> |     <slot> | ||||||
|       <span op-70> |       <span op-70> | ||||||
|         {{ title }} |         {{ title }} | ||||||
| @ -90,6 +138,22 @@ function handleUpload(files: FileList | null | undefined) { | |||||||
|       <c-button> |       <c-button> | ||||||
|         Browse files |         Browse files | ||||||
|       </c-button> |       </c-button> | ||||||
|  | 
 | ||||||
|  |       <div v-if="pasteImage"> | ||||||
|  |         <!-- separator --> | ||||||
|  |         <div my-4 w-full flex items-center justify-center op-70> | ||||||
|  |           <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" /> | ||||||
|  |           <div class="mx-2 text-gray-400"> | ||||||
|  |             or | ||||||
|  |           </div> | ||||||
|  |           <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" /> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <p>Paste an image from clipboard</p> | ||||||
|  |       </div> | ||||||
|     </slot> |     </slot> | ||||||
|  |     <div mt-2> | ||||||
|  |       <img ref="imgPreview" width="150"> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user