grantthomas-nuxt 1.0.2 → 1.0.3

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.2",
4
+ "version": "1.0.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -13,6 +13,8 @@ declare const __VLS_component: import("vue").DefineComponent<{}, {
13
13
  replaceable: boolean;
14
14
  isPublic: boolean;
15
15
  additionalPostData: Record<string, any>;
16
+ crop: boolean;
17
+ aspectRatio: string;
16
18
  title?: any;
17
19
  $props: any;
18
20
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -5,6 +5,7 @@ import { useCrudConverters } from "../composables/useCrudConverters";
5
5
  import CrudErrorDisplay from "../components/CrudErrorDisplay.vue";
6
6
  import CrudListLoader from "./CrudListLoader.vue";
7
7
  import CrudUploadFieldSelection from "./CrudUploadFieldSelection.vue";
8
+ import CrudImageCropper from "./CrudImageCropper.vue";
8
9
  const { toBase64, getFileType } = useCrudConverters();
9
10
  const props = defineProps({
10
11
  replaceable: {
@@ -47,6 +48,14 @@ const props = defineProps({
47
48
  default: () => {
48
49
  return {};
49
50
  }
51
+ },
52
+ crop: {
53
+ type: Boolean,
54
+ default: false
55
+ },
56
+ aspectRatio: {
57
+ type: String,
58
+ default: "1:1"
50
59
  }
51
60
  });
52
61
  const model = defineModel();
@@ -55,6 +64,11 @@ const filesToUpload = ref([]);
55
64
  const fileToUpload = ref(null);
56
65
  const searchValue = ref(null);
57
66
  const tempModel = ref(null);
67
+ const cropDialog = ref(false);
68
+ const imageToCrop = ref(null);
69
+ const fileForCrop = ref(null);
70
+ const cropIndex = ref(null);
71
+ const convertingHeic = ref(false);
58
72
  const cloneModel = () => props.multiple ? [...model.value || []] : model.value;
59
73
  const finish = () => {
60
74
  model.value = props.multiple ? [...tempModel.value] : tempModel.value;
@@ -81,10 +95,123 @@ const {
81
95
  hasFormErrors: hasUploadErrors,
82
96
  formErrors: uploadErrors
83
97
  } = useCrudApi(props.path);
98
+ const isImageFile = (file) => {
99
+ if (!file) return false;
100
+ return file.type && file.type.startsWith("image/");
101
+ };
102
+ const isHeicFile = (file) => {
103
+ if (!file) return false;
104
+ return file.type === "image/heic" || file.type === "image/heic-sequence" || file.name.toLowerCase().endsWith(".heic") || file.name.toLowerCase().endsWith(".heics");
105
+ };
106
+ const convertHeicToJpeg = async (file) => {
107
+ try {
108
+ convertingHeic.value = true;
109
+ const heic2any = (await import("heic2any")).default;
110
+ const convertedBlob = await heic2any({
111
+ blob: file,
112
+ toType: "image/jpeg",
113
+ quality: 0.9
114
+ });
115
+ const blob = Array.isArray(convertedBlob) ? convertedBlob[0] : convertedBlob;
116
+ return new File(
117
+ [blob],
118
+ file.name.replace(/\.heic$/i, ".jpg"),
119
+ { type: "image/jpeg" }
120
+ );
121
+ } catch (error) {
122
+ console.error("HEIC conversion failed:", error);
123
+ if (error.code === 2 || error.message?.includes("format not supported")) {
124
+ throw new Error("This HEIC file format is not supported. Please use a JPEG or PNG instead.");
125
+ } else if (error.code === 1 || error.message?.includes("corrupt")) {
126
+ throw new Error("This HEIC file appears to be corrupted. Please try a different file.");
127
+ } else {
128
+ throw new Error("Failed to convert HEIC image. Please try converting to JPEG/PNG first.");
129
+ }
130
+ } finally {
131
+ convertingHeic.value = false;
132
+ }
133
+ };
134
+ const createImageUrl = (file) => {
135
+ return URL.createObjectURL(file);
136
+ };
137
+ const handleCropComplete = async (croppedBase64) => {
138
+ cropDialog.value = false;
139
+ if (props.multiple) {
140
+ const upload = {
141
+ base64Data: croppedBase64,
142
+ uploadType: "image",
143
+ originalFilename: fileForCrop.value.name,
144
+ ...props.additionalPostData
145
+ };
146
+ await createUpload(upload);
147
+ if (!hasUploadErrors.value && hasItem.value) {
148
+ onSelectImage(completedUpload.value.id);
149
+ }
150
+ const remainingFiles = filesToUpload.value.slice(cropIndex.value + 1);
151
+ filesToUpload.value = remainingFiles;
152
+ } else {
153
+ const upload = {
154
+ base64Data: croppedBase64,
155
+ uploadType: "image",
156
+ originalFilename: fileForCrop.value.name,
157
+ ...props.additionalPostData
158
+ };
159
+ await createUpload(upload);
160
+ if (!hasUploadErrors.value && hasItem.value) {
161
+ model.value = completedUpload.value.id;
162
+ dialog.value = false;
163
+ }
164
+ fileToUpload.value = null;
165
+ }
166
+ if (imageToCrop.value) {
167
+ URL.revokeObjectURL(imageToCrop.value);
168
+ }
169
+ imageToCrop.value = null;
170
+ fileForCrop.value = null;
171
+ cropIndex.value = null;
172
+ uploadsLoading.value = false;
173
+ };
174
+ const handleCropCancel = () => {
175
+ cropDialog.value = false;
176
+ if (imageToCrop.value) {
177
+ URL.revokeObjectURL(imageToCrop.value);
178
+ }
179
+ imageToCrop.value = null;
180
+ fileForCrop.value = null;
181
+ cropIndex.value = null;
182
+ if (props.multiple) {
183
+ filesToUpload.value = [];
184
+ } else {
185
+ fileToUpload.value = null;
186
+ }
187
+ uploadsLoading.value = false;
188
+ };
84
189
  const processUploads = async () => {
85
190
  uploadsLoading.value = true;
191
+ uploadErrors.value = {};
86
192
  if (props.multiple) {
87
- for (let file of filesToUpload.value) {
193
+ for (let i = 0; i < filesToUpload.value.length; i++) {
194
+ let file = filesToUpload.value[i];
195
+ if (isHeicFile(file)) {
196
+ try {
197
+ file = await convertHeicToJpeg(file);
198
+ } catch (error) {
199
+ console.error("Error converting HEIC:", error);
200
+ uploadErrors.value = {
201
+ conversion: [error.message]
202
+ };
203
+ uploadsLoading.value = false;
204
+ filesToUpload.value = [];
205
+ return;
206
+ }
207
+ }
208
+ if (props.crop && isImageFile(file)) {
209
+ cropIndex.value = i;
210
+ fileForCrop.value = file;
211
+ imageToCrop.value = createImageUrl(file);
212
+ cropDialog.value = true;
213
+ return;
214
+ }
88
215
  let upload = {
89
216
  base64Data: await toBase64(file),
90
217
  uploadType: getFileType(file),
@@ -96,7 +223,27 @@ const processUploads = async () => {
96
223
  onSelectImage(completedUpload.value.id);
97
224
  }
98
225
  }
226
+ filesToUpload.value = [];
99
227
  } else {
228
+ if (isHeicFile(fileToUpload.value)) {
229
+ try {
230
+ fileToUpload.value = await convertHeicToJpeg(fileToUpload.value);
231
+ } catch (error) {
232
+ console.error("Error converting HEIC:", error);
233
+ uploadErrors.value = {
234
+ conversion: [error.message]
235
+ };
236
+ uploadsLoading.value = false;
237
+ fileToUpload.value = null;
238
+ return;
239
+ }
240
+ }
241
+ if (props.crop && isImageFile(fileToUpload.value)) {
242
+ fileForCrop.value = fileToUpload.value;
243
+ imageToCrop.value = createImageUrl(fileToUpload.value);
244
+ cropDialog.value = true;
245
+ return;
246
+ }
100
247
  let upload = {
101
248
  base64Data: await toBase64(fileToUpload.value),
102
249
  uploadType: getFileType(fileToUpload.value),
@@ -108,13 +255,9 @@ const processUploads = async () => {
108
255
  model.value = completedUpload.value.id;
109
256
  dialog.value = false;
110
257
  }
111
- }
112
- uploadsLoading.value = false;
113
- if (props.multiple) {
114
- filesToUpload.value = [];
115
- } else {
116
258
  fileToUpload.value = null;
117
259
  }
260
+ uploadsLoading.value = false;
118
261
  };
119
262
  const selectionFilters = computed(() => {
120
263
  let filters = {};
@@ -135,12 +278,18 @@ watch(() => dialog.value, (val) => {
135
278
  tempModel.value = cloneModel();
136
279
  }
137
280
  });
138
- watch(() => filesToUpload.value, () => {
281
+ watch(() => filesToUpload.value, (newVal, oldVal) => {
282
+ if (newVal !== oldVal) {
283
+ uploadErrors.value = {};
284
+ }
139
285
  if (filesToUpload.value.length > 0) {
140
286
  processUploads();
141
287
  }
142
288
  });
143
- watch(() => fileToUpload.value, () => {
289
+ watch(() => fileToUpload.value, (newVal, oldVal) => {
290
+ if (newVal !== oldVal) {
291
+ uploadErrors.value = {};
292
+ }
144
293
  if (fileToUpload.value) {
145
294
  processUploads();
146
295
  }
@@ -190,6 +339,21 @@ watch(() => fileToUpload.value, () => {
190
339
  <div class="flex-wrap">
191
340
  <div class="border pa-3 rounded mb-3">
192
341
  <h4 class="mb-4">Upload new media</h4>
342
+
343
+ <!-- HEIC Conversion Indicator -->
344
+ <v-alert v-if="convertingHeic" type="info" class="mb-3" density="compact">
345
+ <div class="d-flex align-center">
346
+ <v-progress-circular
347
+ indeterminate
348
+ color="primary"
349
+ size="20"
350
+ width="2"
351
+ class="mr-2"
352
+ />
353
+ <span>Converting HEIC image to JPEG...</span>
354
+ </div>
355
+ </v-alert>
356
+
193
357
  <div class="d-flex">
194
358
  <div class="flex-fill">
195
359
  <v-file-input
@@ -307,5 +471,14 @@ watch(() => fileToUpload.value, () => {
307
471
  </div>
308
472
  </v-card>
309
473
  </v-dialog>
474
+
475
+ <!-- Crop Dialog -->
476
+ <crud-image-cropper
477
+ v-if="cropDialog && imageToCrop"
478
+ :image-url="imageToCrop"
479
+ :aspect-ratio="aspectRatio"
480
+ @crop="handleCropComplete"
481
+ @cancel="handleCropCancel"
482
+ />
310
483
  </div>
311
484
  </template>
@@ -13,6 +13,8 @@ declare const __VLS_component: import("vue").DefineComponent<{}, {
13
13
  replaceable: boolean;
14
14
  isPublic: boolean;
15
15
  additionalPostData: Record<string, any>;
16
+ crop: boolean;
17
+ aspectRatio: string;
16
18
  title?: any;
17
19
  $props: any;
18
20
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -38,6 +38,8 @@ export function useCrudConverters() {
38
38
  case "image/jpg":
39
39
  case "image/jpeg":
40
40
  case "image/png":
41
+ case "image/heic":
42
+ case "image/heic-sequence":
41
43
  return "mdi-file-image";
42
44
  case "application/pdf":
43
45
  return "mdi-file-pdf";
@@ -157,6 +159,8 @@ export function useCrudConverters() {
157
159
  case "image/jpg":
158
160
  case "image/jpeg":
159
161
  case "image/png":
162
+ case "image/heic":
163
+ case "image/heic-sequence":
160
164
  return 1;
161
165
  case "application/pdf":
162
166
  return 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grantthomas-nuxt",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Crud module for Nuxt 3 interacting with sibling .net project",
5
5
  "repository": "Tap-Leagues/GrantThomas.Nuxt",
6
6
  "license": "MIT",
@@ -32,10 +32,12 @@
32
32
  "@nuxt/scripts": "^0.11.8",
33
33
  "@vueuse/core": "^13.3.0",
34
34
  "@vueuse/nuxt": "^13.3.0",
35
+ "heic2any": "^0.0.4",
35
36
  "luxon": "^3.6.1",
36
37
  "query-string": "^9.2.1",
37
38
  "ts-luxon": "^6.1.0",
38
39
  "uuid": "^11.0.2",
40
+ "vue-advanced-cropper": "^2.8.9",
39
41
  "vuetify-nuxt-module": "^0.18.7"
40
42
  },
41
43
  "devDependencies": {
@@ -52,7 +54,7 @@
52
54
  "eslint-config-prettier": "^8.8.0",
53
55
  "sass": "^1.89.2",
54
56
  "typescript": "^5.8.3",
55
- "vue-tsc": "^2.2.10",
56
- "vitest": "^3.2.3"
57
+ "vitest": "^3.2.3",
58
+ "vue-tsc": "^2.2.10"
57
59
  }
58
60
  }