From 7236b232813b399dd18186425558808aa26033e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Marseault?= <22910497+Larsluph@users.noreply.github.com> Date: Fri, 19 Jul 2024 07:27:01 -0500 Subject: [PATCH] feat(export): Zip files when exporting several torrents (#1794) --- package-lock.json | 12 +++++++ package.json | 1 + src/components/Dashboard/RightClick.vue | 44 +++++++++++++++---------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ff93501..c2368619 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mdi/font": "^7.4.47", "@mdi/js": "^7.4.47", "@vueuse/core": "^10.11.0", + "@zip.js/zip.js": "^2.7.45", "apexcharts": "^3.50.0", "axios": "^1.7.2", "dayjs": "^1.11.11", @@ -1946,6 +1947,17 @@ "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==" }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.45", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.45.tgz", + "integrity": "sha512-Mm2EXF33DJQ/3GWWEWeP1UCqzpQ5+fiMvT3QWspsXY05DyqqxWu7a9awSzU4/spHMHVFrTjani1PR0vprgZpow==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", diff --git a/package.json b/package.json index dc90d8b3..123d8232 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@mdi/font": "^7.4.47", "@mdi/js": "^7.4.47", "@vueuse/core": "^10.11.0", + "@zip.js/zip.js": "^2.7.45", "apexcharts": "^3.50.0", "axios": "^1.7.2", "dayjs": "^1.11.11", diff --git a/src/components/Dashboard/RightClick.vue b/src/components/Dashboard/RightClick.vue index 6dd9d7a6..d4b1d8d4 100644 --- a/src/components/Dashboard/RightClick.vue +++ b/src/components/Dashboard/RightClick.vue @@ -7,6 +7,7 @@ import ShareLimitDialog from '@/components/Dialogs/ShareLimitDialog.vue' import SpeedLimitDialog from '@/components/Dialogs/SpeedLimitDialog.vue' import { useCategoryStore, useDashboardStore, useDialogStore, useMaindataStore, usePreferenceStore, useTagStore, useTorrentStore } from '@/stores' import { RightClickMenuEntryType } from '@/types/vuetorrent' +import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { computed } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' @@ -117,19 +118,26 @@ function setShareLimit() { dialogStore.createDialog(ShareLimitDialog, { hashes: hashes.value }) } +function downloadFile(filename: string, blob: Blob) { + const href = window.URL.createObjectURL(blob) + const el = Object.assign(document.createElement('a'), { href, download: filename, style: { opacity: '0' } }) + document.body.appendChild(el) + el.click() + el.remove() +} + async function exportTorrents() { - hashes.value.forEach(hash => { - torrentStore.exportTorrent(hash).then(blob => { - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.style.opacity = '0' - link.setAttribute('download', `${hash}.torrent`) - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - }) - }) + const ts = [...torrents.value] + if (ts.length === 1) { + const t = ts[0]! + const blob = await torrentStore.exportTorrent(t.hash) + downloadFile(`${t.name}.torrent`, blob) + return + } + + const zipWriter = new ZipWriter(new BlobWriter('application/zip'), { bufferedWrite: true }) + await Promise.all(hashes.value.map(hash => torrentStore.exportTorrent(hash).then(blob => zipWriter.add(`${torrentStore.getTorrentByHash(hash)!.name}.torrent`, new BlobReader(blob))))) + downloadFile('torrents.zip', await zipWriter.close()) } const menuData = computed(() => [ @@ -216,12 +224,12 @@ const menuData = computed(() => [ children: [ ...(torrent.value?.tags.length ? [ - { - text: t('dashboard.right_click.tags.remove_all'), - action: () => removeAllTags().then(maindataStore.forceMaindataSync), - icon: 'mdi-playlist-remove' - } - ] + { + text: t('dashboard.right_click.tags.remove_all'), + action: () => removeAllTags().then(maindataStore.forceMaindataSync), + icon: 'mdi-playlist-remove' + } + ] : []), ...tagStore.tags.map(tag => ({ text: tag,