diff --git a/.gitignore b/.gitignore index 6c5616fb..8e877c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ Thumbs.db # Other *.local +GEMINI.md \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84fb63d8..e43f0263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '6.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false dependencies: @@ -99,7 +99,7 @@ devDependencies: version: 0.1.2 stylelint-stylus: specifier: ^0.18.0 - version: 0.18.0(postcss-syntax@0.36.2)(stylelint@15.10.2) + version: 0.18.0(stylelint@15.10.2) stylus: specifier: ^0.59.0 version: 0.59.0 @@ -111,19 +111,19 @@ devDependencies: version: 4.9.5 unplugin-auto-import: specifier: ^0.15.2 - version: 0.15.3(rollup@2.79.1) + version: 0.15.3 unplugin-icons: specifier: ^0.16.1 version: 0.16.5(@vue/compiler-sfc@3.3.4) unplugin-vue-components: specifier: ^0.24.1 - version: 0.24.1(rollup@2.79.1)(vue@3.3.4) + version: 0.24.1(vue@3.3.4) vite: specifier: ~2.7.13 version: 2.7.13(stylus@0.59.0) vite-plugin-pwa: specifier: ^0.12.8 - version: 0.12.8(vite@2.7.13)(workbox-build@6.6.0)(workbox-window@6.6.0) + version: 0.12.8(vite@2.7.13) packages: @@ -1835,7 +1835,7 @@ packages: rollup: 2.79.1 dev: true - /@rollup/pluginutils@5.0.2(rollup@2.79.1): + /@rollup/pluginutils@5.0.2: resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1847,7 +1847,6 @@ packages: '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 2.79.1 dev: true /@surma/rollup-plugin-off-main-thread@2.2.3: @@ -6204,7 +6203,7 @@ packages: dependencies: htmlparser2: 3.10.1 postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) dev: true /postcss-html@1.5.0: @@ -6225,7 +6224,7 @@ packages: dependencies: '@babel/core': 7.22.9 postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) transitivePeerDependencies: - supports-color dev: true @@ -6244,7 +6243,7 @@ packages: postcss-syntax: '>=0.36.0' dependencies: postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) remark: 10.0.1 unist-util-find-all-after: 1.0.5 dev: true @@ -6335,7 +6334,7 @@ packages: - supports-color dev: true - /postcss-syntax@0.36.2(postcss-html@1.5.0)(postcss@8.4.27): + /postcss-syntax@0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39): resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} peerDependencies: postcss: '>=5.0.0' @@ -6356,8 +6355,12 @@ packages: postcss-scss: optional: true dependencies: - postcss: 8.4.27 - postcss-html: 1.5.0 + postcss: 7.0.39 + postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-jsx: 0.36.4(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-less: 3.1.4 + postcss-markdown: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-scss: 2.1.1 dev: true /postcss-value-parser@3.3.1: @@ -7033,6 +7036,7 @@ packages: /source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions dependencies: whatwg-url: 7.1.0 dev: true @@ -7283,7 +7287,7 @@ packages: stylelint: 9.10.1 dev: true - /stylelint-stylus@0.18.0(postcss-syntax@0.36.2)(stylelint@15.10.2): + /stylelint-stylus@0.18.0(stylelint@15.10.2): resolution: {integrity: sha512-n3zjLFLonPOUYY3UIUtSKzZzPB9GQo+BvtmcEQfI+4QHiNsHpfr9QrznkWT8sUB+S11dr3JRCzZ45K9lRQviSg==} engines: {node: ^12 || >=14} peerDependencies: @@ -7300,7 +7304,6 @@ packages: postcss-media-query-parser: 0.2.3 postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) style-search: 0.1.0 stylelint: 15.10.2 stylelint-config-html: 1.1.0(postcss-html@1.5.0)(stylelint@15.10.2) @@ -7399,7 +7402,7 @@ packages: postcss-sass: 0.3.5 postcss-scss: 2.1.1 postcss-selector-parser: 3.1.2 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) postcss-value-parser: 3.3.1 resolve-from: 4.0.0 signal-exit: 3.0.7 @@ -7829,10 +7832,10 @@ packages: x-is-string: 0.1.0 dev: true - /unimport@3.1.0(rollup@2.79.1): + /unimport@3.1.0: resolution: {integrity: sha512-ybK3NVWh30MdiqSyqakrrQOeiXyu5507tDA0tUf7VJHrsq4DM6S43gR7oAsZaFojM32hzX982Lqw02D3yf2aiA==} dependencies: - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 escape-string-regexp: 5.0.0 fast-glob: 3.3.1 local-pkg: 0.4.3 @@ -7911,7 +7914,7 @@ packages: engines: {node: '>= 10.0.0'} dev: true - /unplugin-auto-import@0.15.3(rollup@2.79.1): + /unplugin-auto-import@0.15.3: resolution: {integrity: sha512-RLT8SqbPn4bT7yBshZId0uPSofKWnwr66RyDaxWaFb/+f7OTDOWAsVNz+hOQLBWSjvbekr2xZY9ccS8TDHJbCQ==} engines: {node: '>=14'} peerDependencies: @@ -7924,11 +7927,11 @@ packages: optional: true dependencies: '@antfu/utils': 0.7.5 - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 local-pkg: 0.4.3 magic-string: 0.30.1 minimatch: 9.0.3 - unimport: 3.1.0(rollup@2.79.1) + unimport: 3.1.0 unplugin: 1.4.0 transitivePeerDependencies: - rollup @@ -7966,7 +7969,7 @@ packages: - supports-color dev: true - /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.3.4): + /unplugin-vue-components@0.24.1(vue@3.3.4): resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} engines: {node: '>=14'} peerDependencies: @@ -7980,7 +7983,7 @@ packages: optional: true dependencies: '@antfu/utils': 0.7.5 - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 chokidar: 3.5.3 debug: 4.3.4 fast-glob: 3.3.1 @@ -8096,12 +8099,10 @@ packages: vfile-message: 1.1.1 dev: true - /vite-plugin-pwa@0.12.8(vite@2.7.13)(workbox-build@6.6.0)(workbox-window@6.6.0): + /vite-plugin-pwa@0.12.8(vite@2.7.13): resolution: {integrity: sha512-pSiFHmnJGMQJJL8aJzQ8SaraZBSBPMGvGUkCNzheIq9UQCEk/eP3UmANNmS9eupuhIpTK8AdxTOHcaMcAqAbCA==} peerDependencies: vite: ^2.0.0 || ^3.0.0-0 - workbox-build: ^6.4.0 - workbox-window: ^6.4.0 dependencies: debug: 4.3.4 fast-glob: 3.3.1 @@ -8111,6 +8112,7 @@ packages: workbox-build: 6.6.0 workbox-window: 6.6.0 transitivePeerDependencies: + - '@types/babel__core' - supports-color dev: true diff --git a/src/common/api/dir.ts b/src/common/api/dir.ts index 79bbdf67..ce3b293f 100644 --- a/src/common/api/dir.ts +++ b/src/common/api/dir.ts @@ -1,7 +1,8 @@ import { store } from '@/stores' -import { getFileSuffix, isImage, createManagementImageObject } from '@/utils' +import { getFileSuffix, isImage, createManagementImageObject, isVideo } from '@/utils' import request from '@/utils/request' import { UserConfigInfoModel } from '@/common/model' +import { createManagementVideoObject } from '@/utils/video-utils' /** * 获取指定路径 Path 下的目录列表 @@ -65,9 +66,17 @@ export const getRepoPathContent = (userConfigInfo: UserConfigInfoModel, path: st setTimeout(() => { res - .filter((v: any) => v.type === 'file' && isImage(getFileSuffix(v.name))) + .filter((v: any) => { + const suffix = getFileSuffix(v.name) + return v.type === 'file' && (isImage(suffix) || isVideo(suffix)) + }) .forEach((x: any) => { - store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', createManagementImageObject(x, path)) + const suffix = getFileSuffix(x.name) + if (isImage(suffix)) { + store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', createManagementImageObject(x, path)) + } else if (isVideo(suffix)) { + store.dispatch('DIR_IMAGE_LIST_ADD_VIDEO', createManagementVideoObject(x, path)) + } }) }, 120) diff --git a/src/common/constant/init.ts b/src/common/constant/init.ts index a7380dac..81f6cc69 100644 --- a/src/common/constant/init.ts +++ b/src/common/constant/init.ts @@ -5,6 +5,8 @@ export const INIT_REPO_BARNCH = 'master' export const GH_PAGES = 'gh-pages' export const PICX_UPLOAD_IMG_DESC = 'Upload image via PicX (https://github.com/XPoet/picx)' export const PICX_UPLOAD_IMGS_DESC = 'Upload images via PicX (https://github.com/XPoet/picx)' +export const PICX_UPLOAD_VIDEO_DESC = 'Upload video via PicX (https://github.com/XPoet/picx)' +export const PICX_UPLOAD_VIDEOS_DESC = 'Upload videos via PicX (https://github.com/XPoet/picx)' export const PICX_DEL_IMG_DESC = 'Delete image via PicX (https://github.com/XPoet/picx)' export const PICX_INIT_SETTINGS_MSG = 'Init settings via PicX (https://github.com/XPoet/picx)' export const PICX_UPDATE_SETTINGS_MSG = 'Update settings via PicX (https://github.com/XPoet/picx)' diff --git a/src/common/constant/settings.ts b/src/common/constant/settings.ts index c08a8816..f749efef 100644 --- a/src/common/constant/settings.ts +++ b/src/common/constant/settings.ts @@ -8,6 +8,11 @@ export const NEW_DIR_COUNT_MAX: number = 5 */ export const IMG_UPLOAD_MAX_SIZE: number = 30 // MB +/** + * 允许上传视频的最大尺寸 + */ +export const VIDEO_UPLOAD_MAX_SIZE: number = 100 // MB + /** * 图片重命名最大长度 */ diff --git a/src/common/directive/contextmenu.ts b/src/common/directive/contextmenu.ts index 3fd94adf..e5eb1842 100644 --- a/src/common/directive/contextmenu.ts +++ b/src/common/directive/contextmenu.ts @@ -22,9 +22,9 @@ const contextmenuDirective: Directive = { e.preventDefault() e.stopPropagation() - const { type, dir, img } = binding.value + const { type, dir, img, video } = binding.value - store.commit('SET_UPLOAD_AREA_STATE', { activeInfo: { dir, type, img } }) + store.commit('SET_UPLOAD_AREA_STATE', { activeInfo: { dir, type, img, video } }) const viewDir = computed(() => store.getters.getUserViewDir).value diff --git a/src/common/directive/types.ts b/src/common/directive/types.ts index 43cf072b..22cb98d5 100644 --- a/src/common/directive/types.ts +++ b/src/common/directive/types.ts @@ -6,5 +6,7 @@ export enum ContextmenuEnum { // eslint-disable-next-line no-unused-vars img, // eslint-disable-next-line no-unused-vars - uploadArea + uploadArea, + // eslint-disable-next-line no-unused-vars + video = 4 } diff --git a/src/common/model/index.ts b/src/common/model/index.ts index d93b49c5..972d6599 100644 --- a/src/common/model/index.ts +++ b/src/common/model/index.ts @@ -3,3 +3,4 @@ export * from './user-config' export * from './user-settings' export * from './vite-config' export * from './tool' +export * from './video' diff --git a/src/common/model/tool.ts b/src/common/model/tool.ts index a4bc59af..8de13868 100644 --- a/src/common/model/tool.ts +++ b/src/common/model/tool.ts @@ -12,6 +12,13 @@ export interface ImageHandleResult { file: File } +export interface VideoHandleResult { + uuid: string + objectURL: string + file: File + base64: string +} + export interface ImgProcessStateModel { uuid: string originalName: string diff --git a/src/common/model/video.ts b/src/common/model/video.ts new file mode 100644 index 00000000..c1a56139 --- /dev/null +++ b/src/common/model/video.ts @@ -0,0 +1,63 @@ +/** + * Uploaded video object Model + */ +export interface UploadedVideoModel { + type: string + uuid: string + sha: string + dir: string + path: string + name: string + size: number + deleting: boolean + checked: boolean + active?: boolean + deployed?: boolean +} + +/** + * Upload list video object Model + */ +export interface UploadVideoModel { + uuid: string + + base64: { + originalBase64: string + watermarkBase64: string | null + compressBase64: string | null + } + + objectURL: string + + fileInfo: { + originalFile: File | null + compressFile: File | null + watermarkFile: File | null + } + + filename: { + name: string + initName: string // initial name + final: string // final name + suffix: string // suffix + isRename: boolean // whether to rename + newName: string // new name + isAddHash: boolean // whether to add hash + hash: string // hash + isAddPrefix: boolean // whether to add prefix + prefix: string // prefix + } + + uploadStatus: { + progress: 0 | 100 + uploading: boolean + } + + uploadedVideo?: UploadedVideoModel + + reUploadInfo?: { + isReUpload: boolean + path: string + dir: string + } +} diff --git a/src/components.d.ts b/src/components.d.ts index a5f6cc50..97242c16 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -115,10 +115,11 @@ declare module '@vue/runtime-core' { SiteCount: typeof import('./components/site-count/site-count.vue')['default'] UserAvatar: typeof import('./components/user-avatar/user-avatar.vue')['default'] UserAvatarV2: typeof import('./components/user-avatar-v2/user-avatar-v2.vue')['default'] + VideoPreview: typeof import('./components/video-preview/video-preview.vue')['default'] WatermarkConfigBox: typeof import('./components/watermark-config-box/watermark-config-box.vue')['default'] WatermarkTool: typeof import('./components/tools/watermark-tool/watermark-tool.vue')['default'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] } -} \ No newline at end of file +} diff --git a/src/components/nav-content/nav-content.data.ts b/src/components/nav-content/nav-content.data.ts index d013af7a..dfc967f5 100644 --- a/src/components/nav-content/nav-content.data.ts +++ b/src/components/nav-content/nav-content.data.ts @@ -18,6 +18,14 @@ export const navInfoList = shallowRef([ path: '/upload', isShow: true }, + { + uuid: getUuid(), + name: 'nav.video_upload', + icon: IEpUpload, + isActive: false, + path: '/upload-video', + isShow: true + }, { uuid: getUuid(), name: 'nav.management', diff --git a/src/components/nav-content/nav-content.vue b/src/components/nav-content/nav-content.vue index 140916bf..740c2727 100644 --- a/src/components/nav-content/nav-content.vue +++ b/src/components/nav-content/nav-content.vue @@ -87,7 +87,8 @@ const onNavClick = (e: any) => { const changeNavActive = (currentPath: string) => { navInfoList.value.forEach((v) => { const temp = v - temp.isActive = v.path === currentPath || currentPath.includes(v.path) + const rootPath = `/${currentPath.split('/')[1]}` + temp.isActive = v.path === currentPath || rootPath === v.path return temp }) diff --git a/src/components/video-preview/video-preview.vue b/src/components/video-preview/video-preview.vue new file mode 100644 index 00000000..4f7cee32 --- /dev/null +++ b/src/components/video-preview/video-preview.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index a3837ad5..d0c99421 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -19,9 +19,14 @@ "upload": "Upload", "rename": "Rename", "copy_link": "Copy Image Link", + "copy_video_link": "Copy Video Link", "paste_image": "Paste images", "copy_success_1": "Image link has been automatically copied to the system clipboard", "copy_success_2": "Image link copied successfully", + "copy_success_video_1": "Video link has been automatically copied to the system clipboard", + "copy_success_video_2": "Video link copied successfully", + "copy_success_total_1": "Link has been automatically copied to the system clipboard", + "copy_success_total_2": "Link copied successfully", "copy_fail_1": "Copy failed", "header": { "not_login": "Not log in", @@ -163,6 +168,7 @@ }, "upload_page": { "upload_area_text": "Drag / Paste / Click here to select images", + "upload_video_area_text": "Drag / Paste / Click here to select videos", "message1": "Please complete image hosting configuration first", "message2": "Please select a repository", "message3": "Directory cannot be empty", @@ -276,4 +282,4 @@ "text_8": "Copy other repository images", "loading_1": "Copying" } -} +} \ No newline at end of file diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 56a4f55c..3122ea66 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -19,9 +19,14 @@ "upload": "上传", "rename": "重命名", "copy_link": "复制图片链接", + "copy_video_link": "复制视频链接", "paste_image": "粘贴图片", "copy_success_1": "图片链接已自动复制到系统剪贴板", "copy_success_2": "图片链接复制成功", + "copy_success_video_1": "视频链接已自动复制到系统剪贴板", + "copy_success_video_2": "视频链接复制成功", + "copy_success_total_1": "链接已自动复制到系统剪贴板", + "copy_success_total_2": "链接复制成功", "copy_fail_1": "复制失败", "header": { "not_login": "未登录", @@ -40,7 +45,8 @@ "settings": "图床设置", "toolbox": "工具箱", "feedback": "帮助反馈", - "actions": "快捷操作" + "actions": "快捷操作", + "video_upload": "上传视频" }, "actions": { "watermark": "图片水印", @@ -163,6 +169,7 @@ }, "upload_page": { "upload_area_text": "拖拽 / 粘贴 / 点击此处选择图片", + "upload_video_area_text": "拖拽 / 粘贴 / 点击此处选择视频", "message1": "请先完成图床配置", "message2": "请选择一个仓库", "message3": "目录不能为空", @@ -276,4 +283,4 @@ "text_8": "复制其他仓库图片", "loading_1": "复制中" } -} +} \ No newline at end of file diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index a0573561..edd2b77c 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -19,9 +19,14 @@ "upload": "上傳", "rename": "重新命名", "copy_link": "複製圖片連結", + "copy_video_link": "複製影片連結", "paste_image": "貼上圖片", "copy_success_1": "圖片連結已自動複製到系統剪貼簿", "copy_success_2": "圖片連結複製成功", + "copy_success_video_1": "影片連結已自動複製到系統剪貼簿", + "copy_success_video_2": "影片連結複製成功", + "copy_success_total_1": "連結已自動複製到系統剪貼簿", + "copy_success_total_2": "連結複製成功", "copy_fail_1": "複製失敗", "header": { "not_login": "未登入", @@ -163,6 +168,7 @@ }, "upload_page": { "upload_area_text": "拖曳 / 貼上 / 點擊此處選擇圖片", + "upload_video_area_text": "拖曳 / 貼上 / 點擊此處選擇影片", "message1": "請先完成圖床配置", "message2": "請選擇一個倉庫", "message3": "目錄不能為空", @@ -276,4 +282,4 @@ "text_8": "複製其他倉庫圖片", "loading_1": "複製中" } -} +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 78cd0f20..43a7ec61 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -35,6 +35,14 @@ const routes: Array = [ title: 'nav.upload' } }, + { + path: '/upload-video', + name: 'upload-video', + component: () => import('@/views/upload-video/upload-video.vue'), + meta: { + title: 'nav.video_upload' + } + }, { path: '/management', name: 'Management', diff --git a/src/stores/index.ts b/src/stores/index.ts index a9c58f2c..f8b40774 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -10,6 +10,7 @@ import toolboxImageListModule from './modules/toolbox-image-list' import uploadImageListModule from './modules/upload-image-list' import githubAuthorizeModule from './modules/github-authorize' import deployStatusModule from './modules/deploy-status' +import uploadVideoListModule from './modules/upload-video-list' // Create a new store instance export const store = createStore({ @@ -22,7 +23,8 @@ export const store = createStore({ toolboxImageListModule, uploadImageListModule, githubAuthorizeModule, - deployStatusModule + deployStatusModule, + uploadVideoListModule }, state: { rootName: 'root' diff --git a/src/stores/modules/dir-image-list/index.ts b/src/stores/modules/dir-image-list/index.ts index 09c53018..e6a24f8a 100644 --- a/src/stores/modules/dir-image-list/index.ts +++ b/src/stores/modules/dir-image-list/index.ts @@ -1,5 +1,5 @@ import { Module } from 'vuex' -import { UploadedImageModel } from '@/common/model' +import { UploadedImageModel, UploadedVideoModel } from '@/common/model' import { LS_MANAGEMENT } from '@/common/constant' import DirImageListStateTypes, { DirObject } from './types' import RootStateTypes from '../../types' @@ -111,14 +111,7 @@ const dirImageListModule: Module = { let temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir) if (!temp) { - temp = { - type: 'dir', - dir, - dirPath, - childrenDirs: [], - imageList: [] - } - + temp = createDirObject(dir, dirPath) dirObj.childrenDirs.push(temp) } @@ -147,6 +140,50 @@ const dirImageListModule: Module = { dispatch('DIR_IMAGE_LIST_PERSIST') }, + // 图床管理 - 增加视频 + DIR_IMAGE_LIST_ADD_VIDEO({ state, dispatch }, item: UploadedVideoModel) { + const addVideo = ( + dirObj: DirObject, + dir: string, + dirPath: string, + Video: UploadedVideoModel, + isAdd: boolean = false + ) => { + if (!dirObj) { + return state.dirObject + } + + let temp = dirObj.childrenDirs?.find((x: DirObject) => x.dir === dir) + if (!temp) { + temp = createDirObject(dir, dirPath) + dirObj.childrenDirs.push(temp) + } + + if (isAdd && !temp.videoList.some((v) => v.name === Video.name)) { + temp.videoList.push(Video) + } + + return temp + } + + let tempDirObj: DirObject = state.dirObject + + if (item.dir === '/') { + if (!tempDirObj.videoList.some((v) => v.name === item.name)) { + tempDirObj.videoList.push(item) + } + } else { + const dirList: string[] = item.dir.split('/') + let dirPath = '' + dirList.forEach((dir, i) => { + dirPath += `${i > 0 ? '/' : ''}${dir}` + tempDirObj = addVideo(tempDirObj, dir, dirPath, item, i === dirList.length - 1) + }) + } + + dispatch('DIR_IMAGE_LIST_PERSIST') + }, + // 图床管理 - 删除图片(即删除指定目录里的指定图片) DIR_IMAGE_LIST_REMOVE({ state, dispatch }, item: any) { // 删除 @@ -227,6 +264,7 @@ const dirImageListModule: Module = { if (dirPath === '/') { tempDirObj.imageList = [] + tempDirObj.videoList = [] tempDirObj.childrenDirs = [] dispatch('DIR_IMAGE_LIST_PERSIST') return @@ -244,6 +282,7 @@ const dirImageListModule: Module = { if (isInit) { temp.imageList = [] + temp.videoList = [] temp.childrenDirs = [] } diff --git a/src/stores/modules/dir-image-list/types.ts b/src/stores/modules/dir-image-list/types.ts index 2804a2b0..fb12ce48 100644 --- a/src/stores/modules/dir-image-list/types.ts +++ b/src/stores/modules/dir-image-list/types.ts @@ -1,4 +1,4 @@ -import { UploadedImageModel } from '@/common/model' +import { UploadedImageModel, UploadedVideoModel } from '@/common/model' export interface DirObject { type: 'dir' @@ -6,6 +6,7 @@ export interface DirObject { dirPath: string childrenDirs: DirObject[] imageList: UploadedImageModel[] + videoList: UploadedVideoModel[] } export default interface DirImageListStateTypes { diff --git a/src/stores/modules/dir-image-list/utils.ts b/src/stores/modules/dir-image-list/utils.ts index 509d72d7..1d117afb 100644 --- a/src/stores/modules/dir-image-list/utils.ts +++ b/src/stores/modules/dir-image-list/utils.ts @@ -11,7 +11,8 @@ export const createDirObject = (dir: string, dirPath: string): DirObject => { dir, dirPath, childrenDirs: [], - imageList: [] + imageList: [], + videoList: [] } } diff --git a/src/stores/modules/image-card/index.ts b/src/stores/modules/image-card/index.ts index 44728f51..38f33b26 100644 --- a/src/stores/modules/image-card/index.ts +++ b/src/stores/modules/image-card/index.ts @@ -5,7 +5,8 @@ import { UploadedImageModel } from '@/common/model' const imageCardModule: Module = { state: { - imgCardArr: [] + imgCardArr: [], + videoCardArr: [] }, mutations: { IMAGE_CARD(state: ImageCardStateTypes, { imageObj }) { @@ -19,21 +20,45 @@ const imageCardModule: Module = { }) } }, + VIDEO_CARD(state: ImageCardStateTypes, { videoObj }) { + const { uuid, checked } = videoObj + if (checked) { + state.videoCardArr.forEach((item) => { + if (item.uuid === uuid) { + // eslint-disable-next-line no-param-reassign + item.checked = true + } + }) + } + }, REPLACE_IMAGE_CARD(state: ImageCardStateTypes, { checkedImgArr }) { if (checkedImgArr.length > 0) { state.imgCardArr = checkedImgArr } else { state.imgCardArr = [] } + }, + REPLACE_VIDEO_CARD(state: ImageCardStateTypes, { checkedVideoArr }) { + if (checkedVideoArr.length > 0) { + state.videoCardArr = checkedVideoArr + } else { + state.videoCardArr = [] + } } }, actions: {}, getters: { getImageCardArr: (state: ImageCardStateTypes) => state.imgCardArr, + getVideoCardArr: (state: ImageCardStateTypes) => state.videoCardArr, getImageCardCheckedArr: (state: ImageCardStateTypes) => { return state.imgCardArr.filter((item: UploadedImageModel) => { return item.checked }) + }, + getVideoCardCheckedArr: (state: ImageCardStateTypes) => { + return state.videoCardArr.filter((item) => { + return item.checked + }) } } } diff --git a/src/stores/modules/image-card/types.ts b/src/stores/modules/image-card/types.ts index f72afe9b..48b4967b 100644 --- a/src/stores/modules/image-card/types.ts +++ b/src/stores/modules/image-card/types.ts @@ -1,5 +1,6 @@ -import { UploadedImageModel } from '@/common/model' +import { UploadedImageModel, UploadedVideoModel } from '@/common/model' export interface ImageCardStateTypes { imgCardArr: UploadedImageModel[] + videoCardArr: UploadedVideoModel[] } diff --git a/src/stores/modules/upload-video-list/index.ts b/src/stores/modules/upload-video-list/index.ts new file mode 100644 index 00000000..1f24b8ba --- /dev/null +++ b/src/stores/modules/upload-video-list/index.ts @@ -0,0 +1,40 @@ +import { Module } from 'vuex' +import RootStateTypes from '@/stores/types' +import UploadVideoListStateTypes from './types' +import { UploadVideoModel } from '@/common/model' + +const uploadVideoListModule: Module = { + state: { + uploadVideoList: [] + }, + + mutations: {}, + + actions: { + // 上传处理的图片列表 - 增加 + UPLOAD_VIDEO_LIST_ADD({ state }, item: UploadVideoModel) { + state.uploadVideoList.unshift(item) + }, + + // 上传处理的图片列表 - 删除 + UPLOAD_VIDEO_LIST_REMOVE({ state }, uuid: string) { + if (state.uploadVideoList.length > 0) { + const rmIdx = state.uploadVideoList.findIndex((v) => v.uuid === uuid) + if (rmIdx !== -1 && state.uploadVideoList[rmIdx].uploadStatus.progress === 0) { + state.uploadVideoList.splice(rmIdx, 1) + } + } + }, + + // 上传处理的图片列表 - 重置 + UPLOAD_VIDEO_LIST_RESET({ state }) { + state.uploadVideoList = [] + } + }, + + getters: { + getUploadVideoList: (state): UploadVideoModel[] => state.uploadVideoList + } +} + +export default uploadVideoListModule diff --git a/src/stores/modules/upload-video-list/types.ts b/src/stores/modules/upload-video-list/types.ts new file mode 100644 index 00000000..bd54971f --- /dev/null +++ b/src/stores/modules/upload-video-list/types.ts @@ -0,0 +1,5 @@ +import { UploadVideoModel } from '@/common/model' + +export default interface UploadImageListStateTypes { + uploadVideoList: UploadVideoModel[] +} diff --git a/src/utils/common-utils.ts b/src/utils/common-utils.ts index eb4a1b6a..73123f3e 100644 --- a/src/utils/common-utils.ts +++ b/src/utils/common-utils.ts @@ -193,3 +193,29 @@ export const deepObjectEqual = (obj1: object, obj2: object): boolean => { Object.entries(flattenObject(obj2)).toString() ) } + +export const copyMessage = (autoCopy = false, type: 'image' | 'video' | 'total' = 'total') => { + const getMessageMap = { + image: () => { + return autoCopy ? i18n.global.t('copy_success_1') : i18n.global.t('copy_success_2') + }, + video: () => { + return autoCopy + ? i18n.global.t('copy_success_video_1') + : i18n.global.t('copy_success_video_2') + }, + total: () => { + return autoCopy + ? i18n.global.t('copy_success_total_1') + : i18n.global.t('copy_success_total_2') + } + } + + const message = getMessageMap[type]() + + ElMessage({ + type: autoCopy ? 'info' : 'success', + message, + duration: autoCopy ? 6000 : 4000 + }) +} diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts index 77cd3b30..c17c537b 100644 --- a/src/utils/file-utils.ts +++ b/src/utils/file-utils.ts @@ -1,9 +1,10 @@ import { ElMessage } from 'element-plus' -import { ImageHandleResult } from '@/common/model' +import { ImageHandleResult, VideoHandleResult } from '@/common/model' import { getUuid } from '@/utils/common-utils' import { imgFileToBase64 } from '@/utils/image-utils' -import { IMG_UPLOAD_MAX_SIZE } from '@/common/constant' +import { IMG_UPLOAD_MAX_SIZE, VIDEO_UPLOAD_MAX_SIZE } from '@/common/constant' import i18n from '@/plugins/vue/i18n' +import { videoFileToBase64 } from './video-utils' /** * 获取文件名 @@ -95,3 +96,50 @@ export const gettingFilesHandle = (file: File): Promise { + fileType = fileType.toLowerCase() + return /(mp4|mov|avi|mkv|flv|wmv|webm)$/.test(fileType) +} + +/** + * 处理获取的视频文件 + * @param file + */ +export const gettingVideoFilesHandle = (file: File): Promise => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + if (!file) { + resolve(null) + } + + const fileType = file.name.split('.').pop() || '' + + if (!isVideo(fileType)) { + ElMessage.error(i18n.global.t('upload_page.tip_9', { name: file.name })) + resolve(null) + } + + const objectURL = URL.createObjectURL(file) + + const base64 = (await videoFileToBase64(file)) || '' + + if (getFileSize(base64.length) >= VIDEO_UPLOAD_MAX_SIZE * 1024) { + ElMessage.error( + i18n.global.t('upload_page.tip_10', { name: file.name, size: IMG_UPLOAD_MAX_SIZE }) + ) + resolve(null) + } + + resolve({ + uuid: getUuid(), + objectURL, + file, + base64 + }) + }) +} diff --git a/src/utils/image-link-utils.ts b/src/utils/image-link-utils.ts index a49f81ba..cb2f686b 100644 --- a/src/utils/image-link-utils.ts +++ b/src/utils/image-link-utils.ts @@ -1,6 +1,6 @@ import { computed } from 'vue' import { ImageLinkFormatModel, UploadedImageModel } from '@/common/model' -import { copyText } from '@/utils' +import { copyMessage, copyText } from '@/utils' import i18n from '@/plugins/vue/i18n' import { store } from '@/stores' @@ -30,7 +30,7 @@ export const generateImageLink = (imageObj: UploadedImageModel): string | null = * @param imageLink * @param imageName */ -const transformImageLink = (imageLink: string | null, imageName: string) => { +export const transformImageLink = (imageLink: string | null, imageName: string) => { const userSettings = computed(() => store.getters.getUserSettings).value if (userSettings.imageLinkFormat.enable) { const selectedFormat = userSettings.imageLinkFormat.selected @@ -46,18 +46,6 @@ const transformImageLink = (imageLink: string | null, imageName: string) => { return imageLink } -const copyMessage = (autoCopy = false) => { - const message: string = autoCopy - ? i18n.global.t('copy_success_1') - : i18n.global.t('copy_success_2') - - ElMessage({ - type: autoCopy ? 'info' : 'success', - message, - duration: autoCopy ? 6000 : 4000 - }) -} - /** * 复制单张图片链接 * @param imgObj @@ -67,7 +55,7 @@ export const copyImageLink = (imgObj: UploadedImageModel, autoCopy: boolean = fa const link = transformImageLink(generateImageLink(imgObj), imgObj.name) if (link) { copyText(link, () => { - copyMessage(autoCopy) + copyMessage(autoCopy, 'image') }) } else { ElMessage.error({ message: i18n.global.t('copy_fail_1') }) diff --git a/src/utils/upload-utils.ts b/src/utils/upload-utils.ts index 61987e2f..1dfb5d7d 100644 --- a/src/utils/upload-utils.ts +++ b/src/utils/upload-utils.ts @@ -1,4 +1,5 @@ import { UploadedImageModel, UserConfigInfoModel, UploadImageModel } from '@/common/model' +import { UploadedVideoModel, UploadVideoModel } from '@/common/model/video' import { store } from '@/stores' import { createCommit, @@ -8,8 +9,13 @@ import { getFileBlob, getBranchInfo } from '@/common/api' -import { PICX_UPLOAD_IMG_DESC } from '@/common/constant' +import { + PICX_UPLOAD_IMG_DESC, + PICX_UPLOAD_VIDEO_DESC, + PICX_UPLOAD_VIDEOS_DESC +} from '@/common/constant' import i18n from '@/plugins/vue/i18n' +import router from '@/router' /** * 图片上传成功之后的处理 @@ -195,3 +201,239 @@ export function uploadImageToGitHub( } }) } + +/** + * 视频上传成功之后的处理 + * @param res + * @param video + * @param userConfigInfo + */ +const videoUploadedHandle = ( + res: { name: string; sha: string; path: string; size: number }, + video: UploadVideoModel, + userConfigInfo: UserConfigInfoModel +) => { + let dir = userConfigInfo.selectedDir + + if (video?.reUploadInfo?.isReUpload) { + dir = video.reUploadInfo.dir + } + + // 上传状态处理 + video.uploadStatus.progress = 100 + video.uploadStatus.uploading = false + + const uploadedVideo: UploadedVideoModel = { + checked: false, + type: 'video', + uuid: video.uuid, + dir, + name: res.name, + sha: res.sha, + path: res.path, + deleting: false, + size: res.size, + deployed: true + } + + video.uploadedVideo = uploadedVideo + + // dirImageList 增加目录 + store.dispatch('DIR_IMAGE_LIST_ADD_DIR', dir) + + // dirImageList 增加视频 + store.dispatch('DIR_IMAGE_LIST_ADD_VIDEO', uploadedVideo) +} + +/** + * 上传视频的 URL 处理 + * @param config + * @param videoObj + */ +export const uploadVideoUrlHandle = ( + config: UserConfigInfoModel, + videoObj: UploadVideoModel +): string => { + const { owner, repo, selectedDir: dir } = config + const filename: string = videoObj.filename.final + + let path = filename + + if (dir !== '/') { + path = `${dir}/${filename}` + } + + if (videoObj?.reUploadInfo?.isReUpload) { + path = videoObj.reUploadInfo.path + } + + return `/repos/${owner}/${repo}/contents/${path}` +} + +/** + * 上传一个视频到 GitHub 仓库 + * @param userConfigInfo + * @param video + */ +export function uploadVideoToGitHub( + userConfigInfo: UserConfigInfoModel, + video: UploadVideoModel +): Promise { + const { branch, email, owner } = userConfigInfo + + const data: any = { + message: PICX_UPLOAD_VIDEO_DESC, + branch, + content: video.base64.originalBase64.split(',')[1] + } + + if (email) { + data.committer = { + name: owner, + email + } + } + + video.uploadStatus.uploading = true + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + const uploadRes = await uploadSingleImage(uploadVideoUrlHandle(userConfigInfo, video), data) + console.log('uploadSingleVideo >> ', uploadRes) + video.uploadStatus.uploading = false + if (uploadRes) { + const { name, sha, path, size } = uploadRes.content + videoUploadedHandle({ name, sha, path, size }, video, userConfigInfo) + resolve(true) + } else { + resolve(false) + } + }) +} + +/** + * 校验用户配置信息 + * @param userConfigInfo 用户配置信息 + */ +async function validateConfig(userConfigInfo: UserConfigInfoModel) { + const { token, repo, selectedDir } = userConfigInfo + + if (!token) { + ElMessage.error({ message: i18n.global.t('upload_page.message1') }) + await router.push('/config') + return false + } + + if (!repo) { + ElMessage.error({ message: i18n.global.t('upload_page.message2') }) + await router.push('/config') + return false + } + + if (!selectedDir) { + ElMessage.error({ message: i18n.global.t('upload_page.message3') }) + await router.push('/config') + return false + } + + return true +} + +export async function beforeUpload( + userConfigInfo: UserConfigInfoModel, + fileList: T[] +) { + if (!validateConfig(userConfigInfo)) { + return [] + } + + const notYetUploadList = fileList.filter((x) => x.uploadStatus.progress === 0) + + if (notYetUploadList.length === 0) { + ElMessage.error({ message: i18n.global.t('upload_page.message4') }) + return [] + } + + return notYetUploadList +} + +/** + * 上传多个视频到 GitHub 仓库 + * @param userConfigInfo + * @param imgs + */ +export async function uploadVideosToGitHub( + userConfigInfo: UserConfigInfoModel, + videos: UploadVideoModel[] +): Promise { + const { branch, repo, selectedDir, owner } = userConfigInfo + + const blobs = [] + // eslint-disable-next-line no-restricted-syntax + for (const video of videos) { + video.uploadStatus.uploading = true + const tempBase64 = ( + video.base64.compressBase64 || + video.base64.watermarkBase64 || + video.base64.originalBase64 + ).split(',')[1] + // 上传图片文件,为仓库创建 blobs + const blobRes = await getFileBlob(tempBase64, owner, repo) + if (blobRes) { + blobs.push({ video, ...blobRes }) + } else { + video.uploadStatus.uploading = false + ElMessage.error(i18n.global.t('upload_page.tip_11', { name: video.filename.final })) + } + } + + // 获取 head,用于获取当前分支信息(根目录的 tree sha 以及 head commit sha) + const branchRes: any = await getBranchInfo(owner, repo, branch) + if (!branchRes) { + return Promise.resolve(false) + } + + const finalPath = selectedDir === '/' ? '' : `${selectedDir}/` + + // 创建 tree + const treeRes = await createTree( + owner, + repo, + blobs.map((x: any) => ({ + sha: x.sha, + path: `${finalPath}${x.video.filename.final}` + })), + branchRes + ) + if (!treeRes) { + return Promise.resolve(false) + } + + // 创建 commit 节点 + const commitRes: any = await createCommit( + owner, + repo, + treeRes, + branchRes, + PICX_UPLOAD_VIDEOS_DESC + ) + if (!commitRes) { + return Promise.resolve(false) + } + + // 将当前分支 ref 指向新创建的 commit + const refRes = await createRef(owner, repo, branch, commitRes.sha) + if (!refRes) { + return Promise.resolve(false) + } + + blobs.forEach((blob: any) => { + const name = blob.video.filename.final + videoUploadedHandle( + { name, sha: blob.sha, path: `${finalPath}${name}`, size: 0 }, + blob.video, + userConfigInfo + ) + }) + return Promise.resolve(true) +} diff --git a/src/utils/video-utils.ts b/src/utils/video-utils.ts new file mode 100644 index 00000000..79528e83 --- /dev/null +++ b/src/utils/video-utils.ts @@ -0,0 +1,201 @@ +import { computed } from 'vue' +import { UploadedVideoModel, UploadVideoModel } from '@/common/model' +import { store } from '@/stores' +import { copyMessage, copyText, getUuid } from './common-utils' +import i18n from '@/plugins/vue/i18n' + +/** + * 视频 File 格式转 Base64 格式 + * @param file + */ +export function videoFileToBase64(file: File): Promise { + return new Promise((resolve) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => { + const base64 = reader.result as string + resolve(base64) + } + reader.onerror = () => resolve(null) + }) +} + +/** + * 生成一个上传的视频对象 + */ +export const createUploadVideoObject = (): UploadVideoModel => { + return { + uuid: '', + objectURL: '', + base64: { + originalBase64: '', + watermarkBase64: null, + compressBase64: null + }, + fileInfo: { + originalFile: null, + compressFile: null, + watermarkFile: null + }, + filename: { + hash: '', + suffix: '', + name: '', + prefix: '', + final: '', + initName: '', + newName: '', + isAddHash: true, + isRename: false, + isAddPrefix: false + }, + uploadStatus: { + progress: 0, + uploading: false + }, + reUploadInfo: { + dir: '', + path: '', + isReUpload: false + } + } +} + +/** + * 文件名称添加前缀的处理 + * @param filename + * @param isAddPrefix + */ +export const addPrefixHandle = ( + filenameObj: UploadVideoModel['filename'], + isAddPrefix: boolean +) => { + filenameObj.isAddPrefix = isAddPrefix + if (isAddPrefix) { + filenameObj.name = `${filenameObj.prefix}${filenameObj.initName}` + } else { + filenameObj.name = `${filenameObj.initName}` + } + if (filenameObj.isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 文件名称添加哈希值的处理 + * @param filenameObj + * @param isAddHash + */ +export const addHashHandle = (filenameObj: UploadVideoModel['filename'], isAddHash: boolean) => { + filenameObj.isAddHash = isAddHash + if (isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 重命名 + * @param filenameObj + * @param isRename + */ +export const rename = ( + filenameObj: UploadVideoModel['filename'], + isRename: boolean, + isAddPrefix: boolean +) => { + filenameObj.isRename = isRename + + if (isRename) { + filenameObj.name = filenameObj.newName.trim().replace(/\s+/g, '-') + } else { + addPrefixHandle(filenameObj, isAddPrefix) // 恢复列表 prefix 选项 + } + + if (filenameObj.isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 生成一个视频链接 + * @param videoObj + */ +export const generateVideoLink = (videoObj: UploadedVideoModel): string | null => { + const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value + const userSettings = computed(() => store.getters.getUserSettings).value + + const { selected } = userSettings.imageLinkType + const { rule } = userSettings.imageLinkType.presetList[selected] + if (rule) { + const { owner, repo, branch } = userConfigInfo + return rule + .replaceAll('{{owner}}', owner) + .replaceAll('{{repo}}', repo) + .replaceAll('{{branch}}', branch) + .replaceAll('{{path}}', videoObj.path) + } + return null +} + +/** + * 复制单个视频链接 + * @param videoObj + * @param autoCopy + */ +export const copyVideoLink = (videoObj: UploadedVideoModel, autoCopy: boolean = false) => { + const link = generateVideoLink(videoObj) + if (link) { + copyText(link, () => { + copyMessage(autoCopy, 'video') + }) + } else { + ElMessage.error({ message: i18n.global.t('copy_fail_1') }) + } +} + +/** + * 批量复制视频链接 + * @param uploadedVideoList 视频对象列表 + * @param autoCopy + */ +export const batchCopyVideoLinks = ( + uploadedVideoList: UploadedVideoModel[], + autoCopy: boolean = false +) => { + if (uploadedVideoList?.length > 0) { + let linksTxt = '' + uploadedVideoList.forEach((video, index) => { + const link = generateVideoLink(video) + linksTxt += `${link}${index < uploadedVideoList.length - 1 ? '\n' : ''}` + }) + copyText(linksTxt, () => { + copyMessage(autoCopy) + }) + } +} + +/** + * 生成一个图床管理中的视频对象 + * @param item + * @param selectedDir + */ +export const createManagementVideoObject = (item: any, selectedDir: string): UploadedVideoModel => { + return { + type: 'video', + uuid: getUuid(), + dir: selectedDir, + name: item.name, + sha: item.sha, + path: item.path, + deleting: false, + size: item.size, + checked: false, + deployed: true + } +} diff --git a/src/views/imgs-management/components/image-selector/image-selector.vue b/src/views/imgs-management/components/image-selector/image-selector.vue index f0e7f150..62330656 100644 --- a/src/views/imgs-management/components/image-selector/image-selector.vue +++ b/src/views/imgs-management/components/image-selector/image-selector.vue @@ -1,5 +1,5 @@ + + + + diff --git a/src/views/imgs-management/imgs-management.styl b/src/views/imgs-management/imgs-management.styl index bb507297..afedc926 100644 --- a/src/views/imgs-management/imgs-management.styl +++ b/src/views/imgs-management/imgs-management.styl @@ -64,6 +64,7 @@ $grid-gap = 20rem .image-card-list { + margin-bottom $grid-gap .image-card-item { grid-row span 2 grid-column span 2 diff --git a/src/views/imgs-management/imgs-management.util.ts b/src/views/imgs-management/imgs-management.util.ts index 30331317..3eee5a18 100644 --- a/src/views/imgs-management/imgs-management.util.ts +++ b/src/views/imgs-management/imgs-management.util.ts @@ -43,6 +43,10 @@ export const filterDirContent = (content: any, type: string): any => { return content.imageList.filter((x: any) => x.type === 'image') } + if (type === 'video') { + return content.videoList?.filter((x: any) => x.type === 'video') || [] + } + return [] } diff --git a/src/views/imgs-management/imgs-management.vue b/src/views/imgs-management/imgs-management.vue index 36df1bc8..8516e4a8 100644 --- a/src/views/imgs-management/imgs-management.vue +++ b/src/views/imgs-management/imgs-management.vue @@ -9,9 +9,8 @@ :element-loading-text="$t('management_page.loadingTxt1')" >
-
    +
    • - + +
    - + {{ $t('management_page.text_2') }}
+ @@ -56,13 +61,15 @@ import { getDirContent, shiftKeyHandle } from '@/views/imgs-management/imgs-management.util' -import { DirModeEnum, UploadedImageModel } from '@/common/model' +import { DirModeEnum, UploadedImageModel, UploadedVideoModel } from '@/common/model' import { ContextmenuEnum } from '@/common/directive/types' import ImageSelector from '@/views/imgs-management/components/image-selector/image-selector.vue' import ToolsBar from '@/views/imgs-management/components/tools-bar/tools-bar.vue' import FolderCard from '@/views/imgs-management/components/folder-card/folder-card.vue' import ImageCard from '@/views/imgs-management/components/image-card/image-card.vue' import router from '@/router' +import VideoCard from './components/video-card/video-card.vue' +import VideoPreview from '@/components/video-preview/video-preview.vue' const store = useStore() @@ -74,6 +81,11 @@ const loadingImageList = ref(false) const currentPathDirList = ref([]) const currentPathImageList = ref([]) +const currentPathVideoList = ref([]) + +const mediaList = computed(() => { + return [...currentPathImageList.value, ...currentPathVideoList.value] +}) const isShowBatchTools = ref(false) @@ -84,12 +96,15 @@ async function dirContentHandle(dir: string) { if (dirContent) { const dirs = filterDirContent(dirContent, 'dir') const images = filterDirContent(dirContent, 'image') + const videos = filterDirContent(dirContent, 'video') if (!dirs.length && !images.length) { await getRepoPathContent(userConfigInfo, dir) } else { currentPathDirList.value = dirs currentPathImageList.value = images + currentPathVideoList.value = videos store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value }) + store.commit('REPLACE_VIDEO_CARD', { checkedVideoArr: currentPathVideoList.value }) } } else { await getRepoPathContent(userConfigInfo, dir) @@ -133,6 +148,14 @@ async function reloadCurrentDirContent() { loadingImageList.value = false } +const videoPreviewRef = ref | null>(null) +const handlePreview = (videoItem: { name: string; url: string }) => { + videoPreviewRef.value?.handleOpen({ + url: videoItem.url, + name: videoItem.name + }) +} + onMounted(() => { shiftKeyHandle() initDirImageList() @@ -155,7 +178,9 @@ watch( if (dirContent) { currentPathDirList.value = filterDirContent(dirContent, 'dir') currentPathImageList.value = filterDirContent(dirContent, 'image') + currentPathVideoList.value = filterDirContent(dirContent, 'video') store.commit('REPLACE_IMAGE_CARD', { checkedImgArr: currentPathImageList.value }) + store.commit('REPLACE_VIDEO_CARD', { checkedVideoArr: currentPathVideoList.value }) } }, { deep: true } @@ -172,11 +197,14 @@ watch( watch( () => store.getters.getUploadAreaState.activeInfo, (nv) => { - const { type, dir, img } = nv || {} + const { type, dir, img, video } = nv || {} currentPathImageList.value.forEach((item) => { item.active = type === ContextmenuEnum.img && item.name === img?.name }) + currentPathVideoList.value.forEach((item) => { + item.active = type === ContextmenuEnum.video && item.name === video?.name + }) currentPathDirList.value.forEach((dirObj) => { dirObj.active = type === ContextmenuEnum.dir && dirObj.dir === dir diff --git a/src/views/upload-video/components/getting-video/getting-video.styl b/src/views/upload-video/components/getting-video/getting-video.styl new file mode 100644 index 00000000..23ebebc2 --- /dev/null +++ b/src/views/upload-video/components/getting-video/getting-video.styl @@ -0,0 +1,61 @@ +.getting-images-container { + position relative + z-index 999 + display flex + align-items center + justify-content center + box-sizing border-box + width 100% + height 300rem + border 4rem dashed var(--text-color-4) + border-radius 8rem + + &.focus { + border-color var(--el-color-primary) + } + + &.disabled { + pointer-events none + } + + &:hover { + border-color var(--el-color-primary) + } + + label { + position absolute + z-index 1000 + display block + width 100% + height 100% + cursor pointer + } + + input[type="file"] { + position absolute + top -9999rem + left -9999rem + } + + .upload-area-tips { + color #aaa + text-align center + user-select none + + .icon { + font-size 100rem + } + + .text { + font-size 20rem + cursor default + } + } + + + .preview-video { + width 100% + height 100% + background-color #000 + } +} diff --git a/src/views/upload-video/components/getting-video/getting-video.vue b/src/views/upload-video/components/getting-video/getting-video.vue new file mode 100644 index 00000000..fdb5511f --- /dev/null +++ b/src/views/upload-video/components/getting-video/getting-video.vue @@ -0,0 +1,57 @@ + + + + + + diff --git a/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts new file mode 100644 index 00000000..9d34b791 --- /dev/null +++ b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-unused-vars */ + +import { ref } from 'vue' +import { gettingVideoFilesHandle } from '@/utils/file-utils' +import { VideoHandleResult } from '@/common/model' + +export const useGettingVideo = (onSelectSuccess: (result: VideoHandleResult[]) => void) => { + const curShowVideo = ref<{ + uuid: string + objectURL: string + }>({ + uuid: '', + objectURL: '' + }) + + const handleVideoFiles = async ( + files: FileList | undefined | null + ): Promise => { + console.log('files', files) + if (!files || files.length === 0) return [] + + const uploadResult: VideoHandleResult[] = [] + + await Promise.all( + Array.from(files).map(async (file) => { + const result = await gettingVideoFilesHandle(file) + if (result) { + uploadResult.push(result) + } + }) + ) + + if (uploadResult.length > 0) { + const lastIndex = uploadResult.length - 1 + const { uuid, objectURL } = uploadResult[lastIndex] + curShowVideo.value = { + uuid, + objectURL + } + } + + return uploadResult + } + + const onSelect = async (e: Event) => { + const target = e.target as HTMLInputElement + + onSelectSuccess(await handleVideoFiles(target.files)) + + target.value = '' + target.value = target.defaultValue + } + + const onDrop = async (e: DragEvent) => { + onSelectSuccess(await handleVideoFiles(e.dataTransfer?.files)) + } + + const onPaste = async (e: ClipboardEvent) => { + onSelectSuccess(await handleVideoFiles(e.clipboardData?.files)) + } + + return { + curShowVideo, + onSelect, + onDrop, + onPaste + } +} diff --git a/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts b/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts new file mode 100644 index 00000000..4f406512 --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts @@ -0,0 +1,66 @@ +import { computed, ref } from 'vue' +import { Props } from '../type' +import { store } from '@/stores' +import { addHashHandle, addPrefixHandle, rename } from '@/utils/video-utils' + +export const useFilename = (props: Readonly) => { + const renameInputRef = ref(null) + + const userSettings = computed(() => store.getters.getUserSettings).value + + const fileNameOperateData = ref({ + isAddHash: false, + isAddPrefix: false, + isRename: false, + newName: '' + }) + + const onHashRename = (e: boolean) => { + addHashHandle(props.videoItem.filename, e) + } + + const onPrefixNaming = (e: boolean) => { + addPrefixHandle(props.videoItem.filename, e) + } + + const onRename = () => { + props.videoItem!.filename.newName = fileNameOperateData.value.newName + setTimeout(() => { + renameInputRef.value?.focus() + }, 100) + rename( + props.videoItem!.filename, + fileNameOperateData.value.isRename, + fileNameOperateData.value.isAddPrefix + ) + } + + const initFilename = () => { + const { imageName } = userSettings + if (props.videoItem!.uploadStatus.progress === 0) { + props.videoItem!.filename.isAddHash = imageName.enableHash + props.videoItem!.filename.isAddPrefix = imageName.addPrefix.enable + props.videoItem!.filename.prefix = imageName.addPrefix.prefix + + // 添加前缀处理 + addPrefixHandle(props.videoItem.filename, imageName.addPrefix.enable) + + // 添加哈希值处理 + addHashHandle(props.videoItem.filename, imageName.enableHash) + + fileNameOperateData.value.isAddHash = props.videoItem!.filename.isAddHash + fileNameOperateData.value.isAddPrefix = props.videoItem!.filename.isAddPrefix + fileNameOperateData.value.isRename = props.videoItem!.filename.isRename + fileNameOperateData.value.newName = props.videoItem!.filename.newName + } + } + + return { + renameInputRef, + fileNameOperateData, + initFilename, + onHashRename, + onPrefixNaming, + onRename + } +} diff --git a/src/views/upload-video/components/upload-video-card/type.ts b/src/views/upload-video/components/upload-video-card/type.ts new file mode 100644 index 00000000..8742c303 --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/type.ts @@ -0,0 +1,5 @@ +import { UploadVideoModel } from '@/common/model' + +export type Props = { + videoItem: UploadVideoModel +} diff --git a/src/views/upload-video/components/upload-video-card/upload-video-card.styl b/src/views/upload-video/components/upload-video-card/upload-video-card.styl new file mode 100644 index 00000000..da0ee78b --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/upload-video-card.styl @@ -0,0 +1,210 @@ +.upload-video-card-container { + position relative + display flex + flex-direction column + justify-content flex-start + box-sizing border-box + width 100% + margin-bottom 20rem + overflow hidden + border 1rem solid var(--border-color) + border-radius 6rem + + &.wait-upload { + border-color var(--await-upload-color) + } + + &.uploading { + border-color var(--uploading-color) + } + + &.uploaded { + border-color var(--uploaded-color) + } + + &:last-child { + margin-bottom 0 + } + + + &:hover { + .del-video-btn { + display block + } + } + + + .video-show-container { + position relative + box-sizing border-box + width 100% + height 140rem + + .preview-video { + width 100% + height 100% + background #000 + cursor pointer + } + } + + + .before-upload-handle-container { + position relative + box-sizing border-box + width 100% + border-top 1rem solid var(--border-color) + + .video-name-box { + position relative + display flex + align-items center + justify-content space-between + box-sizing border-box + width 100% + padding 2rem 5rem + font-size 13rem + border-bottom 1rem solid var(--border-color) + + &.no-border { + border-bottom none + } + + .video-name { + position relative + box-sizing border-box + width calc(100% - 20rem) + } + + .fold-btn { + position relative + display flex + align-items center + justify-content end + box-sizing border-box + width 20rem + font-size 15rem + cursor pointer + } + } + + .video-name-operate-box { + position relative + display flex + flex-direction column + justify-content center + box-sizing border-box + padding 2rem 5rem + border-bottom 1rem solid var(--border-color) + + &.folded { + display none + } + + + .operate-item { + display flex + align-items center + height 28rem + + .rename-input { + margin-left 10rem + } + } + } + + .video-info-box { + display flex + align-items center + justify-content space-between + padding 5rem + font-size 12rem + user-select none + + .file-size-box { + transform scale(0.9) + + .file-size-item { + padding 2rem 3rem + background var(--background-color-3) + border-radius 3rem + } + + + .original-file-size { + margin-left -4rem + + &.del-line { + text-decoration line-through + } + } + + + .finial-file-size { + margin-left 6rem + color var(--el-color-primary) + } + } + } + } + + + .after-upload-handle-container { + position relative + box-sizing border-box + width 100% + height 30rem + color var(--el-color-primary) + font-size 13rem + background var(--el-color-primary-light-9) + border-top 1rem solid var(--border-color) + cursor pointer + + &:hover { + color var(--el-color-white) + background var(--el-color-primary) + } + } + + + .del-video-btn { + position absolute + top 6rem + right 6rem + display none + color var(--background-color) + font-size 22rem + cursor pointer + } + + .upload-status-box { + position absolute + top -8rem + left -16rem + box-sizing border-box + width 46rem + height 26rem + color #fff + text-align center + box-shadow 0 1rem 1rem var(--border-color) + transform rotate(315deg) + + &.wait-upload { + background var(--await-upload-color) + } + + &.uploaded { + background var(--uploaded-color) + + .el-icon { + margin-top 12rem + } + } + + .el-icon { + margin-top 10rem + font-size 12rem + transform rotate(45deg) + } + } +} diff --git a/src/views/upload-video/components/upload-video-card/upload-video-card.vue b/src/views/upload-video/components/upload-video-card/upload-video-card.vue new file mode 100644 index 00000000..0a0538be --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/upload-video-card.vue @@ -0,0 +1,210 @@ + + + + + + diff --git a/src/views/upload-video/hooks/use-upload-video.ts b/src/views/upload-video/hooks/use-upload-video.ts new file mode 100644 index 00000000..7b3e44fe --- /dev/null +++ b/src/views/upload-video/hooks/use-upload-video.ts @@ -0,0 +1,95 @@ +import { computed, ref, Ref } from 'vue' +import { UploadedVideoModel, UploadStatusEnum, UploadVideoModel } from '@/common/model' +import { store } from '@/stores' +import { beforeUpload, uploadVideosToGitHub, uploadVideoToGitHub } from '@/utils/upload-utils' +import i18n from '@/plugins/vue/i18n' +import { batchCopyVideoLinks, copyVideoLink } from '@/utils/video-utils' + +export const useUploadVideo = ( + uploadVideoList: Ref, + onUploadSuccess: () => void +) => { + const uploading = ref(false) + + const userConfigInfo = computed(() => store.getters.getUserConfigInfo) + + const doUploadVideos = async (videoList: UploadVideoModel[]) => { + // 单个视频 + if (videoList.length === 1) { + if (await uploadVideoToGitHub(userConfigInfo.value, videoList[0])) { + return UploadStatusEnum.uploaded + } + return UploadStatusEnum.uploadFail + } + + // 多个视频 + if (videoList.length > 1) { + if (await uploadVideosToGitHub(userConfigInfo.value, videoList)) { + return UploadStatusEnum.allUploaded + } + return UploadStatusEnum.uploadFail + } + + return UploadStatusEnum.uploadFail + } + + // 上传成功之后的操作 + const afterUploadSuccess = async ( + uploadedVideo: UploadedVideoModel[], + isBatch: boolean = false + ) => { + // 自动复制链接到系统剪贴板 + if (isBatch) { + batchCopyVideoLinks(uploadedVideo, true) + } else { + copyVideoLink(uploadedVideo[0], true) + } + await store.dispatch('SET_USER_CONFIG_INFO', { + viewDir: userConfigInfo.value.selectedDir + }) + onUploadSuccess() + } + + const uploadVideo = async () => { + const notYetUploadList = await beforeUpload(userConfigInfo.value, uploadVideoList.value) + if (notYetUploadList.length === 0) { + return + } + + uploading.value = true + + const result = await doUploadVideos(notYetUploadList) + + uploading.value = false + + const uploadedVideo = notYetUploadList + .filter((v) => v.uploadStatus.progress === 100) + .map((x) => x.uploadedVideo!) + + switch (result) { + // 单视频上传成功 + case UploadStatusEnum.uploaded: + ElMessage.success({ message: i18n.global.t('upload_page.message5') }) + await afterUploadSuccess(uploadedVideo) + break + + // 多视频上传成功 + case UploadStatusEnum.allUploaded: + ElMessage.success({ message: i18n.global.t('upload_page.message6') }) + await afterUploadSuccess(uploadedVideo, true) + break + + // 上传失败(网络错误等原因) + case UploadStatusEnum.uploadFail: + ElMessage.error({ message: i18n.global.t('upload_page.message7') }) + break + default: + break + } + console.log('开始上传视频') + } + + return { + uploadVideo + } +} diff --git a/src/views/upload-video/upload-video.styl b/src/views/upload-video/upload-video.styl new file mode 100644 index 00000000..c3eb614f --- /dev/null +++ b/src/views/upload-video/upload-video.styl @@ -0,0 +1,102 @@ +.upload-page-container { + display flex + justify-content space-between + width 100% + height 100% + + .upload-page-left { + flex-shrink 0 + box-sizing border-box + width 300rem + height 100% + margin-right 10rem + border-top-right-radius 0 + border-bottom-right-radius 0 + + .uploaded-item { + width 100% + margin-bottom 20rem + + &:last-child { + margin-bottom 0 + } + } + } + + + .upload-page-right { + box-sizing border-box + width 100% + height 100% + overflow-y auto + + &.has-left { + border-top-left-radius 0 + border-bottom-left-radius 0 + } + + .row-item { + display flex + justify-content center + box-sizing border-box + width 100% + margin-bottom 16rem + + &:last-child { + margin-bottom 0 + } + + .content-box { + box-sizing border-box + width 100% + max-width $content-max-width + margin 0 auto + + &.upload-area-status { + display flex + align-items center + justify-content space-between + margin-bottom 10rem + font-size 14rem + } + + + &.operation-btn { + display flex + justify-content flex-end + } + + + .shortcut-key { + margin-left 5rem + padding 4rem 5rem + font-size 12rem + letter-spacing 1rem + border-radius 4rem + box-shadow 1rem 2rem 3rem var(--shadow-color) + + +picx-tablet() { + display none + } + } + } + } + + .upload-tools { + width 100% + + .repos-dir-info { + margin-bottom 20rem + font-size 12rem + + .repos-dir-info-item { + margin-right 10rem + + &:last-child { + margin-right 0 + } + } + } + } + } +} diff --git a/src/views/upload-video/upload-video.vue b/src/views/upload-video/upload-video.vue new file mode 100644 index 00000000..fa7bb038 --- /dev/null +++ b/src/views/upload-video/upload-video.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/views/upload-video/utils/generate.ts b/src/views/upload-video/utils/generate.ts new file mode 100644 index 00000000..167b0018 --- /dev/null +++ b/src/views/upload-video/utils/generate.ts @@ -0,0 +1,36 @@ +import { computed } from 'vue' +import { VideoHandleResult, UploadVideoModel } from '@/common/model' +import { store } from '@/stores' +import { createUploadVideoObject } from '@/utils/video-utils' + +const userSettings = computed(() => store.getters.getUserSettings).value +export const generateUploadVideoObject = (obj: VideoHandleResult): UploadVideoModel => { + const tmp: UploadVideoModel = createUploadVideoObject() + tmp.uuid = obj.uuid + tmp.base64.originalBase64 = obj.base64 + tmp.fileInfo.originalFile = obj.file + tmp.objectURL = obj.objectURL + + const { imageName } = userSettings + + const hash = obj.uuid + + // 处理文件名,去除空格字符 + const nameHandled = obj.file.name.trim().replaceAll(' ', '-') + + const tmpIdx = nameHandled.lastIndexOf('.') + const name = nameHandled.slice(0, tmpIdx) + const suffix = nameHandled.slice(tmpIdx + 1) + + tmp.filename.initName = name + tmp.filename.name = imageName.addPrefix.enable ? `${imageName.addPrefix.prefix}${name}` : name + tmp.filename.prefix = imageName.addPrefix.prefix + tmp.filename.hash = hash + tmp.filename.suffix = suffix + tmp.filename.final = imageName.enableHash + ? `${tmp.filename.name}.${hash}.${suffix}` + : `${tmp.filename.name}.${suffix}` + tmp.filename.isAddHash = imageName.enableHash + tmp.filename.isAddPrefix = imageName.addPrefix.enable + return tmp +}