Skip to content

Commit 51ba89d

Browse files
committed
feat(new tool) multi-link-downloader
1 parent 5732483 commit 51ba89d

File tree

8 files changed

+238
-1
lines changed

8 files changed

+238
-1
lines changed

components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ declare module '@vue/runtime-core' {
129129
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
130130
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
131131
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
132+
MultiLinkDownloader: typeof import('./src/tools/multi-link-downloader/multi-link-downloader.vue')['default']
132133
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
133134
NCheckbox: typeof import('naive-ui')['NCheckbox']
134135
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']

locales/en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,7 @@ tools:
391391
text-to-binary:
392392
title: Text to ASCII binary
393393
description: Convert text to its ASCII binary representation and vice-versa.
394+
395+
multi-link-downloader:
396+
title: Multi link downloader
397+
description: Asynchronously downloads from multiple links into a zip file while a single link downloads directly.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"ibantools": "^4.3.3",
7171
"js-base64": "^3.7.6",
7272
"json5": "^2.2.3",
73+
"jszip": "^3.10.1",
7374
"jwt-decode": "^3.1.2",
7475
"libphonenumber-js": "^1.10.28",
7576
"lodash": "^4.17.21",

pnpm-lock.yaml

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

src/tools/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
4+
import { tool as multiLinkDownloader } from './multi-link-downloader';
45
import { tool as emailNormalizer } from './email-normalizer';
56

67
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@@ -188,7 +189,11 @@ export const toolsByCategory: ToolCategory[] = [
188189
},
189190
{
190191
name: 'Data',
191-
components: [phoneParserAndFormatter, ibanValidatorAndParser],
192+
components: [
193+
phoneParserAndFormatter,
194+
ibanValidatorAndParser,
195+
multiLinkDownloader,
196+
],
192197
},
193198
];
194199

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FileDownload } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Multi link downloader',
6+
path: '/multi-link-downloader',
7+
description: '',
8+
keywords: ['multi', 'link', 'downloader'],
9+
component: () => import('./multi-link-downloader.vue'),
10+
icon: FileDownload,
11+
createdAt: new Date('2024-10-18'),
12+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import JSZip from 'jszip';
2+
3+
export async function downloadLinks(links: string): Promise<void> {
4+
// Split links by newline and filter out empty ones
5+
const linksArray: string[] = links.split('\n').filter(link => link.trim() !== '');
6+
7+
// Helper function to handle duplicate filenames
8+
function getUniqueFileName(existingNames: Set<string>, originalName: string): string {
9+
let fileName = originalName;
10+
let fileExtension = '';
11+
12+
// Split filename and extension (if any)
13+
const lastDotIndex = originalName.lastIndexOf('.');
14+
if (lastDotIndex !== -1) {
15+
fileName = originalName.substring(0, lastDotIndex);
16+
fileExtension = originalName.substring(lastDotIndex);
17+
}
18+
19+
let counter = 1;
20+
let uniqueName = originalName;
21+
22+
// Append a counter to the filename if it already exists in the map
23+
while (existingNames.has(uniqueName)) {
24+
uniqueName = `${fileName} (${counter})${fileExtension}`;
25+
counter++;
26+
}
27+
28+
existingNames.add(uniqueName);
29+
return uniqueName;
30+
}
31+
32+
if (linksArray.length === 1) {
33+
// Single link: download directly
34+
const linkUrl: string = linksArray[0];
35+
try {
36+
const response: Response = await fetch(linkUrl);
37+
if (!response.ok) {
38+
throw new Error(`Failed to fetch ${linkUrl}`);
39+
}
40+
41+
// Get file as blob
42+
const blob: Blob = await response.blob();
43+
44+
// Extract filename from URL
45+
const fileName: string = linkUrl.split('/').pop() || 'downloaded_file';
46+
47+
// Trigger download
48+
const a: HTMLAnchorElement = document.createElement('a');
49+
const downloadUrl: string = window.URL.createObjectURL(blob);
50+
a.href = downloadUrl;
51+
a.download = fileName;
52+
document.body.appendChild(a);
53+
a.click();
54+
55+
// Clean up
56+
document.body.removeChild(a);
57+
window.URL.revokeObjectURL(downloadUrl);
58+
}
59+
catch (error) {
60+
console.error('Error downloading the file:', error);
61+
}
62+
}
63+
else if (linksArray.length > 1) {
64+
// Multiple links: create a zip file
65+
const zip = new JSZip();
66+
const fileNamesSet = new Set<string>(); // To track file names for duplicates
67+
68+
await Promise.all(
69+
linksArray.map(async (linkUrl: string) => {
70+
try {
71+
const response: Response = await fetch(linkUrl);
72+
if (!response.ok) {
73+
throw new Error(`Failed to fetch ${linkUrl}`);
74+
}
75+
const blob: Blob = await response.blob();
76+
77+
// Extract filename from URL
78+
let fileName: string = linkUrl.split('/').pop() || 'file';
79+
80+
// Get unique filename if duplicate exists
81+
fileName = getUniqueFileName(fileNamesSet, fileName);
82+
83+
// Add file to the zip
84+
zip.file(fileName, blob);
85+
}
86+
catch (error) {
87+
console.error(`Error downloading file from ${linkUrl}:`, error);
88+
}
89+
}),
90+
);
91+
92+
// Generate the zip file and trigger download
93+
zip.generateAsync({ type: 'blob' }).then((zipBlob: Blob) => {
94+
const downloadUrl: string = window.URL.createObjectURL(zipBlob);
95+
96+
// Trigger download of the zip file
97+
const a: HTMLAnchorElement = document.createElement('a');
98+
a.href = downloadUrl;
99+
a.download = 'downloaded_files.zip';
100+
document.body.appendChild(a);
101+
a.click();
102+
103+
// Clean up
104+
document.body.removeChild(a);
105+
window.URL.revokeObjectURL(downloadUrl);
106+
});
107+
}
108+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts">
2+
import { defineComponent, ref } from 'vue';
3+
import { downloadLinks } from './multi-link-downloader.service';
4+
5+
export default defineComponent({
6+
setup() {
7+
const links = ref<string>('');
8+
const downloadMultiLinks = () => {
9+
if (links.value) {
10+
downloadLinks(links.value);
11+
}
12+
};
13+
14+
const clearInput = () => {
15+
links.value = '';
16+
};
17+
18+
return {
19+
links,
20+
downloadMultiLinks,
21+
clearInput,
22+
};
23+
},
24+
});
25+
</script>
26+
27+
<template>
28+
<c-card>
29+
<div class="mb-4 flex justify-between">
30+
<c-button
31+
class="mr-2"
32+
:disabled="!links"
33+
@click="downloadMultiLinks"
34+
>
35+
Start Download
36+
</c-button>
37+
<c-button
38+
class="ml-2"
39+
@click="clearInput"
40+
>
41+
Clear
42+
</c-button>
43+
</div>
44+
45+
<c-input-text
46+
v-model:value="links"
47+
placeholder="Add links separated by new lines..."
48+
multiline
49+
:rows="20"
50+
/>
51+
</c-card>
52+
</template>

0 commit comments

Comments
 (0)