sprintify-ui 0.11.27 → 0.11.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ type __VLS_Props = {
7
7
  beforeUpload?: () => boolean;
8
8
  twButton?: string;
9
9
  maxSize?: number;
10
+ maxImageSizeBeforeResize?: number;
10
11
  accept?: string;
11
12
  acceptedExtensions?: string[];
12
13
  cropper?: BaseCropperConfig | Record<string, any> | boolean | null;
@@ -64,6 +65,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}
64
65
  accept: string;
65
66
  acceptedExtensions: string[];
66
67
  beforeUpload: () => boolean;
68
+ maxImageSizeBeforeResize: number;
67
69
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
68
70
  declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
69
71
  export default _default;
@@ -52,6 +52,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
52
52
  default: number;
53
53
  type: NumberConstructor;
54
54
  };
55
+ maxImageSizeBeforeResize: {
56
+ default: undefined;
57
+ type: NumberConstructor;
58
+ };
55
59
  accept: {
56
60
  default: undefined;
57
61
  type: StringConstructor;
@@ -127,6 +131,10 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
127
131
  default: number;
128
132
  type: NumberConstructor;
129
133
  };
134
+ maxImageSizeBeforeResize: {
135
+ default: undefined;
136
+ type: NumberConstructor;
137
+ };
130
138
  accept: {
131
139
  default: undefined;
132
140
  type: StringConstructor;
@@ -195,6 +203,7 @@ declare const __VLS_component: import("vue").DefineComponent<import("vue").Extra
195
203
  maxSize: number;
196
204
  accept: string;
197
205
  acceptedExtensions: string[];
206
+ maxImageSizeBeforeResize: number;
198
207
  currentMedia: Media[];
199
208
  uploadUrl: string;
200
209
  pickerComponent: "BaseFilePicker" | "BaseFilePickerCrop";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.11.27",
3
+ "version": "0.11.28",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -35,6 +35,7 @@ import { UploadedFile } from '@/types/UploadedFile';
35
35
  import { useSnackbarsStore } from '@/stores/snackbars';
36
36
  import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
37
37
  import { BaseCropperConfig } from '@/types';
38
+ import { base64ToBlob, blobToBase64, resizeImageFromURI } from '@/utils';
38
39
  import BaseFilePicker from './BaseFilePicker.vue';
39
40
  import BaseFilePickerCrop from './BaseFilePickerCrop.vue';
40
41
  import { t } from '@/i18n';
@@ -51,6 +52,7 @@ const props = withDefaults(
51
52
  beforeUpload?: () => boolean;
52
53
  twButton?: string;
53
54
  maxSize?: number;
55
+ maxImageSizeBeforeResize?: number;
54
56
  accept?: string;
55
57
  acceptedExtensions?: string[];
56
58
  cropper?: BaseCropperConfig | Record<string, any> | boolean | null;
@@ -66,6 +68,7 @@ const props = withDefaults(
66
68
  },
67
69
  twButton: '',
68
70
  maxSize: undefined,
71
+ maxImageSizeBeforeResize: undefined,
69
72
  accept: undefined,
70
73
  acceptedExtensions: undefined,
71
74
  cropper: true,
@@ -149,15 +152,16 @@ async function onFileSelect(files: File | File[]) {
149
152
  }
150
153
 
151
154
  async function processFileUpload(file: File): Promise<UploadedFile> {
155
+ const preparedFile = await processFileBeforeUpload(file);
152
156
 
153
157
  const formData = new FormData();
154
158
 
155
- formData.append('file', file);
159
+ formData.append('file', preparedFile);
156
160
 
157
161
  const response = await http.post(props.url ?? config.upload_url, formData);
158
162
 
159
163
  const payload = response.data as UploadedFile;
160
- payload.original_file = file;
164
+ payload.original_file = preparedFile;
161
165
 
162
166
  // Read file if image, add add data_url to payload
163
167
 
@@ -175,11 +179,137 @@ async function processFileUpload(file: File): Promise<UploadedFile> {
175
179
  };
176
180
 
177
181
  if (payload.mime_type.includes('image')) {
178
- reader.readAsDataURL(file);
182
+ reader.readAsDataURL(preparedFile);
179
183
  } else {
180
184
  resolve(payload);
181
185
  }
182
186
  });
183
187
  }
184
188
 
189
+ async function processFileBeforeUpload(file: File): Promise<File> {
190
+ if (!shouldAutoResize(file)) {
191
+ return file;
192
+ }
193
+
194
+ return await autoResizeImage(file);
195
+ }
196
+
197
+ function shouldAutoResize(file: File): boolean {
198
+ if (!props.maxImageSizeBeforeResize || props.maxImageSizeBeforeResize <= 0) {
199
+ return false;
200
+ }
201
+
202
+ if (!file.type.includes('image')) {
203
+ return false;
204
+ }
205
+
206
+ if (['image/gif', 'image/svg+xml'].includes(file.type)) {
207
+ return false;
208
+ }
209
+
210
+ if (file.size <= props.maxImageSizeBeforeResize) {
211
+ return false;
212
+ }
213
+
214
+ return true;
215
+ }
216
+
217
+ async function autoResizeImage(file: File): Promise<File> {
218
+ if (!props.maxImageSizeBeforeResize) {
219
+ return file;
220
+ }
221
+
222
+ try {
223
+ const source = await blobToBase64(file);
224
+ const dimensions = await getImageDimensions(source);
225
+
226
+ const resizeRatio = Math.sqrt(props.maxImageSizeBeforeResize / file.size);
227
+
228
+ let targetWidth = Math.max(1, Math.floor(dimensions.width * resizeRatio));
229
+ let targetHeight = Math.max(1, Math.floor(dimensions.height * resizeRatio));
230
+
231
+ let resizedBlob = await createResizedBlob(
232
+ source,
233
+ targetHeight,
234
+ targetWidth,
235
+ file.type
236
+ );
237
+
238
+ let attempts = 0;
239
+
240
+ while (
241
+ resizedBlob.size > props.maxImageSizeBeforeResize &&
242
+ attempts < 5 &&
243
+ targetWidth > 1 &&
244
+ targetHeight > 1
245
+ ) {
246
+ targetWidth = Math.max(1, Math.floor(targetWidth * 0.8));
247
+ targetHeight = Math.max(1, Math.floor(targetHeight * 0.8));
248
+ resizedBlob = await createResizedBlob(
249
+ source,
250
+ targetHeight,
251
+ targetWidth,
252
+ file.type
253
+ );
254
+ attempts++;
255
+ }
256
+
257
+ if (resizedBlob.size >= file.size) {
258
+ return file;
259
+ }
260
+
261
+ return new File(
262
+ [resizedBlob],
263
+ getResizedFileName(file.name, resizedBlob.type),
264
+ {
265
+ type: resizedBlob.type || file.type,
266
+ lastModified: file.lastModified,
267
+ }
268
+ );
269
+ } catch (error) {
270
+ console.error(error);
271
+ return file;
272
+ }
273
+ }
274
+
275
+ async function createResizedBlob(
276
+ source: string,
277
+ height: number,
278
+ width: number,
279
+ mimeType: string
280
+ ): Promise<Blob> {
281
+ const resizedSource = await resizeImageFromURI(source, height, width);
282
+ return await base64ToBlob(resizedSource, `data:${mimeType}`);
283
+ }
284
+
285
+ async function getImageDimensions(
286
+ source: string
287
+ ): Promise<{ width: number; height: number }> {
288
+ return await new Promise((resolve, reject) => {
289
+ const image = new Image();
290
+ image.onload = () => {
291
+ resolve({
292
+ width: image.width,
293
+ height: image.height,
294
+ });
295
+ };
296
+ image.onerror = () => {
297
+ reject(new Error('Failed to load image dimensions'));
298
+ };
299
+ image.src = source;
300
+ });
301
+ }
302
+
303
+ function getResizedFileName(filename: string, mimeType: string): string {
304
+ if (mimeType != 'image/jpeg') {
305
+ return filename;
306
+ }
307
+
308
+ if (filename.includes('.')) {
309
+ return filename.replace(/\.[^.]+$/, '.jpg');
310
+ }
311
+
312
+ return `${filename}.jpg`;
313
+ }
314
+
185
315
  </script>
@@ -3,6 +3,7 @@
3
3
  <BaseFileUploader
4
4
  :component="pickerComponent"
5
5
  :max-size="maxSize"
6
+ :max-image-size-before-resize="maxImageSizeBeforeResize"
6
7
  :disabled="disabledInternal"
7
8
  class="w-full"
8
9
  tw-button="w-full"
@@ -142,6 +143,10 @@ const props = defineProps({
142
143
  default: 20 * 1024 * 1024,
143
144
  type: Number,
144
145
  },
146
+ maxImageSizeBeforeResize: {
147
+ default: undefined,
148
+ type: Number,
149
+ },
145
150
  accept: {
146
151
  default: undefined,
147
152
  type: String,
@@ -11,7 +11,7 @@
11
11
  class="fixed inset-0 z-modal w-full overflow-y-auto overflow-x-hidden"
12
12
  >
13
13
  <div class="flex min-h-full w-full pt-20 sm:pt-0">
14
- <div class="min-h-full grow">
14
+ <div class="min-h-full overflow-hidden grow">
15
15
  <transition
16
16
  appear
17
17
  enter-active-class="duration-200 ease-out"