diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index e24074cc6..c8f85fcc8 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -651,6 +651,16 @@ class SchemeInitializer implements SmartLifecycle { .setIndexFunc(simpleAttribute(LocalThumbnail.class, thumbnail -> thumbnail.getSpec().getThumbSignature()) )); + indexSpec.add(new IndexSpec() + .setName("status.phase") + .setIndexFunc( + simpleAttribute(LocalThumbnail.class, + thumbnail -> Optional.of(thumbnail.getStatus()) + .map(LocalThumbnail.Status::getPhase) + .map(LocalThumbnail.Phase::name) + .orElse(null)) + ) + ); }); // metrics.halo.run schemeManager.register(Counter.class); diff --git a/ui/console-src/modules/contents/attachments/AttachmentList.vue b/ui/console-src/modules/contents/attachments/AttachmentList.vue index 40ccb9694..f897d9343 100644 --- a/ui/console-src/modules/contents/attachments/AttachmentList.vue +++ b/ui/console-src/modules/contents/attachments/AttachmentList.vue @@ -1,6 +1,7 @@ + + + + + + + {{ $t("core.attachment.actions.thumbnails") }} + + import AttachmentPermalinkList from "@/components/attachment/AttachmentPermalinkList.vue"; import LazyImage from "@/components/image/LazyImage.vue"; +import HasPermission from "@/components/permission/HasPermission.vue"; import { formatDatetime } from "@/utils/date"; import { isImage } from "@/utils/image"; import { coreApiClient } from "@halo-dev/api-client"; @@ -16,6 +17,7 @@ import { import { useQuery } from "@tanstack/vue-query"; import prettyBytes from "pretty-bytes"; import { computed, ref, toRefs, useTemplateRef } from "vue"; +import AttachmentSingleThumbnailList from "./AttachmentSingleThumbnailList.vue"; import DisplayNameEditForm from "./DisplayNameEditForm.vue"; const props = withDefaults( @@ -230,6 +232,16 @@ const showDisplayNameForm = ref(false); > + + + + + diff --git a/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailItem.vue b/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailItem.vue new file mode 100644 index 000000000..b3d435d36 --- /dev/null +++ b/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailItem.vue @@ -0,0 +1,87 @@ + + + + + + + + + + + + + {{ SIZE_MAP[thumbnail.spec.size] }} + + + {{ thumbnail.spec.thumbnailUri }} + + + + + + {{ $t("core.common.buttons.retry") }} + + + + + diff --git a/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailList.vue b/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailList.vue new file mode 100644 index 000000000..0871a3c7b --- /dev/null +++ b/ui/console-src/modules/contents/attachments/components/AttachmentSingleThumbnailList.vue @@ -0,0 +1,75 @@ + + + + + + + diff --git a/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailItem.vue b/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailItem.vue new file mode 100644 index 000000000..3c825584d --- /dev/null +++ b/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailItem.vue @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + {{ thumbnail.spec.thumbnailUri }} + + + + + + + + + + + + + + + {{ $t("core.common.buttons.retry") }} + + + + + + diff --git a/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailsModal.vue b/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailsModal.vue new file mode 100644 index 000000000..dc7e8deec --- /dev/null +++ b/ui/console-src/modules/contents/attachments/components/AttachmentThumbnailsModal.vue @@ -0,0 +1,216 @@ + + + + + + + + + + {{ + $t( + "core.attachment.thumbnails_modal.operations.retry_all_failed.button" + ) + }} + + + + + + + {{ $t("core.common.buttons.refresh") }} + + + + + + + + + + + + + + + + {{ $t("core.common.buttons.close") }} + + + + diff --git a/ui/console-src/modules/contents/attachments/composables/use-thumbnail-control.ts b/ui/console-src/modules/contents/attachments/composables/use-thumbnail-control.ts new file mode 100644 index 000000000..c13bb0c66 --- /dev/null +++ b/ui/console-src/modules/contents/attachments/composables/use-thumbnail-control.ts @@ -0,0 +1,46 @@ +import { storageAnnotations } from "@/constants/annotations"; +import { + coreApiClient, + LocalThumbnailStatusPhaseEnum, + type LocalThumbnail, +} from "@halo-dev/api-client"; +import { Toast } from "@halo-dev/components"; +import { useQueryClient } from "@tanstack/vue-query"; +import type { Ref } from "vue"; +import { useI18n } from "vue-i18n"; + +export function useThumbnailControl(thumbnail: Ref) { + const queryClient = useQueryClient(); + const { t } = useI18n(); + + async function handleRetry() { + await coreApiClient.storage.localThumbnail.patchLocalThumbnail({ + name: thumbnail.value.metadata.name, + jsonPatchInner: [ + { + op: "add", + path: "/status/phase", + value: LocalThumbnailStatusPhaseEnum.Pending, + }, + { + op: "add", + path: "/metadata/annotations", + value: { + ...thumbnail.value.metadata.annotations, + [storageAnnotations.RETRY_TIMESTAMP]: Date.now().toString(), + }, + }, + ], + }); + + await queryClient.invalidateQueries({ + queryKey: ["core:attachments:thumbnails"], + }); + + Toast.success(t("core.common.toast.operation_success")); + } + + return { + handleRetry, + }; +} diff --git a/ui/console-src/modules/contents/attachments/composables/use-thumbnail-detail.ts b/ui/console-src/modules/contents/attachments/composables/use-thumbnail-detail.ts new file mode 100644 index 000000000..636f2c6d1 --- /dev/null +++ b/ui/console-src/modules/contents/attachments/composables/use-thumbnail-detail.ts @@ -0,0 +1,51 @@ +import { + LocalThumbnailSpecSizeEnum, + LocalThumbnailStatusPhaseEnum, + type LocalThumbnail, +} from "@halo-dev/api-client"; +import { IconCheckboxCircle, IconErrorWarning } from "@halo-dev/components"; +import { computed, type Component, type Ref } from "vue"; +import RiTimeLine from "~icons/ri/time-line"; + +export const SIZE_MAP: Record = { + XL: "1600w", + L: "1200w", + M: "800w", + S: "400w", +}; + +export const PHASE_MAP: Record< + LocalThumbnailStatusPhaseEnum, + { + icon: Component; + label: string; + color: string; + } +> = { + PENDING: { + icon: RiTimeLine, + label: "core.attachment.thumbnails.phase.pending", + color: "text-gray-500", + }, + SUCCEEDED: { + icon: IconCheckboxCircle, + label: "core.attachment.thumbnails.phase.succeeded", + color: "text-green-500", + }, + FAILED: { + icon: IconErrorWarning, + label: "core.attachment.thumbnails.phase.failed", + color: "text-red-500", + }, +}; +export function useThumbnailDetail(thumbnail: Ref) { + const phase = computed(() => { + return PHASE_MAP[ + thumbnail.value.status.phase || LocalThumbnailStatusPhaseEnum.Pending + ]; + }); + + return { + phase, + }; +} diff --git a/ui/package.json b/ui/package.json index 28c91aec1..3dd53595c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -90,6 +90,7 @@ "colorjs.io": "^0.4.3", "core-js": "^3.43.0", "cropperjs": "^1.5.13", + "crypto-js": "^4.2.0", "dayjs": "^1.11.7", "emoji-mart": "^5.6.0", "floating-vue": "^5.2.2", @@ -120,6 +121,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.10", "@tsconfig/node18": "^2.0.1", + "@types/crypto-js": "^4.2.2", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", "@types/node": "^18.11.19", diff --git a/ui/packages/api-client/entry/api-client.ts b/ui/packages/api-client/entry/api-client.ts index 5a4c34a09..df0ea76b3 100644 --- a/ui/packages/api-client/entry/api-client.ts +++ b/ui/packages/api-client/entry/api-client.ts @@ -19,6 +19,7 @@ import { ExtensionPointDefinitionV1alpha1Api, GroupV1alpha1Api, IndicesV1alpha1ConsoleApi, + LocalThumbnailV1alpha1Api, MenuItemV1alpha1Api, MenuV1alpha1Api, MenuV1alpha1PublicApi, @@ -62,6 +63,7 @@ import { TagV1alpha1ConsoleApi, ThemeV1alpha1Api, ThemeV1alpha1ConsoleApi, + ThumbnailV1alpha1Api, TwoFactorAuthV1alpha1UcApi, UserConnectionV1alpha1Api, UserPreferenceV1alpha1UcApi, @@ -163,6 +165,12 @@ function createCoreApiClient(axiosInstance: AxiosInstance) { baseURL, axiosInstance ), + localThumbnail: new LocalThumbnailV1alpha1Api( + undefined, + baseURL, + axiosInstance + ), + thumbnail: new ThumbnailV1alpha1Api(undefined, baseURL, axiosInstance), }, // plugin.halo.run diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 0f6871cae..9f0816980 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -156,6 +156,9 @@ importers: cropperjs: specifier: ^1.5.13 version: 1.5.13 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 dayjs: specifier: ^1.11.7 version: 1.11.7 @@ -241,6 +244,9 @@ importers: '@tsconfig/node18': specifier: ^2.0.1 version: 2.0.1 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -4287,6 +4293,9 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -5816,6 +5825,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -15658,6 +15670,8 @@ snapshots: dependencies: '@types/node': 18.19.34 + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/detect-port@1.3.5': {} @@ -17403,6 +17417,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + crypto-random-string@2.0.0: {} crypto-random-string@4.0.0: diff --git a/ui/src/constants/annotations.ts b/ui/src/constants/annotations.ts index 5dd8a1f91..047329e35 100644 --- a/ui/src/constants/annotations.ts +++ b/ui/src/constants/annotations.ts @@ -33,3 +33,11 @@ export enum patAnnotations { export enum secretAnnotations { DESCRIPTION = "secret.halo.run/description", } + +// storage +export enum storageAnnotations { + URI = "storage.halo.run/uri", + + // Frontend custom annotations, for retry operation + RETRY_TIMESTAMP = "storage.halo.run/retry-timestamp", +} diff --git a/ui/src/locales/_missing_translations_es.yaml b/ui/src/locales/_missing_translations_es.yaml index cc13a275c..5a13f7b77 100644 --- a/ui/src/locales/_missing_translations_es.yaml +++ b/ui/src/locales/_missing_translations_es.yaml @@ -208,11 +208,16 @@ core: original_comment: Original comment content: Reply content attachment: + actions: + thumbnails: Thumbnails filters: sort: items: display_name_asc: Ascending order by display name display_name_desc: Descending order by display name + detail_modal: + fields: + thumbnails: Thumbnails permalink_list: relative: Relative path absolute: Absolute path @@ -232,6 +237,21 @@ core: label: URL toast: success: Downloaded successfully + thumbnails_modal: + title: Thumbnail records + empty: + title: No thumbnail records + message: There are currently no thumbnail records, thumbnails will be automatically generated after uploading image attachments. + operations: + retry_all_failed: + button: Retry all failed records + tips_empty: No failed thumbnail records + tips_success: All failed records retried successfully + thumbnails: + phase: + pending: Pending + succeeded: Succeeded + failed: Failed uc_attachment: empty: title: There are no attachments. @@ -757,6 +777,7 @@ core: disable: Disable enable: Enable continue: Continue + retry: Retry toast: disable_success: Disabled successfully enable_success: Enabled successfully diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml index 83dc27aad..3466e478e 100644 --- a/ui/src/locales/en.yaml +++ b/ui/src/locales/en.yaml @@ -624,6 +624,7 @@ core: ungrouped: Ungrouped actions: storage_policies: Storage Policies + thumbnails: Thumbnails empty: title: There are no attachments in the current group. message: >- @@ -679,6 +680,7 @@ core: owner: Owner creation_time: Creation time permalink: Permalink + thumbnails: Thumbnails preview: click_to_exit: Click to exit preview video_not_support: The current browser does not support video playback. @@ -771,6 +773,23 @@ core: operations: select: result: ({count} items selected) + thumbnails_modal: + title: Thumbnail records + empty: + title: No thumbnail records + message: >- + There are currently no thumbnail records, thumbnails will be + automatically generated after uploading image attachments. + operations: + retry_all_failed: + button: Retry all failed records + tips_empty: No failed thumbnail records + tips_success: All failed records retried successfully + thumbnails: + phase: + pending: Pending + succeeded: Succeeded + failed: Failed uc_attachment: empty: title: There are no attachments. @@ -1927,6 +1946,7 @@ core: disable: Disable enable: Enable continue: Continue + retry: Retry radio: "yes": "Yes" "no": "No" diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml index 120ffc53d..8a9028eea 100644 --- a/ui/src/locales/zh-CN.yaml +++ b/ui/src/locales/zh-CN.yaml @@ -596,6 +596,7 @@ core: ungrouped: 未分组 actions: storage_policies: 存储策略 + thumbnails: 缩略图 empty: title: 当前分组没有附件 message: 当前分组没有附件,你可以尝试刷新或者上传附件 @@ -649,6 +650,7 @@ core: owner: 上传者 creation_time: 上传时间 permalink: 链接 + thumbnails: 缩略图 preview: click_to_exit: 点击退出预览 video_not_support: 当前浏览器不支持该视频播放 @@ -731,6 +733,21 @@ core: permalink_list: relative: 相对路径 absolute: 完整路径 + thumbnails_modal: + title: 缩略图记录 + empty: + title: 没有缩略图记录 + message: 当前没有缩略图记录,缩略图会在上传图片附件后自动生成。 + operations: + retry_all_failed: + button: 重试所有失败记录 + tips_empty: 没有失败记录 + tips_success: 重试所有失败记录成功 + thumbnails: + phase: + pending: 等待生成 + succeeded: 生成成功 + failed: 生成失败 uc_attachment: empty: title: 当前没有附件 @@ -1788,6 +1805,7 @@ core: disable: 禁用 enable: 启用 continue: 继续 + retry: 重试 radio: "yes": 是 "no": 否 diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml index 7d7195e9b..844d6edb2 100644 --- a/ui/src/locales/zh-TW.yaml +++ b/ui/src/locales/zh-TW.yaml @@ -581,6 +581,7 @@ core: ungrouped: 未分組 actions: storage_policies: 存儲策略 + thumbnails: 縮略圖 empty: title: 當前分組沒有附件 message: 當前分組沒有附件,你可以嘗試重整或者上傳附件 @@ -634,6 +635,7 @@ core: owner: 上傳者 creation_time: 上傳時間 permalink: 連結 + thumbnails: 縮略圖 preview: click_to_exit: 點擊離開預覽 video_not_support: 當前瀏覽器不支援該影片播放 @@ -716,6 +718,21 @@ core: permalink_list: relative: 相對路徑 absolute: 完整路徑 + thumbnails_modal: + title: 縮略圖記錄 + empty: + title: 沒有縮略圖記錄 + message: 當前沒有縮略圖記錄,縮略圖會在上傳圖片附件後自動生成。 + operations: + retry_all_failed: + button: 重試所有失敗記錄 + tips_empty: 沒有失敗記錄 + tips_success: 重試所有失敗記錄成功 + thumbnails: + phase: + pending: 等待生成 + succeeded: 生成成功 + failed: 生成失敗 uc_attachment: empty: title: 當前沒有附件 @@ -1773,6 +1790,7 @@ core: disable: 禁用 enable: 启用 continue: 繼續 + retry: 重試 radio: "yes": 是 "no": 否