图片支持相对本地空间的路径存储

This commit is contained in:
xiaoqi.cxq 2022-10-29 15:46:57 +08:00
parent e7450df251
commit a4ab4b2da1
20 changed files with 504 additions and 78 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "StackEdit中文版", "name": "StackEdit中文版",
"description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器", "description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器",
"version": "5.15.14", "version": "5.15.15",
"manifest_version": 2, "manifest_version": 2,
"container" : "GITEE", "container" : "GITEE",
"api_console_project_id" : "241271498917", "api_console_project_id" : "241271498917",

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "stackedit", "name": "stackedit",
"version": "5.15.14", "version": "5.15.15",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "stackedit", "name": "stackedit",
"version": "5.15.14", "version": "5.15.15",
"description": "免费, 开源, 功能齐全的 Markdown 编辑器", "description": "免费, 开源, 功能齐全的 Markdown 编辑器",
"author": "Benoit Schweblin, 豆萁", "author": "Benoit Schweblin, 豆萁",
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@ -34,7 +34,7 @@ export default {
]), ]),
}, },
methods: { methods: {
async setImgAndDoClick(items) { async processUpload(items) {
let file = null; let file = null;
if (!items || items.length === 0) { if (!items || items.length === 0) {
return; return;
@ -73,6 +73,11 @@ export default {
if (currImgStorageStr) { if (currImgStorageStr) {
store.commit('img/changeCheckedStorage', JSON.parse(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 editorElt = this.$el.querySelector('.editor__inner');
const onDiscussionEvt = cb => (evt) => { const onDiscussionEvt = cb => (evt) => {
let elt = evt.target; let elt = evt.target;
@ -100,11 +105,11 @@ export default {
editorElt.addEventListener('drop', (event) => { editorElt.addEventListener('drop', (event) => {
const transItems = event.dataTransfer.items; const transItems = event.dataTransfer.items;
this.setImgAndDoClick(transItems); this.processUpload(transItems);
}); });
editorElt.addEventListener('paste', (event) => { editorElt.addEventListener('paste', (event) => {
const pasteItems = (event.clipboardData || window.clipboardData).items; const pasteItems = (event.clipboardData || window.clipboardData).items;
this.setImgAndDoClick(pasteItems); this.processUpload(pasteItems);
}); });
this.$watch( this.$watch(

View File

@ -40,6 +40,7 @@ import AccountManagementModal from './modals/AccountManagementModal';
import BadgeManagementModal from './modals/BadgeManagementModal'; import BadgeManagementModal from './modals/BadgeManagementModal';
import SponsorModal from './modals/SponsorModal'; import SponsorModal from './modals/SponsorModal';
import CommitMessageModal from './modals/CommitMessageModal'; import CommitMessageModal from './modals/CommitMessageModal';
import WorkspaceImgPathModal from './modals/WorkspaceImgPathModal';
// Providers // Providers
import GooglePhotoModal from './modals/providers/GooglePhotoModal'; import GooglePhotoModal from './modals/providers/GooglePhotoModal';
@ -107,6 +108,7 @@ export default {
BadgeManagementModal, BadgeManagementModal,
SponsorModal, SponsorModal,
CommitMessageModal, CommitMessageModal,
WorkspaceImgPathModal,
// Providers // Providers
GooglePhotoModal, GooglePhotoModal,
GoogleDriveAccountModal, GoogleDriveAccountModal,

View File

@ -15,6 +15,21 @@
<div> <div>
<hr /> <hr />
<p>添加并选择图床后可在编辑区中粘贴/拖拽图片自动上传</p> <p>添加并选择图床后可在编辑区中粘贴/拖拽图片自动上传</p>
<menu-entry @click.native="checkedImgDest(path)" v-for="path in workspaceImgPath" :key="path">
<icon-check-circle v-if="checkedStorage.sub === path" slot="icon"></icon-check-circle>
<icon-check-circle-un v-if="checkedStorage.sub !== path" slot="icon"></icon-check-circle-un>
<menu-item>
<icon-provider slot="icon" :provider-id="currentWorkspace.providerId"></icon-provider>
<div>
本文档空间图片路径
<button class="menu-item__button button" @click.stop="removeByPath(path)" v-title="'删除'">
<icon-delete></icon-delete>
</button>
</div>
<span>路径{{path}}</span>
</menu-item>
</menu-entry>
<menu-entry @click.native="checkedImgDest(token.sub, token.providerId)" v-for="token in imageTokens" :key="token.sub"> <menu-entry @click.native="checkedImgDest(token.sub, token.providerId)" v-for="token in imageTokens" :key="token.sub">
<icon-check-circle v-if="checkedStorage.sub === token.sub" slot="icon"></icon-check-circle> <icon-check-circle v-if="checkedStorage.sub === token.sub" slot="icon"></icon-check-circle>
<icon-check-circle-un v-if="checkedStorage.sub !== token.sub" slot="icon"></icon-check-circle-un> <icon-check-circle-un v-if="checkedStorage.sub !== token.sub" slot="icon"></icon-check-circle-un>
@ -45,6 +60,10 @@
<span> {{tokenStorage.uname}}, 仓库URL: {{tokenStorage.repoUrl}}, 路径: {{tokenStorage.path}}, 分支: {{tokenStorage.branch}}</span> <span> {{tokenStorage.uname}}, 仓库URL: {{tokenStorage.repoUrl}}, 路径: {{tokenStorage.path}}, 分支: {{tokenStorage.branch}}</span>
</menu-item> </menu-item>
</menu-entry> </menu-entry>
<menu-entry @click.native="addWorkspaceImgPath">
<icon-provider slot="icon" :provider-id="currentWorkspace.providerId"></icon-provider>
<span>添加本文档空间图片路径</span>
</menu-entry>
<menu-entry @click.native="addSmmsAccount"> <menu-entry @click.native="addSmmsAccount">
<icon-provider slot="icon" provider-id="smms"></icon-provider> <icon-provider slot="icon" provider-id="smms"></icon-provider>
<span>添加SM.MS图床账号</span> <span>添加SM.MS图床账号</span>
@ -66,6 +85,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import MenuEntry from '../menus/common/MenuEntry'; import MenuEntry from '../menus/common/MenuEntry';
import MenuItem from '../menus/common/MenuItem'; import MenuItem from '../menus/common/MenuItem';
@ -87,9 +107,16 @@ export default modalTemplate({
url: '', url: '',
}), }),
computed: { computed: {
...mapGetters('workspace', [
'currentWorkspace',
]),
checkedStorage() { checkedStorage() {
return store.getters['img/getCheckedStorage']; return store.getters['img/getCheckedStorage'];
}, },
workspaceImgPath() {
const workspaceImgPath = store.getters['img/getWorkspaceImgPath'];
return Object.keys(workspaceImgPath || {});
},
imageTokens() { imageTokens() {
return [ return [
...Object.values(store.getters['data/smmsTokensBySub']).map(token => ({ ...Object.values(store.getters['data/smmsTokensBySub']).map(token => ({
@ -195,6 +222,13 @@ export default modalTemplate({
// Cancel // Cancel
} }
}, },
async removeByPath(path) {
store.dispatch('img/removeWorkspaceImgPath', path);
},
async addWorkspaceImgPath() {
const { path } = await store.dispatch('modal/open', { type: 'workspaceImgPath' });
store.dispatch('img/addWorkspaceImgPath', path);
},
async addSmmsAccount() { async addSmmsAccount() {
const { proxyUrl, apiSecretToken } = await store.dispatch('modal/open', { type: 'smmsAccount' }); const { proxyUrl, apiSecretToken } = await store.dispatch('modal/open', { type: 'smmsAccount' });
await smmsHelper.addAccount(proxyUrl, apiSecretToken); await smmsHelper.addAccount(proxyUrl, apiSecretToken);
@ -227,7 +261,10 @@ export default modalTemplate({
}, },
async checkedImgDest(sub, provider, sid) { async checkedImgDest(sub, provider, sid) {
let type = 'token'; let type = 'token';
if (provider === 'gitea' || provider === 'github') { //
if (!provider) {
type = 'workspace';
} else if (provider === 'gitea' || provider === 'github') {
type = 'tokenRepo'; type = 'tokenRepo';
} }
store.dispatch('img/changeCheckedStorage', { store.dispatch('img/changeCheckedStorage', {

View File

@ -0,0 +1,45 @@
<template>
<modal-inner aria-label="文档空间图片路径">
<div class="modal__content">
<div class="modal__image">
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
</div>
<p>在当前文档空间增加图片上传路径</p>
<form-entry label="图片上传路径" error="path">
<input slot="field" class="textfield" type="text" placeholder="如:/imgs/{YYYY}-{MM}-{DD}" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
如果不提供默认为 /imgs/{YYYY}-{MM}-{DD} 其中{YYYY}为年变量{MM}为月变量{DD}为日变量<br/>
支持相对路径 ./imgs imgs 都是相对当前编辑中文档的路径不支持相对上级路径
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">取消</button>
<button class="button button--resolve" @click="resolve()">确认</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import modalTemplate from './common/modalTemplate';
export default modalTemplate({
computedLocalSettings: {
path: '',
},
computed: {
...mapGetters('workspace', [
'currentWorkspace',
]),
},
methods: {
resolve() {
const path = this.path && this.path.replace(/^\//, '');
this.config.resolve({
path: path || '/imgs/{YYYY}-{MM}-{DD}',
});
},
},
});
</script>

View File

@ -2,6 +2,7 @@ import Vue from 'vue';
import DiffMatchPatch from 'diff-match-patch'; import DiffMatchPatch from 'diff-match-patch';
import Prism from 'prismjs'; import Prism from 'prismjs';
import markdownItPandocRenderer from 'markdown-it-pandoc-renderer'; import markdownItPandocRenderer from 'markdown-it-pandoc-renderer';
import md5 from 'js-md5';
import cledit from './editor/cledit'; import cledit from './editor/cledit';
import pagedown from '../libs/pagedown'; import pagedown from '../libs/pagedown';
import htmlSanitizer from '../libs/htmlSanitizer'; import htmlSanitizer from '../libs/htmlSanitizer';
@ -13,6 +14,9 @@ import editorSvcDiscussions from './editor/editorSvcDiscussions';
import editorSvcUtils from './editor/editorSvcUtils'; import editorSvcUtils from './editor/editorSvcUtils';
import utils from './utils'; import utils from './utils';
import store from '../store'; import store from '../store';
import syncSvc from './syncSvc';
import constants from '../data/constants';
import localDbSvc from './localDbSvc';
const allowDebounce = (action, wait) => { const allowDebounce = (action, wait) => {
let timeoutId; 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 // Use a vue instance as an event bus
const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, { const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, {
// Elements // Elements
@ -212,11 +249,18 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
this.makeTextToPreviewDiffs(); this.makeTextToPreviewDiffs();
// Wait for images to load // 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) { if (!imgElt.src) {
resolve(); resolve();
return; 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(); const img = new window.Image();
img.onload = resolve; img.onload = resolve;
img.onerror = resolve; img.onerror = resolve;
@ -471,6 +515,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
let imgEltsToCache = []; let imgEltsToCache = [];
if (store.getters['data/computedSettings'].editor.inlineImages) { if (store.getters['data/computedSettings'].editor.inlineImages) {
this.clEditor.highlighter.on('sectionHighlighted', (section) => { this.clEditor.highlighter.on('sectionHighlighted', (section) => {
const loadImgs = [];
section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => { section.elt.getElementsByClassName('token img').cl_each((imgTokenElt) => {
const srcElt = imgTokenElt.querySelector('.token.cl-src'); const srcElt = imgTokenElt.querySelector('.token.cl-src');
if (srcElt) { if (srcElt) {
@ -496,6 +541,9 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
} }
} }
imgEltsToCache.push(imgElt); imgEltsToCache.push(imgElt);
if (imgElt.src.indexOf(origin) >= 0) {
loadImgs.push(imgElt);
}
} }
const imgTokenWrapper = document.createElement('span'); const imgTokenWrapper = document.createElement('span');
imgTokenWrapper.className = 'token img-wrapper'; imgTokenWrapper.className = 'token img-wrapper';
@ -504,9 +552,19 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
imgTokenWrapper.appendChild(imgTokenElt); 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', () => { this.clEditor.highlighter.on('highlighted', () => {
imgEltsToCache.forEach((imgElt) => { imgEltsToCache.forEach((imgElt) => {
const cachedImgElt = getFromImgCache(imgElt); const cachedImgElt = getFromImgCache(imgElt);

View File

@ -1,17 +1,52 @@
import md5 from 'js-md5';
import store from '../store'; import store from '../store';
import utils from './utils'; import utils from './utils';
import localDbSvc from './localDbSvc';
import smmsHelper from '../services/providers/helpers/smmsHelper'; import smmsHelper from '../services/providers/helpers/smmsHelper';
import giteaHelper from '../services/providers/helpers/giteaHelper'; import giteaHelper from '../services/providers/helpers/giteaHelper';
import githubHelper from '../services/providers/helpers/githubHelper'; import githubHelper from '../services/providers/helpers/githubHelper';
import customHelper from '../services/providers/helpers/customHelper'; 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 { export default {
// 上传图片 返回图片链接 // 上传图片 返回图片链接
// { url: 'http://xxxx', error: 'xxxxxx'} // { url: 'http://xxxx', error: 'xxxxxx'}
async updateImg(imgFile) { async updateImg(imgFile) {
// 操作图片上传 // 操作图片上传
const currStorage = store.getters['img/getCheckedStorage']; 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: '暂无已选择的图床!' }; return { error: '暂无已选择的图床!' };
} }
const token = store.getters[`data/${currStorage.provider}TokensBySub`][currStorage.sub]; const token = store.getters[`data/${currStorage.provider}TokensBySub`][currStorage.sub];
@ -32,13 +67,7 @@ export default {
return { error: '暂无已选择的图床!' }; return { error: '暂无已选择的图床!' };
} }
const checkStorage = checkStorages[0]; const checkStorage = checkStorages[0];
const time = new Date(); const path = getImagePath(checkStorage.path, imgFile.type);
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]}`;
if (currStorage.provider === 'gitea') { if (currStorage.provider === 'gitea') {
const result = await giteaHelper.uploadFile({ const result = await giteaHelper.uploadFile({
token, token,
@ -46,7 +75,7 @@ export default {
branch: checkStorage.branch, branch: checkStorage.branch,
path, path,
content: imgFile, content: imgFile,
isFile: true, isImg: true,
}); });
url = result.content.download_url; url = result.content.download_url;
} else if (currStorage.provider === 'github') { } else if (currStorage.provider === 'github') {
@ -57,7 +86,7 @@ export default {
branch: checkStorage.branch, branch: checkStorage.branch,
path, path,
content: imgFile, content: imgFile,
isFile: true, isImg: true,
}); });
url = result.content.download_url; url = result.content.download_url;
} }

View File

@ -5,8 +5,10 @@ import workspaceSvc from './workspaceSvc';
import constants from '../data/constants'; import constants from '../data/constants';
const deleteMarkerMaxAge = 1000; const deleteMarkerMaxAge = 1000;
const dbVersion = 1; const dbVersion = 3;
const dbStoreName = 'objects'; const dbStoreName = 'objects';
const imgDbStoreName = 'imgs';
const imgWaitUploadIdsKey = 'waitUploadImgIds';
const { silent } = utils.queryParams; const { silent } = utils.queryParams;
const resetApp = localStorage.getItem('resetStackEdit'); const resetApp = localStorage.getItem('resetStackEdit');
if (resetApp) { if (resetApp) {
@ -24,7 +26,7 @@ class Connection {
const request = indexedDB.open(this.dbName, dbVersion); const request = indexedDB.open(this.dbName, dbVersion);
request.onerror = () => { request.onerror = () => {
throw new Error("Can't connect to IndexedDB."); throw new Error('无法连接到IndexedDB.');
}; };
request.onsuccess = (event) => { request.onsuccess = (event) => {
@ -37,13 +39,8 @@ class Connection {
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
const eventDb = event.target.result; const eventDb = event.target.result;
const oldVersion = event.oldVersion || 0; // const oldVersion = event.oldVersion || 0;
if (!eventDb.objectStoreNames.contains(dbStoreName)) {
// 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 // Create store
const dbStore = eventDb.createObjectStore(dbStoreName, { const dbStore = eventDb.createObjectStore(dbStoreName, {
keyPath: 'id', keyPath: 'id',
@ -52,9 +49,11 @@ class Connection {
unique: false, unique: false,
}); });
} }
default: if (!eventDb.objectStoreNames.contains(imgDbStoreName)) {
eventDb.createObjectStore(imgDbStoreName, {
keyPath: 'id',
});
} }
/* eslint-enable no-fallthrough */
}; };
} }
@ -193,6 +192,55 @@ const localDbSvc = {
cb(storeItemMap); 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. * Write all changes from the store since previous transaction.

View File

@ -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 }) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
@ -200,25 +212,32 @@ export default new Provider({
file, file,
commitMessage, commitMessage,
}) { }) {
const path = store.getters.gitPathsByItemId[file.id]; const isImg = file.type === 'img';
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; const path = store.getters.gitPathsByItemId[file.id] || '';
const sha = gitWorkspaceSvc.shaByPath[path]; const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${store.getters.gitPathsByItemId[file.id]}` : file.path;
await giteaHelper.uploadFile({ const sha = gitWorkspaceSvc.shaByPath[!isImg ? path : file.path];
const res = await giteaHelper.uploadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(content), content: !isImg ? Provider.serializeContent(content) : file.content,
sha, sha,
isImg,
commitMessage, commitMessage,
}); });
if (isImg) {
return {
sha: res.content.sha,
};
}
// Return new sync data // Return new sync data
return { return {
contentSyncData: { contentSyncData: {
id: store.getters.gitPathsByItemId[content.id], id: store.getters.gitPathsByItemId[content.id],
type: content.type, type: content.type,
hash: content.hash, hash: content.hash,
sha, sha: res.content.sha,
}, },
fileSyncData: { fileSyncData: {
id: path, id: path,

View File

@ -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 }) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
@ -145,18 +159,25 @@ export default new Provider({
file, file,
commitMessage, 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({ const res = await giteeHelper.uploadFile({
owner: token.name, owner: token.name,
repo: appDataRepo, repo: appDataRepo,
branch: appDataBranch, branch: appDataBranch,
token, token,
path, path,
content: Provider.serializeContent(content), content: !isImg ? Provider.serializeContent(content) : file.content,
sha: gitWorkspaceSvc.shaByPath[path], sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path],
isImg,
commitMessage, commitMessage,
}); });
if (isImg) {
return {
sha: res.content.sha,
};
}
// Return new sync data // Return new sync data
return { return {
contentSyncData: { contentSyncData: {

View File

@ -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 }) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
@ -185,17 +197,24 @@ export default new Provider({
file, file,
commitMessage, commitMessage,
}) { }) {
const path = store.getters.gitPathsByItemId[file.id]; const isImg = file.type === 'img';
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; const path = store.getters.gitPathsByItemId[file.id] || '';
const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${path}` : file.path;
const res = await giteeHelper.uploadFile({ const res = await giteeHelper.uploadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(content), content: !isImg ? Provider.serializeContent(content) : file.content,
sha: gitWorkspaceSvc.shaByPath[path], sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path],
isImg,
commitMessage, commitMessage,
}); });
if (isImg) {
return {
sha: res.content.sha,
};
}
// Return new sync data // Return new sync data
return { return {
contentSyncData: { contentSyncData: {

View File

@ -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 }) { async downloadWorkspaceData({ token, syncData }) {
if (!syncData) { if (!syncData) {
return {}; return {};
@ -185,17 +197,23 @@ export default new Provider({
file, file,
commitMessage, commitMessage,
}) { }) {
const path = store.getters.gitPathsByItemId[file.id]; const isImg = file.type === 'img';
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; const path = store.getters.gitPathsByItemId[file.id] || '';
const absolutePath = !isImg ? `${store.getters['workspace/currentWorkspace'].path || ''}${path}` : file.path;
const res = await githubHelper.uploadFile({ const res = await githubHelper.uploadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(content), content: !isImg ? Provider.serializeContent(content) : file.content,
sha: gitWorkspaceSvc.shaByPath[path], sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path],
isImg,
commitMessage, commitMessage,
}); });
if (isImg) {
return {
sha: res.content.sha,
};
}
// Return new sync data // Return new sync data
return { return {
contentSyncData: { contentSyncData: {

View File

@ -314,16 +314,20 @@ export default {
path, path,
content, content,
sha, sha,
isFile, isImg,
commitMessage, commitMessage,
}) { }) {
let uploadContent = content;
if (isImg && typeof content !== 'string') {
uploadContent = await utils.encodeFiletoBase64(content);
}
const refreshedToken = await this.refreshToken(token); const refreshedToken = await this.refreshToken(token);
return request(refreshedToken, { return request(refreshedToken, {
method: sha ? 'PUT' : 'POST', method: sha ? 'PUT' : 'POST',
url: `repos/${projectId}/contents/${encodeURIComponent(path)}`, url: `repos/${projectId}/contents/${encodeURIComponent(path)}`,
body: { body: {
message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content: isFile ? await utils.encodeFiletoBase64(content) : utils.encodeBase64(content), content: isImg ? uploadContent : utils.encodeBase64(content),
sha, sha,
branch, branch,
}, },
@ -360,6 +364,7 @@ export default {
projectId, projectId,
branch, branch,
path, path,
isImg,
}) { }) {
const refreshedToken = await this.refreshToken(token); const refreshedToken = await this.refreshToken(token);
const { sha, content } = await request(refreshedToken, { const { sha, content } = await request(refreshedToken, {
@ -368,7 +373,7 @@ export default {
}); });
return { return {
sha, sha,
data: utils.decodeBase64(content), data: !isImg ? utils.decodeBase64(content) : content,
}; };
}, },
}; };

View File

@ -276,15 +276,20 @@ export default {
path, path,
content, content,
sha, sha,
isImg,
commitMessage, commitMessage,
}) { }) {
let uploadContent = content;
if (isImg && typeof content !== 'string') {
uploadContent = await utils.encodeFiletoBase64(content);
}
const refreshedToken = await this.refreshToken(token); const refreshedToken = await this.refreshToken(token);
return repoRequest(refreshedToken, owner, repo, { return repoRequest(refreshedToken, owner, repo, {
method: sha ? 'PUT' : 'POST', method: sha ? 'PUT' : 'POST',
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
body: { body: {
message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content: utils.encodeBase64(content || ' '), content: isImg ? uploadContent : utils.encodeBase64(content || ' '),
sha, sha,
branch, branch,
}, },
@ -323,6 +328,7 @@ export default {
repo, repo,
branch, branch,
path, path,
isImg,
}) { }) {
const refreshedToken = await this.refreshToken(token); const refreshedToken = await this.refreshToken(token);
const { sha, content } = await repoRequest(refreshedToken, owner, repo, { const { sha, content } = await repoRequest(refreshedToken, owner, repo, {
@ -330,7 +336,7 @@ export default {
params: { ref: branch }, params: { ref: branch },
}); });
if (sha) { if (sha) {
const data = utils.decodeBase64(content); const data = !isImg ? utils.decodeBase64(content) : content;
return { return {
sha, sha,
data: data === ' ' ? '' : data, data: data === ' ' ? '' : data,

View File

@ -176,15 +176,19 @@ export default {
path, path,
content, content,
sha, sha,
isFile, isImg,
commitMessage, commitMessage,
}) { }) {
let uploadContent = content;
if (isImg && typeof content !== 'string') {
uploadContent = await utils.encodeFiletoBase64(content);
}
return repoRequest(token, owner, repo, { return repoRequest(token, owner, repo, {
method: 'PUT', method: 'PUT',
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
body: { body: {
message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path), message: commitMessage || getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content: isFile ? await utils.encodeFiletoBase64(content) : utils.encodeBase64(content), content: isImg ? uploadContent : utils.encodeBase64(content),
sha, sha,
branch, branch,
}, },
@ -222,6 +226,7 @@ export default {
repo, repo,
branch, branch,
path, path,
isImg,
}) { }) {
const { sha, content } = await repoRequest(token, owner, repo, { const { sha, content } = await repoRequest(token, owner, repo, {
url: `contents/${encodeURIComponent(path)}`, url: `contents/${encodeURIComponent(path)}`,
@ -229,7 +234,7 @@ export default {
}); });
return { return {
sha, sha,
data: utils.decodeBase64(content), data: !isImg ? utils.decodeBase64(content) : content,
}; };
}, },
/** /**

View File

@ -1,3 +1,4 @@
import md5 from 'js-md5';
import localDbSvc from './localDbSvc'; import localDbSvc from './localDbSvc';
import store from '../store'; import store from '../store';
import utils from './utils'; 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. * Enqueue a sync task, if possible.
*/ */
@ -888,6 +944,8 @@ const requestSync = (addTriggerSyncBadge = false) => {
// all the syncedContent objects. // all the syncedContent objects.
await syncFile(store.getters['file/current'].id); await syncFile(store.getters['file/current'].id);
} }
// 同步图片
await uploadImgs();
// Clean files // Clean files
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => { Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
@ -983,6 +1041,7 @@ export default {
}, 5000); }, 5000);
} }
}, },
syncImg,
isSyncPossible, isSyncPossible,
requestSync, requestSync,
createSyncLocation, createSyncLocation,

View File

@ -190,6 +190,19 @@ export default {
reader.onerror = error => reject(error); 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) { decodeBase64(str) {
// In case of URL safe base64 // In case of URL safe base64
const sanitizedStr = str.replace(/_/g, '/').replace(/-/g, '+'); const sanitizedStr = str.replace(/_/g, '/').replace(/-/g, '+');
@ -370,4 +383,18 @@ export default {
elt.parentNode.removeChild(elt); 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}`;
},
}; };

View File

@ -1,23 +1,26 @@
const localKey = 'img/checkedStorage'; import utils from '../services/utils';
const checkStorageLocalKey = 'img/checkedStorage';
const workspacePathLocalKey = 'img/workspaceImgPath';
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
// 来自粘贴板 或者 拖拽的图片的文件对象 // 当前图片上传中的临时ID
currImg: null,
// 当前图片ID
currImgId: null, currImgId: null,
// 选择的存储图床信息 // 选择的存储图床信息
checkedStorage: { checkedStorage: {
type: null, // 目前存储类型分两种 token 与 tokenRepo type: 'workspace', // 目前存储类型分三种 token 与 tokenRepo 、workspace
provider: null, // 对应是何种账号 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: { mutations: {
setNewImg: (state, value) => {
state.currImg = value;
},
setCurrImgId: (state, value) => { setCurrImgId: (state, value) => {
state.currImgId = 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: { getters: {
getImg: state => state.currImg,
currImgId: state => state.currImgId, currImgId: state => state.currImgId,
getCheckedStorage: state => state.checkedStorage, getCheckedStorage: state => state.checkedStorage,
getCheckedStorageSub: state => state.checkedStorage.sub, getCheckedStorageSub: state => state.checkedStorage.sub,
getWorkspaceImgPath: state => state.workspaceImagePath,
}, },
actions: { actions: {
setImg({ commit }, img) {
commit('setNewImg', img);
},
setCurrImgId({ commit }, imgId) { setCurrImgId({ commit }, imgId) {
commit('setCurrImgId', imgId); commit('setCurrImgId', imgId);
}, },
@ -60,7 +74,16 @@ export default {
}, },
changeCheckedStorage({ commit }, checkedStorage) { changeCheckedStorage({ commit }, checkedStorage) {
commit('changeCheckedStorage', 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);
}, },
}, },
}; };