Skip to content

Commit 6a1f2c7

Browse files
authored
Merge pull request #32 from NMSCD/dev
Add error handling; paginate concurrent compressions
2 parents 756212f + 7085b65 commit 6a1f2c7

21 files changed

+442
-1710
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919

2020
steps:
2121
- name: Checkout Repo
22-
uses: actions/checkout@v3
22+
uses: actions/checkout@v4
2323

2424
- name: Build App
2525
run: |

.github/workflows/test-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919

2020
steps:
2121
- name: Checkout Repo
22-
uses: actions/checkout@v3
22+
uses: actions/checkout@v4
2323

2424
- name: Test Build
2525
uses: Lenni009/test-build-vite-action@main

.vscode/extensions.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

package-lock.json

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

package.json

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,25 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "run-p type-check build-only",
8+
"build": "vue-tsc && vite build",
99
"preview": "vite preview",
10-
"build-only": "vite build",
11-
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
1210
"format": "prettier --write src/"
1311
},
1412
"dependencies": {
15-
"@picocss/pico": "^2.0.0",
13+
"@picocss/pico": "^2.0.6",
1614
"jszip": "^3.10.1",
1715
"pinia": "^2.1.6",
18-
"sass": "^1.64.1",
19-
"simple-image-compressor": "^1.0.1",
20-
"vue": "^3.3.4",
21-
"vue-i18n": "^9.8.0"
16+
"sass": "^1.71.1",
17+
"simple-image-compressor": "^1.4.2",
18+
"vue": "^3.4.21",
19+
"vue-i18n": "^9.10.1"
2220
},
2321
"devDependencies": {
24-
"@tsconfig/node20": "^20.1.2",
25-
"@types/node": "^20.10.5",
22+
"@types/node": "^20.11.25",
2623
"@vitejs/plugin-vue": "^5.0.3",
27-
"@vue/tsconfig": "^0.5.1",
28-
"npm-run-all": "^4.1.5",
2924
"prettier": "^3.0.0",
30-
"typescript": "^5.3.3",
31-
"vite": "^5.0.10",
32-
"vue-tsc": "^1.8.6"
25+
"typescript": "^5.4.2",
26+
"vite": "^5.1.5",
27+
"vue-tsc": "^2.0.6"
3328
}
3429
}

src/App.vue

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import FileItem from './components/FileItem.vue';
55
import { useFileDataStore } from './stores/fileData';
66
import { storeToRefs } from 'pinia';
77
import type { FileObj } from './types/file';
8-
import { computed, ref, watch } from 'vue';
9-
import { useImageCompression } from './composables/useImageCompression';
10-
import { useZipCompression } from './composables/useZipCompression';
8+
import { computed, ref } from 'vue';
9+
import { compressFile } from './functions/imageCompression';
10+
import { compressToZip } from './functions/zipCompression';
11+
import { paginate } from './functions/paginate';
1112
import { useI18n } from './hooks/useI18n';
1213
1314
const { t } = useI18n();
@@ -20,44 +21,46 @@ const zipData = ref('');
2021
const isZipCompressing = ref(false);
2122
const anyUncompressed = computed(() => files.value.some((file) => !file.isCompressed));
2223
24+
const availableThreads = Math.max(navigator.hardwareConcurrency - 2, 1);
25+
2326
async function editFileObj(fileObj: FileObj) {
2427
try {
25-
const compressedFile = await useImageCompression(fileObj.file);
26-
const item = files.value.find((item) => item.id === fileObj.id);
27-
if (!item) return;
28-
item.file = compressedFile;
29-
item.isCompressed = true;
28+
const compressedFile = await compressFile(fileObj.file);
29+
fileObj.file = compressedFile;
30+
fileObj.isCompressed = true;
3031
} catch {
31-
fileObj.isTooLarge = true;
32+
fileObj.isError = true;
3233
}
3334
}
3435
3536
async function compressFiles() {
3637
isCompressing.value = true;
3738
38-
const promises = [];
39-
4039
const uncompressedFiles = files.value.filter((fileObj: FileObj) => !fileObj.isCompressed);
4140
42-
for (const fileObj of uncompressedFiles) {
43-
promises.push(editFileObj(fileObj));
41+
// option 1 (no errors, slower)
42+
const paginatedFileArray = paginate(uncompressedFiles, availableThreads);
43+
44+
for (const subArray of paginatedFileArray) {
45+
const promises = subArray.map(editFileObj);
46+
await Promise.all(promises);
4447
}
4548
46-
await Promise.all(promises);
49+
// option 2 (errors, faster -> better option once the errors are fixed in Firefox)
50+
// const promises = uncompressedFiles.map(editFileObj);
51+
// await Promise.all(promises);
52+
4753
isCompressing.value = false;
54+
55+
URL.revokeObjectURL(zipData.value);
56+
isZipCompressing.value = true;
57+
zipData.value = await compressToZip();
58+
isZipCompressing.value = false;
4859
}
4960
5061
function removeItem(file: FileObj) {
5162
files.value = files.value.filter((item) => item !== file);
5263
}
53-
54-
watch(anyUncompressed, async (newVal) => {
55-
if (files.value.length && !newVal) {
56-
isZipCompressing.value = true;
57-
zipData.value = await useZipCompression();
58-
isZipCompressing.value = false;
59-
}
60-
});
6164
</script>
6265

6366
<template>
@@ -92,7 +95,7 @@ watch(anyUncompressed, async (newVal) => {
9295
</button>
9396
<a
9497
:aria-busy="isZipCompressing"
95-
:aria-disabled="!zipData"
98+
:disabled="!zipData || undefined"
9699
:href="zipData || undefined"
97100
role="button"
98101
download

src/components/FileItem.vue

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, ref, watchEffect } from 'vue';
2+
import { computed, ref, watchEffect, watch, onUnmounted } from 'vue';
33
import type { FileObj } from '@/types/file';
44
import { useI18n } from '@/hooks/useI18n';
55
@@ -21,57 +21,65 @@ watchEffect(() => {
2121
}
2222
});
2323
24-
const fileData = computed(() => URL.createObjectURL(props.fileObj.file));
24+
const objectUrl = computed(() => URL.createObjectURL(props.fileObj.file));
25+
26+
watch(objectUrl, (_, oldUrl) => URL.revokeObjectURL(oldUrl));
2527
2628
const computeFileSize = (size: number) =>
2729
(size / (1024 * 1024)).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 }); // NoSonar this is to convert byte to MB
30+
31+
onUnmounted(() => URL.revokeObjectURL(objectUrl.value));
2832
</script>
2933

3034
<template>
3135
<div class="file-item">
32-
<a
33-
:href="fileData"
34-
rel="noopener noreferrer"
35-
target="_blank"
36-
>
37-
<img
38-
:src="fileData"
39-
alt="Image preview"
40-
class="preview"
41-
width="200"
42-
/>
43-
</a>
44-
<div>
45-
<div>
46-
<span class="field-title">{{ t('translation.name') }}</span> {{ fileObj.file.name }}
47-
</div>
36+
<div class="item-set">
37+
<a
38+
:href="objectUrl"
39+
rel="noopener noreferrer"
40+
target="_blank"
41+
>
42+
<img
43+
:src="objectUrl"
44+
alt="Image preview"
45+
class="preview"
46+
width="200"
47+
/>
48+
</a>
4849
<div>
49-
<span class="field-title">{{ t('translation.originalsize') }}</span> {{ computeFileSize(orgSize) }}MB
50-
</div>
51-
<div v-if="compSize">
52-
<span class="field-title">{{ t('translation.compressedsize') }}</span> {{ computeFileSize(compSize) }}MB
50+
<div>
51+
<span class="field-title">{{ t('translation.name') }}</span> {{ fileObj.file.name }}
52+
</div>
53+
<div>
54+
<span class="field-title">{{ t('translation.originalsize') }}</span> {{ computeFileSize(orgSize) }}MB
55+
</div>
56+
<div v-if="compSize">
57+
<span class="field-title">{{ t('translation.compressedsize') }}</span> {{ computeFileSize(compSize) }}MB
58+
</div>
59+
<div
60+
v-if="fileObj.isError"
61+
class="error"
62+
>
63+
<span class="field-title">{{ t('translation.error') }}</span> {{ t('translation.hasFailed') }}
64+
</div>
5365
</div>
54-
<div
55-
v-if="fileObj.isTooLarge"
56-
class="error"
66+
</div>
67+
<div class="item-set">
68+
<a
69+
:class="{ secondary: !fileObj.isCompressed }"
70+
:disabled="!fileObj.isCompressed || undefined"
71+
:download="fileObj.file.name"
72+
:href="fileObj.isCompressed ? objectUrl : undefined"
73+
role="button"
74+
>{{ t('translation.download') }}</a
5775
>
58-
<span class="field-title">{{ t('translation.error') }}</span> {{ t('translation.filetoolarge') }}
59-
</div>
76+
<button
77+
class="secondary delete-button"
78+
@click="$emit('remove')"
79+
>
80+
81+
</button>
6082
</div>
61-
<a
62-
:class="{ secondary: !fileObj.isCompressed }"
63-
:disabled="!fileObj.isCompressed || undefined"
64-
:href="fileObj.isCompressed ? fileData : undefined"
65-
role="button"
66-
download
67-
>{{ t('translation.download') }}</a
68-
>
69-
<button
70-
class="secondary delete-button"
71-
@click="$emit('remove')"
72-
>
73-
74-
</button>
7583
</div>
7684
</template>
7785

@@ -85,9 +93,15 @@ const computeFileSize = (size: number) =>
8593
padding-inline: 1rem;
8694
align-items: center;
8795
width: 100%;
88-
justify-content: center;
96+
justify-content: space-between;
8997
flex-wrap: wrap;
9098
99+
.item-set {
100+
display: flex;
101+
gap: 1rem;
102+
flex-wrap: wrap;
103+
}
104+
91105
.field-title {
92106
font-weight: bold;
93107
}

src/components/FileUpload.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function addFiles(uploadedFiles: FileList) {
3232
const fileObj: FileObj = {
3333
id: id++,
3434
isCompressed: false,
35-
isTooLarge: false,
35+
isError: false,
3636
file,
3737
};
3838
files.value.push(fileObj);

src/composables/useImageCompression.ts renamed to src/functions/imageCompression.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { compressImage, imageTypes } from 'simple-image-compressor';
22
import { maxSize } from '@/variables/constants';
33

4-
export async function useImageCompression(file: File, quality: number = 1): Promise<File> {
4+
export async function compressFile(file: File, quality: number = 1): Promise<File> {
55
if (file.size < maxSize) return file; // if below 10 MB, don't do anything
66
const res = await compressImage(file, {
77
quality,
88
type: imageTypes.JPEG,
99
});
1010
const lowerQuality = quality - 0.01; // NoSonar reduce quality by 1%;
11-
if (res.size > maxSize) return await useImageCompression(file, lowerQuality); // compress original file with lower quality setting to avoid double compression
11+
if (res.size > maxSize) return await compressFile(file, lowerQuality); // compress original file with lower quality setting to avoid double compression
1212
const fileName = file.name.split('.').slice(0, -1).join('.');
1313
const newFileName = fileName + '-min.jpg';
1414
return new File([res], newFileName, { type: imageTypes.JPEG });

src/functions/paginate.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { FileObj } from '@/types/file';
2+
3+
export function paginate(files: FileObj[], chunkSize: number): FileObj[][] {
4+
const paginatedArray: FileObj[][] = files.reduce((resultArray: FileObj[][], file, index) => {
5+
const chunkIndex = Math.floor(index / chunkSize);
6+
7+
resultArray[chunkIndex] ??= []; // start a new chunk
8+
9+
resultArray[chunkIndex].push(file);
10+
11+
return resultArray;
12+
}, []);
13+
14+
return paginatedArray;
15+
}

0 commit comments

Comments
 (0)