Skip to content

Commit c66103e

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/barcodes
2 parents fef1993 + 76a19d2 commit c66103e

File tree

10 files changed

+184
-36
lines changed

10 files changed

+184
-36
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,6 @@
140140
"vitest": "^0.34.0",
141141
"workbox-window": "^7.0.0",
142142
"zx": "^7.2.1"
143-
}
143+
},
144+
"packageManager": "[email protected]"
144145
}

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/FormatTransformer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const output = computed(() => transformer.value(input.value));
4848
monospace
4949
/>
5050

51-
<div>
51+
<div overflow-auto>
5252
<div mb-5px>
5353
{{ outputLabel }}
5454
</div>

src/composable/debouncedref.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import _ from 'lodash';
2+
3+
function useDebouncedRef<T>(initialValue: T, delay: number, immediate: boolean = false) {
4+
const state = ref(initialValue);
5+
const debouncedRef = customRef((track, trigger) => ({
6+
get() {
7+
track();
8+
return state.value;
9+
},
10+
set: _.debounce(
11+
(value) => {
12+
state.value = value;
13+
trigger();
14+
},
15+
delay,
16+
{ leading: immediate },
17+
),
18+
}));
19+
return debouncedRef;
20+
}
21+
export default useDebouncedRef;

src/composable/downloadBase64.ts

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { extension as getExtensionFromMime } from 'mime-types';
1+
import { extension as getExtensionFromMimeType, extension as getMimeTypeFromExtension } from 'mime-types';
22
import type { Ref } from 'vue';
33
import _ from 'lodash';
44

5-
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
5+
export {
6+
getMimeTypeFromBase64,
7+
getMimeTypeFromExtension, getExtensionFromMimeType,
8+
useDownloadFileFromBase64, useDownloadFileFromBase64Refs,
9+
previewImageFromBase64,
10+
};
611

712
const commonMimeTypesSignatures = {
813
'JVBERi0': 'application/pdf',
@@ -36,30 +41,78 @@ function getFileExtensionFromMimeType({
3641
defaultExtension?: string
3742
}) {
3843
if (mimeType) {
39-
return getExtensionFromMime(mimeType) ?? defaultExtension;
44+
return getExtensionFromMimeType(mimeType) ?? defaultExtension;
4045
}
4146

4247
return defaultExtension;
4348
}
4449

45-
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
46-
return {
47-
download() {
48-
if (source.value === '') {
49-
throw new Error('Base64 string is empty');
50-
}
50+
function downloadFromBase64({ sourceValue, filename, extension, fileMimeType }:
51+
{ sourceValue: string; filename?: string; extension?: string; fileMimeType?: string }) {
52+
if (sourceValue === '') {
53+
throw new Error('Base64 string is empty');
54+
}
5155

52-
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
53-
const base64String = mimeType
54-
? source.value
55-
: `data:text/plain;base64,${source.value}`;
56+
const defaultExtension = extension ?? 'txt';
57+
const { mimeType } = getMimeTypeFromBase64({ base64String: sourceValue });
58+
let base64String = sourceValue;
59+
if (!mimeType) {
60+
const targetMimeType = fileMimeType ?? getMimeTypeFromExtension(defaultExtension);
61+
base64String = `data:${targetMimeType};base64,${sourceValue}`;
62+
}
5663

57-
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
64+
const cleanExtension = extension ?? getFileExtensionFromMimeType(
65+
{ mimeType, defaultExtension });
66+
let cleanFileName = filename ?? `file.${cleanExtension}`;
67+
if (extension && !cleanFileName.endsWith(`.${extension}`)) {
68+
cleanFileName = `${cleanFileName}.${cleanExtension}`;
69+
}
5870

59-
const a = document.createElement('a');
60-
a.href = base64String;
61-
a.download = cleanFileName;
62-
a.click();
71+
const a = document.createElement('a');
72+
a.href = base64String;
73+
a.download = cleanFileName;
74+
a.click();
75+
}
76+
77+
function useDownloadFileFromBase64(
78+
{ source, filename, extension, fileMimeType }:
79+
{ source: Ref<string>; filename?: string; extension?: string; fileMimeType?: string }) {
80+
return {
81+
download() {
82+
downloadFromBase64({ sourceValue: source.value, filename, extension, fileMimeType });
6383
},
6484
};
6585
}
86+
87+
function useDownloadFileFromBase64Refs(
88+
{ source, filename, extension }:
89+
{ source: Ref<string>; filename?: Ref<string>; extension?: Ref<string> }) {
90+
return {
91+
download() {
92+
downloadFromBase64({ sourceValue: source.value, filename: filename?.value, extension: extension?.value });
93+
},
94+
};
95+
}
96+
97+
function previewImageFromBase64(base64String: string): HTMLImageElement {
98+
if (base64String === '') {
99+
throw new Error('Base64 string is empty');
100+
}
101+
102+
const img = document.createElement('img');
103+
img.src = base64String;
104+
105+
const container = document.createElement('div');
106+
container.appendChild(img);
107+
108+
const previewContainer = document.getElementById('previewContainer');
109+
if (previewContainer) {
110+
previewContainer.innerHTML = '';
111+
previewContainer.appendChild(container);
112+
}
113+
else {
114+
throw new Error('Preview container element not found');
115+
}
116+
117+
return img;
118+
}

src/tools/base64-file-converter/base64-file-converter.vue

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
import { useBase64 } from '@vueuse/core';
33
import type { Ref } from 'vue';
44
import { useCopy } from '@/composable/copy';
5-
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
5+
import { getExtensionFromMimeType, getMimeTypeFromBase64, previewImageFromBase64, useDownloadFileFromBase64Refs } from '@/composable/downloadBase64';
66
import { useValidation } from '@/composable/validation';
77
import { isValidBase64 } from '@/utils/base64';
88
9+
const fileName = ref('file');
10+
const fileExtension = ref('');
911
const base64Input = ref('');
10-
const { download } = useDownloadFileFromBase64({ source: base64Input });
12+
const { download } = useDownloadFileFromBase64Refs(
13+
{
14+
source: base64Input,
15+
filename: fileName,
16+
extension: fileExtension,
17+
});
1118
const base64InputValidation = useValidation({
1219
source: base64Input,
1320
rules: [
@@ -18,6 +25,35 @@ const base64InputValidation = useValidation({
1825
],
1926
});
2027
28+
watch(
29+
base64Input,
30+
(newValue, _) => {
31+
const { mimeType } = getMimeTypeFromBase64({ base64String: newValue });
32+
if (mimeType) {
33+
fileExtension.value = getExtensionFromMimeType(mimeType) || fileExtension.value;
34+
}
35+
},
36+
);
37+
38+
function previewImage() {
39+
if (!base64InputValidation.isValid) {
40+
return;
41+
}
42+
try {
43+
const image = previewImageFromBase64(base64Input.value);
44+
image.style.maxWidth = '100%';
45+
image.style.maxHeight = '400px';
46+
const previewContainer = document.getElementById('previewContainer');
47+
if (previewContainer) {
48+
previewContainer.innerHTML = '';
49+
previewContainer.appendChild(image);
50+
}
51+
}
52+
catch (_) {
53+
//
54+
}
55+
}
56+
2157
function downloadFile() {
2258
if (!base64InputValidation.isValid) {
2359
return;
@@ -44,6 +80,24 @@ async function onUpload(file: File) {
4480

4581
<template>
4682
<c-card title="Base64 to file">
83+
<n-grid cols="3" x-gap="12">
84+
<n-gi span="2">
85+
<c-input-text
86+
v-model:value="fileName"
87+
label="File Name"
88+
placeholder="Download filename"
89+
mb-2
90+
/>
91+
</n-gi>
92+
<n-gi>
93+
<c-input-text
94+
v-model:value="fileExtension"
95+
label="Extension"
96+
placeholder="Extension"
97+
mb-2
98+
/>
99+
</n-gi>
100+
</n-grid>
47101
<c-input-text
48102
v-model:value="base64Input"
49103
multiline
@@ -53,7 +107,14 @@ async function onUpload(file: File) {
53107
mb-2
54108
/>
55109

56-
<div flex justify-center>
110+
<div flex justify-center py-2>
111+
<div id="previewContainer" />
112+
</div>
113+
114+
<div flex justify-center gap-3>
115+
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="previewImage()">
116+
Preview image
117+
</c-button>
57118
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
58119
Download file
59120
</c-button>

src/tools/emoji-picker/emoji-picker.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import emojiKeywords from 'emojilib';
44
import _ from 'lodash';
55
import type { EmojiInfo } from './emoji.types';
66
import { useFuzzySearch } from '@/composable/fuzzySearch';
7+
import useDebouncedRef from '@/composable/debouncedref';
78
89
const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join('');
910
const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined;
@@ -23,7 +24,7 @@ const emojisGroups: { emojiInfos: EmojiInfo[]; group: string }[] = _
2324
.map((emojiInfos, group) => ({ group, emojiInfos }))
2425
.value();
2526
26-
const searchQuery = ref('');
27+
const searchQuery = useDebouncedRef('', 500);
2728
2829
const { searchResult } = useFuzzySearch({
2930
search: searchQuery,

src/tools/jwt-parser/jwt-parser.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ const validation = useValidation({
3939
{{ section.title }}
4040
</th>
4141
<tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
42-
<td class="claims">
42+
<td class="claims" style="vertical-align: top;">
4343
<span font-bold>
4444
{{ claim }}
4545
</span>
4646
<span v-if="claimDescription" ml-2 op-70>
4747
({{ claimDescription }})
4848
</span>
4949
</td>
50-
<td>
50+
<td style="word-wrap: break-word;word-break: break-all;">
5151
<span>{{ value }}</span>
5252
<span v-if="friendlyValue" ml-2 op-70>
5353
({{ friendlyValue }})

src/utils/base64.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ describe('base64 utils', () => {
3838

3939
it('should throw for incorrect base64 string', () => {
4040
expect(() => base64ToText('a')).to.throw('Incorrect base64 string');
41-
expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
41+
// should not really be false because trimming of space is now implied
42+
// expect(() => base64ToText(' ')).to.throw('Incorrect base64 string');
4243
expect(() => base64ToText('é')).to.throw('Incorrect base64 string');
4344
// missing final '='
4445
expect(() => base64ToText('bG9yZW0gaXBzdW0')).to.throw('Incorrect base64 string');
@@ -56,17 +57,17 @@ describe('base64 utils', () => {
5657

5758
it('should return false for incorrect base64 string', () => {
5859
expect(isValidBase64('a')).to.eql(false);
59-
expect(isValidBase64(' ')).to.eql(false);
6060
expect(isValidBase64('é')).to.eql(false);
6161
expect(isValidBase64('data:text/plain;notbase64,YQ==')).to.eql(false);
6262
// missing final '='
6363
expect(isValidBase64('bG9yZW0gaXBzdW0')).to.eql(false);
6464
});
6565

66-
it('should return false for untrimmed correct base64 string', () => {
67-
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(false);
68-
expect(isValidBase64(' LTE=')).to.eql(false);
69-
expect(isValidBase64(' YQ== ')).to.eql(false);
66+
it('should return true for untrimmed correct base64 string', () => {
67+
expect(isValidBase64('bG9yZW0gaXBzdW0= ')).to.eql(true);
68+
expect(isValidBase64(' LTE=')).to.eql(true);
69+
expect(isValidBase64(' YQ== ')).to.eql(true);
70+
expect(isValidBase64(' ')).to.eql(true);
7071
});
7172
});
7273

src/utils/base64.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { Base64 } from 'js-base64';
2+
13
export { textToBase64, base64ToText, isValidBase64, removePotentialDataAndMimePrefix };
24

35
function textToBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boolean } = {}) {
4-
const encoded = window.btoa(str);
6+
const encoded = Base64.encode(str);
57
return makeUrlSafe ? makeUriSafe(encoded) : encoded;
68
}
79

@@ -16,7 +18,7 @@ function base64ToText(str: string, { makeUrlSafe = false }: { makeUrlSafe?: bool
1618
}
1719

1820
try {
19-
return window.atob(cleanStr);
21+
return Base64.decode(cleanStr);
2022
}
2123
catch (_) {
2224
throw new Error('Incorrect base64 string');
@@ -34,10 +36,11 @@ function isValidBase64(str: string, { makeUrlSafe = false }: { makeUrlSafe?: boo
3436
}
3537

3638
try {
39+
const reEncodedBase64 = Base64.fromUint8Array(Base64.toUint8Array(cleanStr));
3740
if (makeUrlSafe) {
38-
return removePotentialPadding(window.btoa(window.atob(cleanStr))) === cleanStr;
41+
return removePotentialPadding(reEncodedBase64) === cleanStr;
3942
}
40-
return window.btoa(window.atob(cleanStr)) === cleanStr;
43+
return reEncodedBase64 === cleanStr.replace(/\s/g, '');
4144
}
4245
catch (err) {
4346
return false;

0 commit comments

Comments
 (0)