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 +1 -1
- package/dist/runtime/components/CrudUploadField.d.vue.ts +2 -0
- package/dist/runtime/components/CrudUploadField.vue +181 -8
- package/dist/runtime/components/CrudUploadField.vue.d.ts +2 -0
- package/dist/runtime/composables/useCrudConverters.js +4 -0
- package/package.json +5 -3
package/dist/module.json
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
"
|
|
56
|
-
"
|
|
57
|
+
"vitest": "^3.2.3",
|
|
58
|
+
"vue-tsc": "^2.2.10"
|
|
57
59
|
}
|
|
58
60
|
}
|