grantthomas-nuxt 1.0.27 → 1.0.29

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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@grantthomas/nuxt",
3
3
  "configKey": "grantThomasNuxt",
4
- "version": "1.0.27",
4
+ "version": "1.0.29",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -3,7 +3,14 @@ export default _default;
3
3
  declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  imageUrl: {
5
5
  type: StringConstructor;
6
- required: true;
6
+ default: null;
7
+ };
8
+ imageFile: {
9
+ type: {
10
+ new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
11
+ prototype: File;
12
+ };
13
+ default: null;
7
14
  };
8
15
  aspectRatio: {
9
16
  type: StringConstructor;
@@ -13,13 +20,39 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
13
20
  type: StringConstructor;
14
21
  default: null;
15
22
  };
23
+ mimeType: {
24
+ type: StringConstructor;
25
+ default: null;
26
+ };
27
+ outputMimeType: {
28
+ type: StringConstructor;
29
+ default: null;
30
+ };
31
+ quality: {
32
+ type: NumberConstructor;
33
+ default: number;
34
+ };
35
+ fileName: {
36
+ type: StringConstructor;
37
+ default: null;
38
+ };
16
39
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
40
+ error: (...args: any[]) => void;
17
41
  cancel: (...args: any[]) => void;
42
+ change: (...args: any[]) => void;
18
43
  crop: (...args: any[]) => void;
44
+ ready: (...args: any[]) => void;
19
45
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
20
46
  imageUrl: {
21
47
  type: StringConstructor;
22
- required: true;
48
+ default: null;
49
+ };
50
+ imageFile: {
51
+ type: {
52
+ new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
53
+ prototype: File;
54
+ };
55
+ default: null;
23
56
  };
24
57
  aspectRatio: {
25
58
  type: StringConstructor;
@@ -29,10 +62,35 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
29
62
  type: StringConstructor;
30
63
  default: null;
31
64
  };
65
+ mimeType: {
66
+ type: StringConstructor;
67
+ default: null;
68
+ };
69
+ outputMimeType: {
70
+ type: StringConstructor;
71
+ default: null;
72
+ };
73
+ quality: {
74
+ type: NumberConstructor;
75
+ default: number;
76
+ };
77
+ fileName: {
78
+ type: StringConstructor;
79
+ default: null;
80
+ };
32
81
  }>> & Readonly<{
82
+ onError?: ((...args: any[]) => any) | undefined;
33
83
  onCancel?: ((...args: any[]) => any) | undefined;
84
+ onChange?: ((...args: any[]) => any) | undefined;
34
85
  onCrop?: ((...args: any[]) => any) | undefined;
86
+ onReady?: ((...args: any[]) => any) | undefined;
35
87
  }>, {
88
+ imageUrl: string;
89
+ imageFile: File;
36
90
  aspectRatio: string;
37
91
  defaultAspectRatio: string;
92
+ mimeType: string;
93
+ outputMimeType: string;
94
+ quality: number;
95
+ fileName: string;
38
96
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,11 +1,16 @@
1
1
  <script setup>
2
- import { ref, computed } from "vue";
2
+ import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
3
3
  import { Cropper } from "vue-advanced-cropper";
4
4
  import "vue-advanced-cropper/dist/style.css";
5
+ import { heicTo } from "heic-to";
5
6
  const props = defineProps({
6
7
  imageUrl: {
7
8
  type: String,
8
- required: true
9
+ default: null
10
+ },
11
+ imageFile: {
12
+ type: File,
13
+ default: null
9
14
  },
10
15
  aspectRatio: {
11
16
  type: String,
@@ -14,10 +19,30 @@ const props = defineProps({
14
19
  defaultAspectRatio: {
15
20
  type: String,
16
21
  default: null
22
+ },
23
+ mimeType: {
24
+ type: String,
25
+ default: null
26
+ },
27
+ outputMimeType: {
28
+ type: String,
29
+ default: null
30
+ },
31
+ quality: {
32
+ type: Number,
33
+ default: 0.9
34
+ },
35
+ fileName: {
36
+ type: String,
37
+ default: null
17
38
  }
18
39
  });
19
- const emit = defineEmits(["crop", "cancel"]);
40
+ const emit = defineEmits(["crop", "cancel", "ready", "change", "error"]);
20
41
  const cropper = ref(null);
42
+ const processedImageUrl = ref(null);
43
+ const convertingHeic = ref(false);
44
+ const actualMimeType = ref(null);
45
+ const actualFileName = ref(null);
21
46
  const aspectRatioOptions = [
22
47
  { label: "Freeform", value: null },
23
48
  { label: "1:1 (Square)", value: "1:1" },
@@ -53,17 +78,124 @@ const stencilProps = computed(() => {
53
78
  }
54
79
  return props2;
55
80
  });
81
+ const finalOutputMimeType = computed(() => {
82
+ const type = props.outputMimeType || actualMimeType.value || props.mimeType || "image/png";
83
+ const supportedTypes = ["image/png", "image/jpeg", "image/webp"];
84
+ if (supportedTypes.includes(type)) {
85
+ return type;
86
+ }
87
+ console.warn(`Unsupported MIME type: ${type}. Falling back to image/png`);
88
+ return "image/png";
89
+ });
90
+ const shouldUseQuality = computed(() => {
91
+ return finalOutputMimeType.value === "image/jpeg" || finalOutputMimeType.value === "image/webp";
92
+ });
56
93
  const crop = () => {
57
94
  if (cropper.value) {
58
- const { canvas } = cropper.value.getResult();
95
+ const { canvas, coordinates } = cropper.value.getResult();
59
96
  if (canvas) {
60
- const base64 = canvas.toDataURL("image/png");
61
- emit("crop", base64);
97
+ const base64 = shouldUseQuality.value ? canvas.toDataURL(finalOutputMimeType.value, props.quality) : canvas.toDataURL(finalOutputMimeType.value);
98
+ emit("crop", base64, {
99
+ mimeType: finalOutputMimeType.value,
100
+ quality: props.quality,
101
+ fileName: props.fileName,
102
+ dimensions: {
103
+ width: canvas.width,
104
+ height: canvas.height
105
+ },
106
+ coordinates,
107
+ estimatedSize: Math.round(base64.length * 3 / 4)
108
+ // Rough base64 to bytes conversion
109
+ });
62
110
  }
63
111
  }
64
112
  };
65
113
  const cancel = () => {
66
114
  emit("cancel");
115
+ if (processedImageUrl.value && !props.imageUrl) {
116
+ URL.revokeObjectURL(processedImageUrl.value);
117
+ }
118
+ };
119
+ const isHeicFile = (file) => {
120
+ if (!file) return false;
121
+ return file.type === "image/heic" || file.type === "image/heic-sequence" || file.name.toLowerCase().endsWith(".heic") || file.name.toLowerCase().endsWith(".heics");
122
+ };
123
+ const convertHeicToJpeg = async (file) => {
124
+ try {
125
+ convertingHeic.value = true;
126
+ const convertedBlob = await heicTo({
127
+ blob: file,
128
+ type: "image/jpeg",
129
+ quality: 0.9
130
+ });
131
+ return new File(
132
+ [convertedBlob],
133
+ file.name.replace(/\.heic$/i, ".jpg"),
134
+ { type: "image/jpeg" }
135
+ );
136
+ } catch (error) {
137
+ console.error("HEIC conversion failed:", error);
138
+ if (error && error.message) {
139
+ throw new Error(`HEIC conversion failed: ${error.message}`);
140
+ } else {
141
+ throw new Error("Failed to convert HEIC image. The file may be corrupted or use an unsupported format.");
142
+ }
143
+ } finally {
144
+ convertingHeic.value = false;
145
+ }
146
+ };
147
+ const processImageFile = async (file) => {
148
+ try {
149
+ let processedFile = file;
150
+ if (isHeicFile(file)) {
151
+ processedFile = await convertHeicToJpeg(file);
152
+ }
153
+ actualMimeType.value = processedFile.type;
154
+ actualFileName.value = processedFile.name;
155
+ const url = URL.createObjectURL(processedFile);
156
+ processedImageUrl.value = url;
157
+ } catch (error) {
158
+ onError(error);
159
+ }
160
+ };
161
+ watch(() => props.imageFile, async (newFile) => {
162
+ if (newFile) {
163
+ await processImageFile(newFile);
164
+ }
165
+ }, { immediate: true });
166
+ watch(() => props.imageUrl, (newUrl) => {
167
+ if (newUrl) {
168
+ processedImageUrl.value = newUrl;
169
+ actualMimeType.value = props.mimeType;
170
+ actualFileName.value = props.fileName;
171
+ }
172
+ }, { immediate: true });
173
+ onBeforeUnmount(() => {
174
+ if (processedImageUrl.value && !props.imageUrl) {
175
+ URL.revokeObjectURL(processedImageUrl.value);
176
+ }
177
+ });
178
+ const onReady = () => {
179
+ emit("ready", {
180
+ fileName: props.fileName,
181
+ mimeType: props.mimeType
182
+ });
183
+ };
184
+ const onChange = ({ coordinates, canvas }) => {
185
+ emit("change", {
186
+ coordinates,
187
+ dimensions: canvas ? {
188
+ width: canvas.width,
189
+ height: canvas.height
190
+ } : null
191
+ });
192
+ };
193
+ const onError = (error) => {
194
+ console.error("Cropper error:", error);
195
+ emit("error", {
196
+ error,
197
+ fileName: props.fileName
198
+ });
67
199
  };
68
200
  </script>
69
201
 
@@ -89,14 +221,35 @@ const cancel = () => {
89
221
  </v-card-title>
90
222
 
91
223
  <v-card-text class="pa-4">
92
- <div style="max-height: 70vh;">
224
+ <!-- HEIC Conversion Loading -->
225
+ <v-alert v-if="convertingHeic" type="info" class="mb-3" density="compact">
226
+ <div class="d-flex align-center">
227
+ <v-progress-circular
228
+ indeterminate
229
+ color="primary"
230
+ size="20"
231
+ width="2"
232
+ class="mr-2"
233
+ />
234
+ <span>Converting HEIC image to JPEG...</span>
235
+ </div>
236
+ </v-alert>
237
+
238
+ <div v-if="processedImageUrl" style="max-height: 70vh;">
93
239
  <Cropper
94
240
  ref="cropper"
95
- :src="imageUrl"
241
+ :src="processedImageUrl"
96
242
  :stencil-props="stencilProps"
97
243
  class="cropper"
244
+ @ready="onReady"
245
+ @change="onChange"
246
+ @error="onError"
98
247
  />
99
248
  </div>
249
+ <div v-else-if="!convertingHeic" class="text-center pa-4">
250
+ <v-progress-circular indeterminate color="primary" />
251
+ <p class="mt-2">Loading image...</p>
252
+ </div>
100
253
  </v-card-text>
101
254
 
102
255
  <v-card-actions class="border-t pa-4">
@@ -3,7 +3,14 @@ export default _default;
3
3
  declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
4
4
  imageUrl: {
5
5
  type: StringConstructor;
6
- required: true;
6
+ default: null;
7
+ };
8
+ imageFile: {
9
+ type: {
10
+ new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
11
+ prototype: File;
12
+ };
13
+ default: null;
7
14
  };
8
15
  aspectRatio: {
9
16
  type: StringConstructor;
@@ -13,13 +20,39 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
13
20
  type: StringConstructor;
14
21
  default: null;
15
22
  };
23
+ mimeType: {
24
+ type: StringConstructor;
25
+ default: null;
26
+ };
27
+ outputMimeType: {
28
+ type: StringConstructor;
29
+ default: null;
30
+ };
31
+ quality: {
32
+ type: NumberConstructor;
33
+ default: number;
34
+ };
35
+ fileName: {
36
+ type: StringConstructor;
37
+ default: null;
38
+ };
16
39
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
40
+ error: (...args: any[]) => void;
17
41
  cancel: (...args: any[]) => void;
42
+ change: (...args: any[]) => void;
18
43
  crop: (...args: any[]) => void;
44
+ ready: (...args: any[]) => void;
19
45
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
20
46
  imageUrl: {
21
47
  type: StringConstructor;
22
- required: true;
48
+ default: null;
49
+ };
50
+ imageFile: {
51
+ type: {
52
+ new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File;
53
+ prototype: File;
54
+ };
55
+ default: null;
23
56
  };
24
57
  aspectRatio: {
25
58
  type: StringConstructor;
@@ -29,10 +62,35 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
29
62
  type: StringConstructor;
30
63
  default: null;
31
64
  };
65
+ mimeType: {
66
+ type: StringConstructor;
67
+ default: null;
68
+ };
69
+ outputMimeType: {
70
+ type: StringConstructor;
71
+ default: null;
72
+ };
73
+ quality: {
74
+ type: NumberConstructor;
75
+ default: number;
76
+ };
77
+ fileName: {
78
+ type: StringConstructor;
79
+ default: null;
80
+ };
32
81
  }>> & Readonly<{
82
+ onError?: ((...args: any[]) => any) | undefined;
33
83
  onCancel?: ((...args: any[]) => any) | undefined;
84
+ onChange?: ((...args: any[]) => any) | undefined;
34
85
  onCrop?: ((...args: any[]) => any) | undefined;
86
+ onReady?: ((...args: any[]) => any) | undefined;
35
87
  }>, {
88
+ imageUrl: string;
89
+ imageFile: File;
36
90
  aspectRatio: string;
37
91
  defaultAspectRatio: string;
92
+ mimeType: string;
93
+ outputMimeType: string;
94
+ quality: number;
95
+ fileName: string;
38
96
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -236,7 +236,7 @@ const convertHeicToJpeg = async (file) => {
236
236
  const createImageUrl = (file) => {
237
237
  return URL.createObjectURL(file);
238
238
  };
239
- const handleCropComplete = async (croppedBase64) => {
239
+ const handleCropComplete = async (croppedBase64, metadata) => {
240
240
  cropDialog.value = false;
241
241
  if (props.multiple) {
242
242
  const upload = {
@@ -635,6 +635,8 @@ watch(selectedObjects, (newObjects) => {
635
635
  :image-url="imageToCrop"
636
636
  :aspect-ratio="aspectRatio"
637
637
  :default-aspect-ratio="defaultAspectRatio"
638
+ :mime-type="fileForCrop?.type"
639
+ :file-name="fileForCrop?.name"
638
640
  @crop="handleCropComplete"
639
641
  @cancel="handleCropCancel"
640
642
  />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grantthomas-nuxt",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Crud module for Nuxt 3 interacting with sibling .net project",
5
5
  "repository": "Tap-Leagues/GrantThomas.Nuxt",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "@nuxt/scripts": "^0.13.2",
34
34
  "@vueuse/core": "^14.2.1",
35
35
  "@vueuse/nuxt": "^14.2.1",
36
- "heic2any": "^0.0.4",
36
+ "heic-to": "^1.4.2",
37
37
  "luxon": "^3.6.1",
38
38
  "query-string": "^9.2.1",
39
39
  "ts-luxon": "^6.1.0",