diff --git a/chrome-app/manifest.json b/chrome-app/manifest.json index 721142f7..038a65c7 100644 --- a/chrome-app/manifest.json +++ b/chrome-app/manifest.json @@ -1,7 +1,7 @@ { "name": "StackEdit中文版", "description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器", - "version": "5.15.14", + "version": "5.15.15", "manifest_version": 2, "container" : "GITEE", "api_console_project_id" : "241271498917", diff --git a/package-lock.json b/package-lock.json index abdaafe5..16eafb33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stackedit", - "version": "5.15.14", + "version": "5.15.15", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index cfafecbe..d95dd2bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stackedit", - "version": "5.15.14", + "version": "5.15.15", "description": "免费, 开源, 功能齐全的 Markdown 编辑器", "author": "Benoit Schweblin, 豆萁", "license": "Apache-2.0", diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 5aebac3f..6569c0a4 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -34,7 +34,7 @@ export default { ]), }, methods: { - async setImgAndDoClick(items) { + async processUpload(items) { let file = null; if (!items || items.length === 0) { return; @@ -73,6 +73,11 @@ export default { if (currImgStorageStr) { store.commit('img/changeCheckedStorage', JSON.parse(currImgStorageStr)); } + // 当前本地图片路径配置 + const workspaceImgPath = localStorage.getItem('img/workspaceImgPath'); + if (workspaceImgPath) { + store.commit('img/setWorkspaceImgPath', JSON.parse(workspaceImgPath)); + } const editorElt = this.$el.querySelector('.editor__inner'); const onDiscussionEvt = cb => (evt) => { let elt = evt.target; @@ -100,11 +105,11 @@ export default { editorElt.addEventListener('drop', (event) => { const transItems = event.dataTransfer.items; - this.setImgAndDoClick(transItems); + this.processUpload(transItems); }); editorElt.addEventListener('paste', (event) => { const pasteItems = (event.clipboardData || window.clipboardData).items; - this.setImgAndDoClick(pasteItems); + this.processUpload(pasteItems); }); this.$watch( diff --git a/src/components/Modal.vue b/src/components/Modal.vue index 1e3920c8..b3f224bd 100644 --- a/src/components/Modal.vue +++ b/src/components/Modal.vue @@ -40,6 +40,7 @@ import AccountManagementModal from './modals/AccountManagementModal'; import BadgeManagementModal from './modals/BadgeManagementModal'; import SponsorModal from './modals/SponsorModal'; import CommitMessageModal from './modals/CommitMessageModal'; +import WorkspaceImgPathModal from './modals/WorkspaceImgPathModal'; // Providers import GooglePhotoModal from './modals/providers/GooglePhotoModal'; @@ -107,6 +108,7 @@ export default { BadgeManagementModal, SponsorModal, CommitMessageModal, + WorkspaceImgPathModal, // Providers GooglePhotoModal, GoogleDriveAccountModal, diff --git a/src/components/modals/ImageModal.vue b/src/components/modals/ImageModal.vue index 4c5324af..9772fb5c 100644 --- a/src/components/modals/ImageModal.vue +++ b/src/components/modals/ImageModal.vue @@ -15,6 +15,21 @@

添加并选择图床后可在编辑区中粘贴/拖拽图片自动上传

+ + + + + + +
+ 本文档空间图片路径 + +
+ 路径:{{path}} +
+
@@ -45,6 +60,10 @@ {{tokenStorage.uname}}, 仓库URL: {{tokenStorage.repoUrl}}, 路径: {{tokenStorage.path}}, 分支: {{tokenStorage.branch}} + + + 添加本文档空间图片路径 + 添加SM.MS图床账号 @@ -66,6 +85,7 @@ diff --git a/src/services/editorSvc.js b/src/services/editorSvc.js index c5f8789a..f11ce180 100644 --- a/src/services/editorSvc.js +++ b/src/services/editorSvc.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import DiffMatchPatch from 'diff-match-patch'; import Prism from 'prismjs'; import markdownItPandocRenderer from 'markdown-it-pandoc-renderer'; +import md5 from 'js-md5'; import cledit from './editor/cledit'; import pagedown from '../libs/pagedown'; import htmlSanitizer from '../libs/htmlSanitizer'; @@ -13,6 +14,9 @@ import editorSvcDiscussions from './editor/editorSvcDiscussions'; import editorSvcUtils from './editor/editorSvcUtils'; import utils from './utils'; import store from '../store'; +import syncSvc from './syncSvc'; +import constants from '../data/constants'; +import localDbSvc from './localDbSvc'; const allowDebounce = (action, wait) => { let timeoutId; @@ -40,6 +44,39 @@ class SectionDesc { } } +const pathUrlMap = Object.create(null); + +const getCurrAbsolutePath = () => { + const fileId = store.getters['file/current'].id; + const fileSyncData = store.getters['data/syncDataByItemId'][fileId] || { id: '' }; + const fileAbsolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${fileSyncData.id}`; + return fileAbsolutePath.substring(0, fileAbsolutePath.lastIndexOf('/')); +}; + +const getImgUrl = async (uri) => { + if (uri.indexOf('http://') !== 0 && uri.indexOf('https://') !== 0) { + const absoluteImgPath = utils.getAbsoluteFilePath(getCurrAbsolutePath(), uri); + if (pathUrlMap[absoluteImgPath]) { + return pathUrlMap[absoluteImgPath]; + } + const md5Id = md5(absoluteImgPath); + let imgItem = await localDbSvc.getImgItem(md5Id); + if (!imgItem) { + await syncSvc.syncImg(absoluteImgPath); + imgItem = await localDbSvc.getImgItem(md5Id); + } + if (imgItem) { + // imgItem 如果不存在 则加载 TODO + const imgFile = utils.base64ToBlob(imgItem.content, uri); + const url = URL.createObjectURL(imgFile); + pathUrlMap[absoluteImgPath] = url; + return url; + } + return ''; + } + return uri; +}; + // Use a vue instance as an event bus const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, { // Elements @@ -212,11 +249,18 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, this.makeTextToPreviewDiffs(); // Wait for images to load - const loadedPromises = loadingImages.map(imgElt => new Promise((resolve) => { + const loadedPromises = loadingImages.map(imgElt => new Promise((resolve, reject) => { if (!imgElt.src) { resolve(); return; } + if (imgElt.src.indexOf(constants.origin) >= 0) { + getImgUrl(imgElt.src.replace(constants.origin, '')).then((newUrl) => { + imgElt.src = newUrl; + resolve(); + }, () => reject(new Error('加载本地空间图片出错'))); + return; + } const img = new window.Image(); img.onload = resolve; img.onerror = resolve; @@ -471,6 +515,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, let imgEltsToCache = []; if (store.getters['data/computedSettings'].editor.inlineImages) { this.clEditor.highlighter.on('sectionHighlighted', (section) => { + const loadImgs = []; section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => { const srcElt = imgTokenElt.querySelector('.token.cl-src'); if (srcElt) { @@ -496,6 +541,9 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, } } imgEltsToCache.push(imgElt); + if (imgElt.src.indexOf(origin) >= 0) { + loadImgs.push(imgElt); + } } const imgTokenWrapper = document.createElement('span'); imgTokenWrapper.className = 'token img-wrapper'; @@ -504,9 +552,19 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, imgTokenWrapper.appendChild(imgTokenElt); } }); + if (loadImgs.length) { + // Wait for images to load + const loadWorkspaceImg = loadImgs.map(imgElt => new Promise((resolve, reject) => { + const uri = imgElt.src.replace(origin, ''); + getImgUrl(uri).then((newUrl) => { + imgElt.src = newUrl; + resolve(); + }, () => reject(new Error(`加载本地空间图片出错,uri:${uri}`))); + })); + Promise.all(loadWorkspaceImg).then(); + } }); } - this.clEditor.highlighter.on('highlighted', () => { imgEltsToCache.forEach((imgElt) => { const cachedImgElt = getFromImgCache(imgElt); diff --git a/src/services/imageSvc.js b/src/services/imageSvc.js index f5a6c24f..b811349f 100644 --- a/src/services/imageSvc.js +++ b/src/services/imageSvc.js @@ -1,17 +1,52 @@ +import md5 from 'js-md5'; import store from '../store'; import utils from './utils'; +import localDbSvc from './localDbSvc'; import smmsHelper from '../services/providers/helpers/smmsHelper'; import giteaHelper from '../services/providers/helpers/giteaHelper'; import githubHelper from '../services/providers/helpers/githubHelper'; import customHelper from '../services/providers/helpers/customHelper'; +function getCurrAbsolutePath() { + const fileId = store.getters['file/current'].id; + const fileSyncData = store.getters['data/syncDataByItemId'][fileId] || { id: '' }; + const fileAbsolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${fileSyncData.id}`; + return fileAbsolutePath.substring(0, fileAbsolutePath.lastIndexOf('/')); +} + +function getImagePath(confPath, imgType) { + const time = new Date(); + const date = time.getDate(); + const month = time.getMonth() + 1; + const year = time.getFullYear(); + const path = confPath.replace('{YYYY}', year) + .replace('{MM}', `0${month}`.slice(-2)).replace('{DD}', `0${date}`.slice(-2)); + return `${path}${path.endsWith('/') ? '' : '/'}${utils.uid()}.${imgType.split('/')[1]}`; +} + export default { // 上传图片 返回图片链接 // { url: 'http://xxxx', error: 'xxxxxx'} async updateImg(imgFile) { // 操作图片上传 const currStorage = store.getters['img/getCheckedStorage']; - if (!currStorage || !currStorage.provider) { + if (!currStorage) { + return { error: '暂无已选择的图床!' }; + } + // 判断是否文档空间路径 + if (currStorage.type === 'workspace') { + const path = getImagePath(currStorage.sub, imgFile.type); + // 保存到indexeddb + const base64 = await utils.encodeFiletoBase64(imgFile); + const absolutePath = utils.getAbsoluteFilePath(getCurrAbsolutePath(), path); + await localDbSvc.saveImg({ + id: md5(absolutePath), + path: absolutePath, + content: base64, + }); + return { url: path }; + } + if (!currStorage.provider) { return { error: '暂无已选择的图床!' }; } const token = store.getters[`data/${currStorage.provider}TokensBySub`][currStorage.sub]; @@ -32,13 +67,7 @@ export default { return { error: '暂无已选择的图床!' }; } const checkStorage = checkStorages[0]; - const time = new Date(); - const date = time.getDate(); - const month = time.getMonth() + 1; - const year = time.getFullYear(); - let path = checkStorage.path.replace('{YYYY}', year) - .replace('{MM}', `0${month}`.slice(-2)).replace('{DD}', `0${date}`.slice(-2)); - path = `${path}${path.endsWith('/') ? '' : '/'}${utils.uid()}.${imgFile.type.split('/')[1]}`; + const path = getImagePath(checkStorage.path, imgFile.type); if (currStorage.provider === 'gitea') { const result = await giteaHelper.uploadFile({ token, @@ -46,7 +75,7 @@ export default { branch: checkStorage.branch, path, content: imgFile, - isFile: true, + isImg: true, }); url = result.content.download_url; } else if (currStorage.provider === 'github') { @@ -57,7 +86,7 @@ export default { branch: checkStorage.branch, path, content: imgFile, - isFile: true, + isImg: true, }); url = result.content.download_url; } diff --git a/src/services/localDbSvc.js b/src/services/localDbSvc.js index 855e3f7d..11b4fc08 100644 --- a/src/services/localDbSvc.js +++ b/src/services/localDbSvc.js @@ -5,8 +5,10 @@ import workspaceSvc from './workspaceSvc'; import constants from '../data/constants'; const deleteMarkerMaxAge = 1000; -const dbVersion = 1; +const dbVersion = 3; const dbStoreName = 'objects'; +const imgDbStoreName = 'imgs'; +const imgWaitUploadIdsKey = 'waitUploadImgIds'; const { silent } = utils.queryParams; const resetApp = localStorage.getItem('resetStackEdit'); if (resetApp) { @@ -24,7 +26,7 @@ class Connection { const request = indexedDB.open(this.dbName, dbVersion); request.onerror = () => { - throw new Error("Can't connect to IndexedDB."); + throw new Error('无法连接到IndexedDB.'); }; request.onsuccess = (event) => { @@ -37,24 +39,21 @@ class Connection { request.onupgradeneeded = (event) => { const eventDb = event.target.result; - const oldVersion = event.oldVersion || 0; - - // We don't use 'break' in this switch statement, - // the fall-through behavior is what we want. - /* eslint-disable no-fallthrough */ - switch (oldVersion) { - case 0: { - // Create store - const dbStore = eventDb.createObjectStore(dbStoreName, { - keyPath: 'id', - }); - dbStore.createIndex('tx', 'tx', { - unique: false, - }); - } - default: + // const oldVersion = event.oldVersion || 0; + if (!eventDb.objectStoreNames.contains(dbStoreName)) { + // Create store + const dbStore = eventDb.createObjectStore(dbStoreName, { + keyPath: 'id', + }); + dbStore.createIndex('tx', 'tx', { + unique: false, + }); + } + if (!eventDb.objectStoreNames.contains(imgDbStoreName)) { + eventDb.createObjectStore(imgDbStoreName, { + keyPath: 'id', + }); } - /* eslint-enable no-fallthrough */ }; } @@ -193,6 +192,55 @@ const localDbSvc = { cb(storeItemMap); }; }, + async saveImg(imgItem) { + await this.writeImgItem(imgItem); + const waitUploadIdsItem = (await this.getImgItem(imgWaitUploadIdsKey)) + || { id: imgWaitUploadIdsKey, ids: [] }; + const waitUplodIds = waitUploadIdsItem.ids || []; + // 如果已上传 + if (imgItem.uploaded) { + waitUplodIds.splice(waitUplodIds.indexOf(imgItem.id), 1); + } else { + waitUplodIds.push(imgItem.id); + } + waitUploadIdsItem.ids = waitUplodIds; + await this.writeImgItem(waitUploadIdsItem); + }, + // 获取待上传的图片id + async getWaitUploadImgIds() { + const waitUploadIdsItem = (await this.getImgItem(imgWaitUploadIdsKey)) + || { id: imgWaitUploadIdsKey, ids: [] }; + return waitUploadIdsItem.ids || []; + }, + /** + * 写入图片 + */ + async writeImgItem(imgItem) { + return new Promise((resolve, reject) => { + // Create the DB transaction + this.connection.createTx((tx) => { + const dbStore = tx.objectStore(imgDbStoreName); + dbStore.put(imgItem); + resolve(); + }, () => reject(new Error('保存图片异常'))); + }); + }, + /** + * 读取图片 + */ + async getImgItem(id) { + return new Promise((resolve, reject) => { + // Get the item from DB + this.connection.createTx((tx) => { + const dbStore = tx.objectStore(imgDbStoreName); + const request = dbStore.get(id); + request.onsuccess = () => { + const dbItem = request.result; + resolve(dbItem); + }; + }, () => reject(new Error('indexeddb获取图片异常'))); + }); + }, /** * Write all changes from the store since previous transaction. diff --git a/src/services/providers/giteaWorkspaceProvider.js b/src/services/providers/giteaWorkspaceProvider.js index f97d7723..97138707 100644 --- a/src/services/providers/giteaWorkspaceProvider.js +++ b/src/services/providers/giteaWorkspaceProvider.js @@ -173,6 +173,18 @@ export default new Provider({ }, }; }, + async downloadFile({ token, path }) { + const { sha, data } = await giteaHelper.downloadFile({ + ...store.getters['workspace/currentWorkspace'], + token, + path, + isImg: true, + }); + return { + content: data, + sha, + }; + }, async downloadWorkspaceData({ token, syncData }) { if (!syncData) { return {}; @@ -200,25 +212,32 @@ export default new Provider({ file, commitMessage, }) { - const path = store.getters.gitPathsByItemId[file.id]; - const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; - const sha = gitWorkspaceSvc.shaByPath[path]; - await giteaHelper.uploadFile({ + const isImg = file.type === 'img'; + const path = store.getters.gitPathsByItemId[file.id] || ''; + const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${store.getters.gitPathsByItemId[file.id]}` : file.path; + const sha = gitWorkspaceSvc.shaByPath[!isImg ? path : file.path]; + const res = await giteaHelper.uploadFile({ ...store.getters['workspace/currentWorkspace'], token, path: absolutePath, - content: Provider.serializeContent(content), + content: !isImg ? Provider.serializeContent(content) : file.content, sha, + isImg, commitMessage, }); + if (isImg) { + return { + sha: res.content.sha, + }; + } // Return new sync data return { contentSyncData: { id: store.getters.gitPathsByItemId[content.id], type: content.type, hash: content.hash, - sha, + sha: res.content.sha, }, fileSyncData: { id: path, diff --git a/src/services/providers/giteeAppDataProvider.js b/src/services/providers/giteeAppDataProvider.js index 401e1e9f..e0f87db9 100644 --- a/src/services/providers/giteeAppDataProvider.js +++ b/src/services/providers/giteeAppDataProvider.js @@ -110,6 +110,20 @@ export default new Provider({ }, }; }, + async downloadFile({ token, path }) { + const { sha, data } = await giteeHelper.downloadFile({ + owner: token.name, + repo: appDataRepo, + branch: appDataBranch, + token, + path, + isImg: true, + }); + return { + content: data, + sha, + }; + }, async downloadWorkspaceData({ token, syncData }) { if (!syncData) { return {}; @@ -145,18 +159,25 @@ export default new Provider({ file, commitMessage, }) { - const path = store.getters.gitPathsByItemId[file.id]; + const isImg = file.type === 'img'; + const path = !isImg ? store.getters.gitPathsByItemId[file.id] : file.path; const res = await giteeHelper.uploadFile({ owner: token.name, repo: appDataRepo, branch: appDataBranch, token, path, - content: Provider.serializeContent(content), - sha: gitWorkspaceSvc.shaByPath[path], + content: !isImg ? Provider.serializeContent(content) : file.content, + sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path], + isImg, commitMessage, }); + if (isImg) { + return { + sha: res.content.sha, + }; + } // Return new sync data return { contentSyncData: { diff --git a/src/services/providers/giteeWorkspaceProvider.js b/src/services/providers/giteeWorkspaceProvider.js index 65551072..b9797ae7 100644 --- a/src/services/providers/giteeWorkspaceProvider.js +++ b/src/services/providers/giteeWorkspaceProvider.js @@ -158,6 +158,18 @@ export default new Provider({ }, }; }, + async downloadFile({ token, path }) { + const { sha, data } = await giteeHelper.downloadFile({ + ...store.getters['workspace/currentWorkspace'], + token, + path, + isImg: true, + }); + return { + content: data, + sha, + }; + }, async downloadWorkspaceData({ token, syncData }) { if (!syncData) { return {}; @@ -185,17 +197,24 @@ export default new Provider({ file, commitMessage, }) { - const path = store.getters.gitPathsByItemId[file.id]; - const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; + const isImg = file.type === 'img'; + const path = store.getters.gitPathsByItemId[file.id] || ''; + const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${path}` : file.path; const res = await giteeHelper.uploadFile({ ...store.getters['workspace/currentWorkspace'], token, path: absolutePath, - content: Provider.serializeContent(content), - sha: gitWorkspaceSvc.shaByPath[path], + content: !isImg ? Provider.serializeContent(content) : file.content, + sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path], + isImg, commitMessage, }); + if (isImg) { + return { + sha: res.content.sha, + }; + } // Return new sync data return { contentSyncData: { diff --git a/src/services/providers/githubWorkspaceProvider.js b/src/services/providers/githubWorkspaceProvider.js index 9a46822e..cc5e9387 100644 --- a/src/services/providers/githubWorkspaceProvider.js +++ b/src/services/providers/githubWorkspaceProvider.js @@ -158,6 +158,18 @@ export default new Provider({ }, }; }, + async downloadFile({ token, path }) { + const { sha, data } = await githubHelper.downloadFile({ + ...store.getters['workspace/currentWorkspace'], + token, + path, + isImg: true, + }); + return { + content: data, + sha, + }; + }, async downloadWorkspaceData({ token, syncData }) { if (!syncData) { return {}; @@ -185,17 +197,23 @@ export default new Provider({ file, commitMessage, }) { - const path = store.getters.gitPathsByItemId[file.id]; - const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; + const isImg = file.type === 'img'; + const path = store.getters.gitPathsByItemId[file.id] || ''; + const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${path}` : file.path; const res = await githubHelper.uploadFile({ ...store.getters['workspace/currentWorkspace'], token, path: absolutePath, - content: Provider.serializeContent(content), - sha: gitWorkspaceSvc.shaByPath[path], + content: !isImg ? Provider.serializeContent(content) : file.content, + sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path], + isImg, commitMessage, }); - + if (isImg) { + return { + sha: res.content.sha, + }; + } // Return new sync data return { contentSyncData: { diff --git a/src/services/providers/helpers/giteaHelper.js b/src/services/providers/helpers/giteaHelper.js index 7f84b2f9..d9def936 100644 --- a/src/services/providers/helpers/giteaHelper.js +++ b/src/services/providers/helpers/giteaHelper.js @@ -314,16 +314,20 @@ export default { path, content, sha, - isFile, + isImg, commitMessage, }) { + let uploadContent = content; + if (isImg && typeof content !== 'string') { + uploadContent = await utils.encodeFiletoBase64(content); + } const refreshedToken = await this.refreshToken(token); return request(refreshedToken, { method: sha ? 'PUT' : 'POST', url: `repos/${projectId}/contents/${encodeURIComponent(path)}`, body: { message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), - content: isFile ? await utils.encodeFiletoBase64(content) : utils.encodeBase64(content), + content: isImg ? uploadContent : utils.encodeBase64(content), sha, branch, }, @@ -360,6 +364,7 @@ export default { projectId, branch, path, + isImg, }) { const refreshedToken = await this.refreshToken(token); const { sha, content } = await request(refreshedToken, { @@ -368,7 +373,7 @@ export default { }); return { sha, - data: utils.decodeBase64(content), + data: !isImg ? utils.decodeBase64(content) : content, }; }, }; diff --git a/src/services/providers/helpers/giteeHelper.js b/src/services/providers/helpers/giteeHelper.js index f8db1c9c..a6ba57d6 100644 --- a/src/services/providers/helpers/giteeHelper.js +++ b/src/services/providers/helpers/giteeHelper.js @@ -276,15 +276,20 @@ export default { path, content, sha, + isImg, commitMessage, }) { + let uploadContent = content; + if (isImg && typeof content !== 'string') { + uploadContent = await utils.encodeFiletoBase64(content); + } const refreshedToken = await this.refreshToken(token); return repoRequest(refreshedToken, owner, repo, { method: sha ? 'PUT' : 'POST', url: `contents/${encodeURIComponent(path)}`, body: { message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), - content: utils.encodeBase64(content || ' '), + content: isImg ? uploadContent : utils.encodeBase64(content || ' '), sha, branch, }, @@ -323,6 +328,7 @@ export default { repo, branch, path, + isImg, }) { const refreshedToken = await this.refreshToken(token); const { sha, content } = await repoRequest(refreshedToken, owner, repo, { @@ -330,7 +336,7 @@ export default { params: { ref: branch }, }); if (sha) { - const data = utils.decodeBase64(content); + const data = !isImg ? utils.decodeBase64(content) : content; return { sha, data: data === ' ' ? '' : data, diff --git a/src/services/providers/helpers/githubHelper.js b/src/services/providers/helpers/githubHelper.js index ba61d406..c5285c6b 100644 --- a/src/services/providers/helpers/githubHelper.js +++ b/src/services/providers/helpers/githubHelper.js @@ -176,15 +176,19 @@ export default { path, content, sha, - isFile, + isImg, commitMessage, }) { + let uploadContent = content; + if (isImg && typeof content !== 'string') { + uploadContent = await utils.encodeFiletoBase64(content); + } return repoRequest(token, owner, repo, { method: 'PUT', url: `contents/${encodeURIComponent(path)}`, body: { message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), - content: isFile ? await utils.encodeFiletoBase64(content) : utils.encodeBase64(content), + content: isImg ? uploadContent : utils.encodeBase64(content), sha, branch, }, @@ -222,6 +226,7 @@ export default { repo, branch, path, + isImg, }) { const { sha, content } = await repoRequest(token, owner, repo, { url: `contents/${encodeURIComponent(path)}`, @@ -229,7 +234,7 @@ export default { }); return { sha, - data: utils.decodeBase64(content), + data: !isImg ? utils.decodeBase64(content) : content, }; }, /** diff --git a/src/services/syncSvc.js b/src/services/syncSvc.js index 3029851f..4f3f1ee7 100644 --- a/src/services/syncSvc.js +++ b/src/services/syncSvc.js @@ -1,3 +1,4 @@ +import md5 from 'js-md5'; import localDbSvc from './localDbSvc'; import store from '../store'; import utils from './utils'; @@ -841,6 +842,61 @@ const syncWorkspace = async (skipContents = false) => { } }; +const syncImg = async (absolutePath) => { + const token = workspaceProvider.getToken(); + const path = absolutePath.substring(1, absolutePath.length); + const { sha, content } = await workspaceProvider.downloadFile({ + token, + path, + }); + if (!sha || !content) { + return; + } + await localDbSvc.saveImg({ + id: md5(absolutePath), + path: absolutePath, + content, + uploaded: 1, + sha, + }); +}; + +const uploadImg = async (imgIds, index = 0) => { + if (imgIds.length - 1 < index) { + return; + } + const item = await localDbSvc.getImgItem(imgIds[index]); + // 不存在item 或已上传 则跳过 + if (!item || item.uploaded) { + setTimeout(await uploadImg(imgIds, index + 1), 10); + return; + } + const token = workspaceProvider.getToken(); + const { sha } = await workspaceProvider.uploadWorkspaceContent({ + token, + file: { + ...utils.deepCopy(item), + type: 'img', + path: item.path.substring(1, item.path.length), + }, + isImg: true, + }); + await localDbSvc.saveImg({ + ...item, + uploaded: 1, + sha, + }); + setTimeout(await uploadImg(imgIds, index + 1), 500); +}; + +const uploadImgs = async () => { + // 新增的图片 + const imgIds = await localDbSvc.getWaitUploadImgIds(); + if (imgIds.length > 0) { + await uploadImg(imgIds); + } +}; + /** * Enqueue a sync task, if possible. */ @@ -888,6 +944,8 @@ const requestSync = (addTriggerSyncBadge = false) => { // all the syncedContent objects. await syncFile(store.getters['file/current'].id); } + // 同步图片 + await uploadImgs(); // Clean files Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => { @@ -983,6 +1041,7 @@ export default { }, 5000); } }, + syncImg, isSyncPossible, requestSync, createSyncLocation, diff --git a/src/services/utils.js b/src/services/utils.js index 88d27d3a..60bfbf36 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -190,6 +190,19 @@ export default { reader.onerror = error => reject(error); }); }, + base64ToBlob(dataurl, fileName) { + const potIdx = fileName.lastIndexOf('.'); + const suffix = potIdx > -1 ? fileName.substring(potIdx + 1) : 'png'; + const mime = `image/${suffix}`; + const bstr = atob(dataurl); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n >= 0) { + n -= 1; + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], { type: mime }); + }, decodeBase64(str) { // In case of URL safe base64 const sanitizedStr = str.replace(/_/g, '/').replace(/-/g, '+'); @@ -370,4 +383,18 @@ export default { elt.parentNode.removeChild(elt); }); }, + // 根据当前绝对路径 与 文件路径计算出文件绝对路径 + getAbsoluteFilePath(currAbsolutePath, filePath) { + // "/"开头说明已经是绝对路径 + if (filePath.indexOf('/') === 0) { + return filePath; + } + let path = filePath; + if (filePath.indexOf('./') === 0) { + path = `${currAbsolutePath}/${path.replace('./', '')}`; + } else { + path = `${currAbsolutePath}/${path}`; + } + return path.indexOf('/') === 0 ? path : `/${path}`; + }, }; diff --git a/src/store/img.js b/src/store/img.js index 7ba4499d..fe40455a 100644 --- a/src/store/img.js +++ b/src/store/img.js @@ -1,23 +1,26 @@ -const localKey = 'img/checkedStorage'; +import utils from '../services/utils'; + +const checkStorageLocalKey = 'img/checkedStorage'; +const workspacePathLocalKey = 'img/workspaceImgPath'; export default { namespaced: true, state: { - // 来自粘贴板 或者 拖拽的图片的文件对象 - currImg: null, - // 当前图片ID + // 当前图片上传中的临时ID currImgId: null, // 选择的存储图床信息 checkedStorage: { - type: null, // 目前存储类型分两种 token 与 tokenRepo + type: 'workspace', // 目前存储类型分三种 token 与 tokenRepo 、workspace provider: null, // 对应是何种账号 - sub: null, // 对应 token 中的sub + sub: '/imgs/{YYYY}-{MM}-{DD}', // 对应 token 中的sub + sid: null, + }, + // 当前仓库图片存储位置 key 为path value 为true + workspaceImagePath: { + '/imgs/{YYYY}-{MM}-{DD}': true, }, }, mutations: { - setNewImg: (state, value) => { - state.currImg = value; - }, setCurrImgId: (state, value) => { state.currImgId = value; }, @@ -41,17 +44,28 @@ export default { }; } }, + setWorkspaceImgPath: (state, value) => { + state.workspaceImagePath = value; + localStorage.setItem(workspacePathLocalKey, JSON.stringify(state.workspaceImagePath)); + }, + addWorkspaceImgPath: (state, value) => { + state.workspaceImagePath[value] = true; + state.workspaceImagePath = utils.deepCopy(state.workspaceImagePath); + localStorage.setItem(workspacePathLocalKey, JSON.stringify(state.workspaceImagePath)); + }, + removeWorkspaceImgPath: (state, value) => { + delete state.workspaceImagePath[value]; + state.workspaceImagePath = utils.deepCopy(state.workspaceImagePath); + localStorage.setItem(workspacePathLocalKey, JSON.stringify(state.workspaceImagePath)); + }, }, getters: { - getImg: state => state.currImg, currImgId: state => state.currImgId, getCheckedStorage: state => state.checkedStorage, getCheckedStorageSub: state => state.checkedStorage.sub, + getWorkspaceImgPath: state => state.workspaceImagePath, }, actions: { - setImg({ commit }, img) { - commit('setNewImg', img); - }, setCurrImgId({ commit }, imgId) { commit('setCurrImgId', imgId); }, @@ -60,7 +74,16 @@ export default { }, changeCheckedStorage({ commit }, checkedStorage) { commit('changeCheckedStorage', checkedStorage); - localStorage.setItem(localKey, JSON.stringify(checkedStorage)); + localStorage.setItem(checkStorageLocalKey, JSON.stringify(checkedStorage)); + }, + setWorkspaceImgPath({ commit }, workspaceImgPath) { + commit('setWorkspaceImgPath', workspaceImgPath); + }, + addWorkspaceImgPath({ commit }, workspaceImgPathValue) { + commit('addWorkspaceImgPath', workspaceImgPathValue); + }, + removeWorkspaceImgPath({ commit }, workspaceImgPathValue) { + commit('removeWorkspaceImgPath', workspaceImgPathValue); }, }, };