diff --git a/.eslintrc.js b/.eslintrc.js index e0f9900..1e529a6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,8 @@ module.exports = { 'no-async-promise-executor': 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/indent': ['error', 2] + '@typescript-eslint/indent': ['error', 2], + 'vue/no-v-html': 'off' }, parserOptions: { parser: '@typescript-eslint/parser' diff --git a/README.md b/README.md index 1200aa6..983eb5a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ - 保留了PicGo的所有功能,兼容已有的PicGo插件系统,包括和typora、obsidian等的搭配 - 相册中可同步删除云端图片 -- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件和图片预览等 +- 支持管理所有图床,可以在线进行云端目录查看、文件搜索、批量上传、批量下载、删除文件等 +- 支持预览多种格式的文件,包括图片、视频、纯文本文件和markdown文件等,具体支持的格式请参考[支持的文件格式列表](https://github.com/Kuingsmile/PicList/supported_format.md) - 管理界面使用内置数据库缓存目录,加速目录加载速度 - 对于私有存储桶等支持复制预签名链接进行分享 - 优化了PicGo的界面,解锁了窗口大小限制,同时美化了部分界面布局 diff --git a/package.json b/package.json index 1307960..4e10631 100644 --- a/package.json +++ b/package.json @@ -16,18 +16,21 @@ "postuninstall": "electron-builder install-app-deps", "release": "vue-cli-service electron:build --publish always", "upload-dist": "node ./scripts/upload-dist-to-r2.js", - "link": "node ./scripts/cos-link.js" + "link": "node ./scripts/link.js" }, "dependencies": { "@aws-sdk/client-s3": "^3.272.0", "@aws-sdk/lib-storage": "^3.272.0", "@aws-sdk/s3-request-presigner": "^3.272.0", "@element-plus/icons-vue": "^2.0.10", + "@highlightjs/vue-plugin": "^2.1.0", "@imengyu/vue3-context-menu": "^1.2.2", "@octokit/rest": "^19.0.7", "@picgo/i18n": "^1.0.0", "@picgo/store": "^2.0.4", + "@types/marked": "^4.0.8", "@types/mime-types": "^2.1.1", + "@videojs-player/vue": "^1.0.0", "ali-oss": "^6.17.1", "aws-sdk": "^2.1317.0", "axios": "^1.3.2", @@ -41,10 +44,12 @@ "form-data": "^4.0.0", "fs-extra": "^11.1.0", "got": "^12.5.3", + "highlight.js": "^11.7.0", "hpagent": "^1.2.0", "keycode": "^2.2.0", "lodash-id": "^0.14.0", "lowdb": "^1.0.0", + "marked": "^4.2.12", "mime-types": "^2.1.35", "mitt": "^3.0.0", "piclist": "^0.0.8", @@ -55,10 +60,12 @@ "shell-path": "2.1.0", "upyun": "^3.4.6", "uuid": "^9.0.0", + "video.js": "^8.0.4", "vue": "^3.2.47", "vue-router": "^4.1.6", "vue3-lazyload": "^0.3.6", "vue3-photo-preview": "^0.2.9", + "webdav": "^5.0.0-r3", "write-file-atomic": "^4.0.1" }, "devDependencies": { diff --git a/scripts/cos-link.js b/scripts/link.js similarity index 100% rename from scripts/cos-link.js rename to scripts/link.js diff --git a/src/main.ts b/src/main.ts index 337fd94..251ba8c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,11 @@ import vue3PhotoPreview from 'vue3-photo-preview' import 'vue3-photo-preview/dist/index.css' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' +import 'highlight.js/styles/atom-one-dark.css' +import hljsVuePlugin from '@highlightjs/vue-plugin' +import hljsCommon from 'highlight.js/lib/common' +import VueVideoPlayer from '@videojs-player/vue' +import 'video.js/dist/video-js.css' webFrame.setVisualZoomLevelLimits(1, 1) @@ -60,6 +65,9 @@ app.use(store) app.use(vue3PhotoPreview) app.use(pinia) app.use(ContextMenu) +console.log(hljsCommon.highlightAuto('

Highlight.js has been registered successfully!

').value) +app.use(hljsVuePlugin) +app.use(VueVideoPlayer) app.mount('#app') initTalkingData() diff --git a/src/main/manage/apis/api.ts b/src/main/manage/apis/api.ts index b183431..3f611fe 100644 --- a/src/main/manage/apis/api.ts +++ b/src/main/manage/apis/api.ts @@ -6,6 +6,7 @@ import SmmsApi from './smms' import GithubApi from './github' import ImgurApi from './imgur' import S3plistApi from './s3plist' +import WebdavplistApi from './webdavplist' export default { TcyunApi, @@ -15,5 +16,6 @@ export default { SmmsApi, GithubApi, ImgurApi, - S3plistApi + S3plistApi, + WebdavplistApi } diff --git a/src/main/manage/apis/s3plist.ts b/src/main/manage/apis/s3plist.ts index c5d8e03..f91b6e5 100644 --- a/src/main/manage/apis/s3plist.ts +++ b/src/main/manage/apis/s3plist.ts @@ -18,7 +18,7 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner' import https from 'https' import http from 'http' import { ManageLogger } from '../utils/logger' -import { formatError, getAgent, getFileMimeType, gotDownload } from '../utils/common' +import { formatEndpoint, formatError, getAgent, getFileMimeType, gotDownload } from '../utils/common' import { isImage } from '@/manage/utils/common' import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent' import windowManager from 'apis/app/window/windowManager' @@ -64,7 +64,7 @@ class S3plistApi { accessKeyId, secretAccessKey }, - endpoint: endpoint ? this.formatEndpoint(endpoint, sslEnabled) : undefined, + endpoint: endpoint ? formatEndpoint(endpoint, sslEnabled) : undefined, sslEnabled, s3ForcePathStyle, httpOptions: { @@ -75,9 +75,6 @@ class S3plistApi { this.agent = this.setAgent(proxy, sslEnabled) } - formatEndpoint = (endpoint: string, sslEnabled: boolean): string => - !/^https?:\/\//.test(endpoint) ? `${sslEnabled ? 'https' : 'http'}://${endpoint}` : endpoint - setAgent (proxy: string | undefined, sslEnabled: boolean) : HttpProxyAgent | HttpsProxyAgent | undefined { if (sslEnabled) { const agent = getAgent(proxy, true).https diff --git a/src/main/manage/apis/webdavplist.ts b/src/main/manage/apis/webdavplist.ts new file mode 100644 index 0000000..58a2757 --- /dev/null +++ b/src/main/manage/apis/webdavplist.ts @@ -0,0 +1,130 @@ +import ManageLogger from '../utils/logger' +import { createClient, WebDAVClient, FileStat } from 'webdav' +import { formatError, formatEndpoint, getInnerAgent } from '../utils/common' +import { isImage } from '@/manage/utils/common' +import http from 'http' +import https from 'https' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' + +class WebdavplistApi { + endpoint: string + username: string + password: string + sslEnabled: boolean + proxy: string | undefined + logger: ManageLogger + agent: https.Agent | http.Agent + ctx: WebDAVClient + + constructor (endpoint: string, username: string, password: string, sslEnabled: boolean, proxy: string | undefined, logger: ManageLogger) { + this.endpoint = formatEndpoint(endpoint, sslEnabled) + this.username = username + this.password = password + this.sslEnabled = sslEnabled + this.proxy = proxy + this.logger = logger + this.agent = getInnerAgent(proxy, sslEnabled).agent + this.ctx = createClient( + this.endpoint, + { + username: this.username, + password: this.password, + maxBodyLength: 4 * 1024 * 1024 * 1024, + maxContentLength: 4 * 1024 * 1024 * 1024, + httpsAgent: sslEnabled ? this.agent : undefined, + httpAgent: !sslEnabled ? this.agent : undefined + } + ) + } + + logParam = (error:any, method: string) => + this.logger.error(formatError(error, { class: 'WebdavplistApi', method })) + + formatFolder (item: FileStat, urlPrefix: string) { + return { + ...item, + key: item.filename.replace(/^\/+/, ''), + fileName: item.basename, + fileSize: 0, + Key: item.filename.replace(/^\/+/, ''), + formatedTime: '', + isDir: true, + checked: false, + isImage: false, + match: false, + url: `${urlPrefix}${item.filename}` + } + } + + formatFile (item: FileStat, urlPrefix: string) { + return { + ...item, + key: item.filename.replace(/^\/+/, ''), + fileName: item.basename, + fileSize: item.size, + Key: item.filename.replace(/^\/+/, ''), + formatedTime: new Date(item.lastmod).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.basename), + url: `${urlPrefix}${item.filename}` + } + } + + isRequestSuccess = (code: number) => code >= 200 && code < 300 + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { prefix, customUrl, cancelToken } = configMap + const urlPrefix = customUrl || this.endpoint + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as any + const result = { + fullList: [], + success: false, + finished: false + } + try { + res = await this.ctx.getDirectoryContents(prefix, { + deep: false, + details: true + }) + if (this.isRequestSuccess(res.status)) { + if (res.data && res.data.length) { + res.data.forEach((item: FileStat) => { + if (item.type === 'directory') { + result.fullList.push(this.formatFolder(item, urlPrefix)) + } else { + result.fullList.push(this.formatFile(item, urlPrefix)) + } + }) + } + } else { + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + } catch (error) { + this.logParam(error, 'getBucketListBackstage') + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } +} + +export default WebdavplistApi diff --git a/src/main/manage/manageApi.ts b/src/main/manage/manageApi.ts index 5458c23..6771915 100644 --- a/src/main/manage/manageApi.ts +++ b/src/main/manage/manageApi.ts @@ -69,6 +69,8 @@ export class ManageApi extends EventEmitter implements ManageApiType { return new API.ImgurApi(this.currentPicBedConfig.imgurUserName, this.currentPicBedConfig.accessToken, this.currentPicBedConfig.proxy, this.logger) case 's3plist': return new API.S3plistApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.secretAccessKey, this.currentPicBedConfig.endpoint, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.s3ForcePathStyle, this.currentPicBedConfig.proxy, this.logger) + case 'webdavplist': + return new API.WebdavplistApi(this.currentPicBedConfig.endpoint, this.currentPicBedConfig.username, this.currentPicBedConfig.password, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.proxy, this.logger) default: return {} as any } @@ -172,6 +174,12 @@ export class ManageApi extends EventEmitter implements ManageApiType { Location: 'smms', CreationDate: new Date().toISOString() }] + case 'webdavplist': + return [{ + Name: 'webdav', + Location: 'webdav', + CreationDate: new Date().toISOString() + }] default: console.log(param) return [] @@ -309,6 +317,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'github': case 'imgur': case 's3plist': + case 'webdavplist': try { client = this.createClient() as any return await client.getBucketListBackstage(param!) diff --git a/src/main/manage/utils/common.ts b/src/main/manage/utils/common.ts index f04f1c8..faa344e 100644 --- a/src/main/manage/utils/common.ts +++ b/src/main/manage/utils/common.ts @@ -14,8 +14,10 @@ import UpDownTaskQueue, downloadTaskSpecialStatus } from '../datastore/upDownTaskQueue' import { ManageLogger } from '../utils/logger' -import { formatHttpProxy } from '@/manage/utils/common' +import { formatHttpProxy, IHTTPProxy } from '@/manage/utils/common' import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent' +import http from 'http' +import https from 'https' export const getFSFile = async ( filePath: string, @@ -244,6 +246,47 @@ export const getAgent = (proxy:any, https: boolean = true) => { } } +export const getInnerAgent = (proxy: any, sslEnabled: boolean = true) => { + const formatProxy = formatHttpProxy(proxy, 'object') as IHTTPProxy + if (sslEnabled) { + return formatProxy + ? { + agent: new https.Agent({ + keepAlive: true, + keepAliveMsecs: 1000, + rejectUnauthorized: false, + scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, + host: formatProxy.host, + port: formatProxy.port + }) + } + : { + agent: new https.Agent({ + rejectUnauthorized: false, + keepAlive: true + }) + } + } else { + return formatProxy + ? { + agent: new http.Agent({ + keepAlive: true, + keepAliveMsecs: 1000, + scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, + host: formatProxy.host, + port: formatProxy.port + }) + } + : { + agent: new http.Agent({ + keepAlive: true, + keepAliveMsecs: 1000, + scheduling: 'lifo' as 'lifo' | 'fifo' | undefined + }) + } + } +} + export function getOptions ( method?: string, headers?: IStringKeyMap, @@ -270,3 +313,10 @@ export function getOptions ( }) return options } + +export const formatEndpoint = (endpoint: string, sslEnabled: boolean): string => + !/^https?:\/\//.test(endpoint) + ? `${sslEnabled ? 'https' : 'http'}://${endpoint}` + : sslEnabled + ? endpoint.replace('http://', 'https://') + : endpoint.replace('https://', 'http://') diff --git a/src/renderer/manage/pages/assets/webdavplist.png b/src/renderer/manage/pages/assets/webdavplist.png new file mode 100644 index 0000000..1c3b190 Binary files /dev/null and b/src/renderer/manage/pages/assets/webdavplist.png differ diff --git a/src/renderer/manage/pages/bucketPage.vue b/src/renderer/manage/pages/bucketPage.vue index d4275cd..fe5bf8a 100644 --- a/src/renderer/manage/pages/bucketPage.vue +++ b/src/renderer/manage/pages/bucketPage.vue @@ -1,4 +1,4 @@ -/* +ea/* *UI布局和部分样式代码参考了https://github.com/willnewii/qiniuClient *感谢作者@willnewii */ @@ -29,7 +29,7 @@ /> + +
+ + + + + + + + + +