grantthomas-nuxt 1.0.27 → 1.0.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.
- package/dist/module.json +1 -1
- package/dist/runtime/components/CrudImageCropper.d.vue.ts +60 -2
- package/dist/runtime/components/CrudImageCropper.vue +161 -8
- package/dist/runtime/components/CrudImageCropper.vue.d.ts +60 -2
- package/dist/runtime/components/CrudUploadField.vue +3 -1
- package/package.json +2 -2
package/dist/module.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.28",
|
|
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
|
-
"
|
|
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",
|