diff --git a/components.d.ts b/components.d.ts index 8e59366a..808eaf16 100644 --- a/components.d.ts +++ b/components.d.ts @@ -89,6 +89,7 @@ declare module '@vue/runtime-core' { HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] + IcoConverter: typeof import('./src/tools/ico-converter/ico-converter.vue')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] @@ -134,19 +135,18 @@ declare module '@vue/runtime-core' { NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] + NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NForm: typeof import('naive-ui')['NForm'] - NFormItem: typeof import('naive-ui')['NFormItem'] + NGi: typeof import('naive-ui')['NGi'] + NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] - NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSlider: typeof import('naive-ui')['NSlider'] - NSwitch: typeof import('naive-ui')['NSwitch'] + NSpin: typeof import('naive-ui')['NSpin'] + NTag: typeof import('naive-ui')['NTag'] 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'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/package.json b/package.json index 63e5856a..2babaa9d 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", "js-base64": "^3.7.6", + "image-in-browser": "^3.1.0", + "js-base64": "^3.7.7", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2311f3af..d90a6910 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,8 +98,11 @@ dependencies: ibantools: specifier: ^4.3.3 version: 4.3.3 + image-in-browser: + specifier: ^3.1.0 + version: 3.1.0 js-base64: - specifier: ^3.7.6 + specifier: ^3.7.7 version: 3.7.7 json5: specifier: ^2.2.3 @@ -6142,6 +6145,10 @@ packages: engines: {node: '>= 4'} dev: true + /image-in-browser@3.1.0: + resolution: {integrity: sha512-8VYDQU/AzN6cLrPEjF1W3QFLZS9clmEbztXE6WMBoxBJxOzjzSvC1kIusEQEc+IBFxsCbMKREFw/Cn3xVE8VMA==} + dev: false + /image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} diff --git a/src/composable/downloadBase64.ts b/src/composable/downloadBase64.ts index 3bc20226..773541e2 100644 --- a/src/composable/downloadBase64.ts +++ b/src/composable/downloadBase64.ts @@ -1,6 +1,7 @@ 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 { get } from '@vueuse/core'; export { getMimeTypeFromBase64, @@ -75,21 +76,11 @@ function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }: } function useDownloadFileFromBase64( - { source, filename, extension, fileMimeType }: - { source: Ref; filename?: string; extension?: string; fileMimeType?: string }) { - return { - download() { - downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType }); - }, - }; -} - -function useDownloadFileFromBase64Refs( { source, filename, extension }: - { source: Ref; filename?: Ref; extension?: Ref }) { + { source: MaybeRef; filename?: MaybeRef; extension?: MaybeRef }) { return { 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; } + +function useDownloadFileFromBase64Refs( + { source, filename, extension }: + { source: Ref; filename?: Ref; extension?: Ref }) { + return { + download() { + downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value }); + }, + }; +} diff --git a/src/tools/ico-converter/ico-converter.vue b/src/tools/ico-converter/ico-converter.vue new file mode 100644 index 00000000..8acfe8eb --- /dev/null +++ b/src/tools/ico-converter/ico-converter.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/tools/ico-converter/index.ts b/src/tools/ico-converter/index.ts new file mode 100644 index 00000000..e30a82c8 --- /dev/null +++ b/src/tools/ico-converter/index.ts @@ -0,0 +1,12 @@ +import { PictureInPicture } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'ICO/PNG converter', + path: '/ico-converter', + description: 'Convert from PNG/JPEG to/from ICO', + keywords: ['ico', 'png', 'jpeg', 'icon', 'converter'], + component: () => import('./ico-converter.vue'), + icon: PictureInPicture, + createdAt: new Date('2024-08-15'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index c9003fe8..8987bed6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as icoConverter } from './ico-converter'; import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -139,7 +140,13 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Images and videos', - components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], + components: [ + qrCodeGenerator, + wifiQrCodeGenerator, + svgPlaceholderGenerator, + cameraRecorder, + icoConverter, + ], }, { name: 'Development', diff --git a/src/ui/c-file-upload/c-file-upload.vue b/src/ui/c-file-upload/c-file-upload.vue index b48d8c3b..ecb7f1e4 100644 --- a/src/ui/c-file-upload/c-file-upload.vue +++ b/src/ui/c-file-upload/c-file-upload.vue @@ -5,10 +5,12 @@ const props = withDefaults(defineProps<{ multiple?: boolean accept?: string title?: string + pasteImage?: boolean }>(), { multiple: false, accept: undefined, title: 'Drag and drop files here, or click to select files', + pasteImage: false, }); const emit = defineEmits<{ @@ -16,11 +18,31 @@ const emit = defineEmits<{ (event: 'fileUpload', file: File): void }>(); -const { multiple } = toRefs(props); +const { multiple, pasteImage } = toRefs(props); const isOverDropZone = ref(false); +function toBase64(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result?.toString() ?? ''); + reader.onerror = error => reject(error); + }); +} + const fileInput = ref(null); +const imgPreview = ref(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() { fileInput.value?.click(); @@ -39,7 +61,30 @@ function handleDrop(event: DragEvent) { 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)) { return; } @@ -49,6 +94,7 @@ function handleUpload(files: FileList | null | undefined) { return; } + await handlePreview(files[0]); emit('fileUpload', files[0]); } @@ -60,6 +106,7 @@ function handleUpload(files: FileList | null | undefined) { 'border-primary border-opacity-100': isOverDropZone, }" @click="triggerFileInput" + @paste.prevent="onPasteImage" @drop.prevent="handleDrop" @dragover.prevent @dragenter="isOverDropZone = true" @@ -73,6 +120,7 @@ function handleUpload(files: FileList | null | undefined) { :accept="accept" @change="handleFileInput" > + {{ title }} @@ -90,6 +138,22 @@ function handleUpload(files: FileList | null | undefined) { Browse files + +
+ +
+
+
+ or +
+
+
+ +

Paste an image from clipboard

+
+
+ +