pukaad-ui-lib 1.423.0 → 1.425.0

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": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.423.0",
4
+ "version": "1.425.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -14,7 +14,7 @@
14
14
  />
15
15
 
16
16
  <div
17
- class="absolute inset-x-0 bottom-0 flex h-[84px] flex-col justify-between bg-gradient-to-t from-black/70 to-transparent p-[8px]"
17
+ class="absolute inset-x-0 bottom-0 flex h-[84px] flex-col justify-end gap-1 bg-gradient-to-t from-black/70 to-transparent p-[8px]"
18
18
  >
19
19
  <p class="line-clamp-2 break-words font-title-small-prominent text-white">
20
20
  {{ props.item.title }}
@@ -159,11 +159,12 @@
159
159
  </template>
160
160
 
161
161
  <script setup>
162
- import { ref, computed, watch } from "vue";
162
+ import { ref, computed, onBeforeUnmount, watch } from "vue";
163
163
  import { useNuxtApp } from "nuxt/app";
164
164
  import { useApi } from "../../composables/useApi";
165
165
  import { usePresignedUpload } from "../../composables/usePresignedUpload";
166
166
  import { CONTENT_COVER_COLORS } from "../../composables/useContentCard";
167
+ import { createVideoThumbnail } from "../../utils/createVideoThumbnail";
167
168
  const emit = defineEmits(["success", "saveDraft"]);
168
169
  const { $toast, $alert } = useNuxtApp();
169
170
  const api = useApi();
@@ -188,6 +189,7 @@ const selectedColor = ref(colors[0].hex);
188
189
  const coverText = ref("");
189
190
  const coverFiles = ref([]);
190
191
  const posterFiles = ref([]);
192
+ const generatedPoster = ref(null);
191
193
  const description = ref("");
192
194
  const fileColumns = ref(false);
193
195
  const selectedPlaces = ref([]);
@@ -227,6 +229,43 @@ watch(coverFileType, (type) => {
227
229
  posterFiles.value = [];
228
230
  }
229
231
  });
232
+ let generatedPosterUrl = "";
233
+ let thumbnailGeneration = 0;
234
+ let thumbnailPromise = Promise.resolve();
235
+ const clearGeneratedPoster = () => {
236
+ if (generatedPosterUrl) URL.revokeObjectURL(generatedPosterUrl);
237
+ generatedPosterUrl = "";
238
+ generatedPoster.value = null;
239
+ };
240
+ watch(
241
+ [() => coverFiles.value[0]?.file, () => posterFiles.value[0]],
242
+ ([videoFile, manualPoster]) => {
243
+ const generation = ++thumbnailGeneration;
244
+ clearGeneratedPoster();
245
+ if (manualPoster || typeof File === "undefined" || !(videoFile instanceof File) || !videoFile.type.startsWith("video/")) {
246
+ thumbnailPromise = Promise.resolve();
247
+ return;
248
+ }
249
+ thumbnailPromise = createVideoThumbnail(videoFile).then((thumbnail) => {
250
+ if (generation !== thumbnailGeneration) return;
251
+ generatedPosterUrl = URL.createObjectURL(thumbnail);
252
+ generatedPoster.value = {
253
+ file: thumbnail,
254
+ url: generatedPosterUrl,
255
+ mediaType: "image"
256
+ };
257
+ }).catch(() => {
258
+ });
259
+ },
260
+ { immediate: true }
261
+ );
262
+ onBeforeUnmount(() => {
263
+ thumbnailGeneration++;
264
+ clearGeneratedPoster();
265
+ });
266
+ const effectivePoster = computed(
267
+ () => posterFiles.value[0] ?? generatedPoster.value
268
+ );
230
269
  const previewItem = computed(() => {
231
270
  if (hasCoverFile.value) {
232
271
  const file = coverFiles.value[0];
@@ -234,7 +273,7 @@ const previewItem = computed(() => {
234
273
  return {
235
274
  id: "preview",
236
275
  type: "video",
237
- video: { src: file.url, poster: posterFiles.value[0]?.url },
276
+ video: { src: file.url, poster: effectivePoster.value?.url },
238
277
  title: description.value,
239
278
  location: previewLocation.value
240
279
  };
@@ -404,9 +443,12 @@ const submitContent = async (status) => {
404
443
  isSubmitting.value = true;
405
444
  try {
406
445
  const contentType = hasCoverFile.value ? coverFileType.value === "video" ? "video" : "image" : "post_it";
446
+ if (contentType === "video" && !posterFiles.value[0]) {
447
+ await thumbnailPromise;
448
+ }
407
449
  const [uploadedMedia, uploadedPoster] = await Promise.all([
408
450
  uploadFiles(coverFiles.value, "content-media"),
409
- contentType === "video" ? uploadFiles(posterFiles.value, "content-media") : Promise.resolve([])
451
+ contentType === "video" && effectivePoster.value ? uploadFiles([effectivePoster.value], "content-media") : Promise.resolve([])
410
452
  ]);
411
453
  const media = uploadedMedia.map((item, index) => ({
412
454
  media_type: contentType === "video" ? "video" : "photo",
@@ -44,8 +44,9 @@
44
44
  />
45
45
  </div>
46
46
  <div v-if="post.type === 'video'" class="shrink-0">
47
- <div class="relative w-[40px] h-[40px] shrink-0">
47
+ <div class="relative w-[40px] h-[40px] shrink-0 bg-black/20 rounded-[4px]">
48
48
  <Image
49
+ v-if="post.thumnail"
49
50
  :src="post.thumnail"
50
51
  :width="40"
51
52
  :height="40"
@@ -185,8 +186,9 @@
185
186
  <Icon name="fa6-solid:message" size="24" class="text-primary" />
186
187
  </div>
187
188
  <div v-else-if="item.type === 'video'" class="shrink-0">
188
- <div class="relative w-[40px] h-[40px] shrink-0">
189
+ <div class="relative w-[40px] h-[40px] shrink-0 bg-black/20 rounded-[4px]">
189
190
  <Image
191
+ v-if="item.thumnail"
190
192
  :src="item.thumnail"
191
193
  :width="40"
192
194
  :height="40"
@@ -243,7 +245,7 @@ const props = defineProps({
243
245
  label: { type: String, required: false, default: "\u0E01\u0E32\u0E23\u0E40\u0E2D\u0E48\u0E22\u0E16\u0E36\u0E07" },
244
246
  placeholder: { type: String, required: false, default: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E01\u0E32\u0E23\u0E40\u0E2D\u0E48\u0E22\u0E16\u0E36\u0E07" }
245
247
  });
246
- const api = useApi();
248
+ const api = useApi({ includeProvince: false });
247
249
  const comboboxRef = ref();
248
250
  const selectedUsernames = ref([]);
249
251
  const isSelected = (id) => selectedUsernames.value.includes(id);
@@ -334,7 +336,7 @@ const mapContentToPostOption = (raw) => {
334
336
  type,
335
337
  title,
336
338
  image: type === "image" ? firstMedia?.url ?? null : null,
337
- thumnail: type === "video" ? raw.video_poster_url || firstMedia?.url : void 0
339
+ thumnail: type === "video" ? raw.video_poster_url : void 0
338
340
  };
339
341
  };
340
342
  const mapBlogToOption = (raw) => ({
@@ -1 +1,3 @@
1
- export declare const useApi: () => import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
1
+ export declare const useApi: (apiOptions?: {
2
+ includeProvince?: boolean;
3
+ }) => import("nitropack").$Fetch<unknown, import("nitropack").NitroFetchRequest>;
@@ -1,5 +1,5 @@
1
1
  import { useRuntimeConfig, useCookie, useState } from "nuxt/app";
2
- export const useApi = () => {
2
+ export const useApi = (apiOptions = {}) => {
3
3
  const config = useRuntimeConfig();
4
4
  const baseURL = config.public.BASE_URL_API;
5
5
  const provinceIdState = useState("pukaad:province-id", () => null);
@@ -20,7 +20,7 @@ export const useApi = () => {
20
20
  headers.set("Authorization", `Bearer ${secId.value}`);
21
21
  }
22
22
  const pid = provinceIdState.value ?? configProvinceId;
23
- if (pid) {
23
+ if (apiOptions.includeProvince !== false && pid) {
24
24
  headers.set("X-Province-ID", String(pid));
25
25
  }
26
26
  if (businessProfileId.value) {
@@ -0,0 +1 @@
1
+ export declare const createVideoThumbnail: (file: File, atSecond?: number) => Promise<File>;
@@ -0,0 +1,47 @@
1
+ export const createVideoThumbnail = (file, atSecond = 0.1) => new Promise((resolve, reject) => {
2
+ const video = document.createElement("video");
3
+ const objectUrl = URL.createObjectURL(file);
4
+ const timeout = window.setTimeout(fail, 1e4);
5
+ function cleanup() {
6
+ window.clearTimeout(timeout);
7
+ URL.revokeObjectURL(objectUrl);
8
+ }
9
+ function fail() {
10
+ cleanup();
11
+ reject(new Error("Failed to create video thumbnail"));
12
+ }
13
+ video.preload = "auto";
14
+ video.muted = true;
15
+ video.playsInline = true;
16
+ video.src = objectUrl;
17
+ video.addEventListener("error", fail, { once: true });
18
+ video.addEventListener(
19
+ "loadeddata",
20
+ () => {
21
+ const duration = Number.isFinite(video.duration) ? video.duration : 0;
22
+ const target = duration > 0 ? Math.min(atSecond, duration / 2) : 0;
23
+ if (target === 0) captureFrame();
24
+ else video.currentTime = target;
25
+ },
26
+ { once: true }
27
+ );
28
+ video.addEventListener("seeked", captureFrame, { once: true });
29
+ function captureFrame() {
30
+ const canvas = document.createElement("canvas");
31
+ canvas.width = video.videoWidth;
32
+ canvas.height = video.videoHeight;
33
+ canvas.getContext("2d")?.drawImage(video, 0, 0);
34
+ canvas.toBlob(
35
+ (blob) => {
36
+ cleanup();
37
+ if (!blob)
38
+ return reject(new Error("Failed to create video thumbnail"));
39
+ resolve(
40
+ new File([blob], "video-thumbnail.jpg", { type: "image/jpeg" })
41
+ );
42
+ },
43
+ "image/jpeg",
44
+ 0.85
45
+ );
46
+ }
47
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.423.0",
3
+ "version": "1.425.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",