Skip to content

Commit 236b7ae

Browse files
authored
Merge branch 'main' into text-to-utf8-bin
2 parents 35d8264 + e1b4f9a commit 236b7ae

File tree

11 files changed

+198
-44
lines changed

11 files changed

+198
-44
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"highlight.js": "^11.7.0",
6565
"iarna-toml-esm": "^3.0.5",
6666
"ibantools": "^4.3.3",
67+
"js-base64": "^3.7.6",
6768
"json5": "^2.2.3",
6869
"jwt-decode": "^3.1.2",
6970
"libphonenumber-js": "^1.10.28",
@@ -137,5 +138,6 @@
137138
"vitest": "^0.34.0",
138139
"workbox-window": "^7.0.0",
139140
"zx": "^7.2.1"
140-
}
141+
},
142+
"packageManager": "[email protected]"
141143
}

pnpm-lock.yaml

Lines changed: 13 additions & 6 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/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
import { generateLoremIpsum } from './lorem-ipsum-generator.service';
33
import { useCopy } from '@/composable/copy';
44
import { randIntFromInterval } from '@/utils/random';
5+
import { computedRefreshable } from '@/composable/computedRefreshable';
56
67
const paragraphs = ref(1);
78
const sentences = ref([3, 8]);
89
const words = ref([8, 15]);
910
const startWithLoremIpsum = ref(true);
1011
const asHTML = ref(false);
1112
12-
const loremIpsumText = computed(() =>
13+
const [loremIpsumText, refreshLoremIpsum] = computedRefreshable(() =>
1314
generateLoremIpsum({
1415
paragraphCount: paragraphs.value,
1516
asHTML: asHTML.value,
@@ -18,6 +19,7 @@ const loremIpsumText = computed(() =>
1819
startWithLoremIpsum: startWithLoremIpsum.value,
1920
}),
2021
);
22+
2123
const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' });
2224
</script>
2325

@@ -41,10 +43,13 @@ const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to
4143

4244
<c-input-text :value="loremIpsumText" multiline placeholder="Your lorem ipsum..." readonly mt-5 rows="5" />
4345

44-
<div mt-5 flex justify-center>
46+
<div mt-5 flex justify-center gap-3>
4547
<c-button autofocus @click="copy()">
4648
Copy
4749
</c-button>
50+
<c-button @click="refreshLoremIpsum">
51+
Refresh
52+
</c-button>
4853
</div>
4954
</c-card>
5055
</template>

0 commit comments

Comments
 (0)