支持自定义图床

This commit is contained in:
xiaoqi.cxq 2022-07-10 11:24:29 +08:00
parent cc6c8ff5ab
commit cb1354516f
13 changed files with 305 additions and 20 deletions

6
package-lock.json generated
View File

@ -11267,6 +11267,12 @@
"nopt": "~3.0.1"
}
},
"js-md5": {
"version": "0.7.3",
"resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.7.3.tgz",
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==",
"dev": true
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",

View File

@ -100,6 +100,7 @@
"jest": "^23.0.0",
"jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^0.3.0",
"js-md5": "^0.7.3",
"node-sass": "^4.0.0",
"npm-bump": "^0.0.23",
"offline-plugin": "^5.0.3",

View File

@ -0,0 +1,3 @@
<svg t="1657361174041" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4922" width="32" height="32">
<path d="M259.072 303.104q30.72 0 52.736 22.016t22.016 53.76q0 30.72-22.016 52.736t-52.736 22.016q-31.744 0-53.248-22.016t-21.504-52.736q0-31.744 21.504-53.76t53.248-22.016zM864.256 57.344q43.008 0 69.12 28.672t26.112 65.536l0 550.912q0 23.552-16.896 39.936t-40.448 16.384l-70.656 0 0-123.904 44.032 0q11.264 0 19.456-8.192t8.192-20.48q0-11.264-8.192-19.456t-19.456-8.192l-44.032 0 0-79.872 44.032 0q11.264 0 19.456-8.192t8.192-19.456-8.192-19.968-19.456-8.704l-44.032 0 0-72.704 44.032 0q11.264 0 19.456-8.192t8.192-20.48q0-11.264-8.192-19.456t-19.456-8.192l-44.032 0 0-86.016q0-57.344-26.624-80.896t-90.112-23.552l-394.24 0 0-9.216q0-23.552 16.896-39.936t40.448-16.384l486.4 0zM692.224 184.32q39.936 0 57.856 23.04t17.92 59.904l0 565.248q0 23.552-19.456 43.52t-48.128 19.968l-572.416 0q-24.576 0-44.032-20.48t-19.456-48.128l0-575.488q0-29.696 16.384-48.64t43.008-18.944l568.32 0zM703.488 291.84q0-17.408-10.752-30.208t-34.304-12.8l-488.448 0q-4.096 0-11.264 1.536t-14.336 5.12-12.288 9.728-5.12 15.36l0 274.432q8.192 9.216 23.04 22.016t34.816 23.552 44.544 18.432 53.248 7.68q43.008 0 75.264-13.824t59.904-34.816 54.272-45.056 58.88-45.568 73.728-36.352 98.816-16.896l0-142.336z" p-id="4923" fill="#1296db"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -80,6 +80,7 @@ import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';
import SmmsAccountModal from './modals/providers/SmmsAccountModal';
import CustomAccountModal from './modals/providers/CustomAccountModal';
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield, input[type=checkbox]')
// Filter enabled and visible element
@ -143,6 +144,7 @@ export default {
CouchdbWorkspaceModal,
CouchdbCredentialsModal,
SmmsAccountModal,
CustomAccountModal,
},
computed: {
...mapGetters([

View File

@ -4,8 +4,8 @@
<div class="modal__image">
<icon-key></icon-key>
</div>
<p v-if="entries.length">Stackedit可以访问以下外部账号</p>
<p v-else>Stackedit尚未访问任何外部账号</p>
<p v-if="entries.length">StackEdit中文版可以访问以下外部账号</p>
<p v-else>StackEdit中文版尚未访问任何外部账号</p>
<div>
<div class="account-entry flex flex--column" v-for="entry in entries" :key="entry.token.sub">
<div class="account-entry__header flex flex--row flex--align-center">
@ -30,6 +30,14 @@
<b>URL:</b>
{{entry.url}}
</span>
<span class="account-entry__field line-entry" v-if="entry.customHeaders">
<b>自定义请求头:</b>
{{entry.customHeaders}}
</span>
<span class="account-entry__field line-entry" v-if="entry.customParams">
<b>自定义Form参数:</b>
{{entry.customParams}}
</span>
<span class="account-entry__field" v-if="entry.scopes">
<b>权限范围:</b>
{{entry.scopes.join(', ')}}
@ -81,6 +89,10 @@
<icon-provider slot="icon" provider-id="smms"></icon-provider>
<span>添加SM.MS账号</span>
</menu-entry>
<menu-entry @click.native="addCustomAccount">
<icon-provider slot="icon" provider-id="custom"></icon-provider>
<span>添加自定义图床账号</span>
</menu-entry>
</div>
<div class="modal__button-bar">
<button class="button button--resolve" @click="config.resolve()">关闭</button>
@ -103,6 +115,7 @@ import giteaHelper from '../../services/providers/helpers/giteaHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import smmsHelper from '../../services/providers/helpers/smmsHelper';
import customHelper from '../../services/providers/helpers/customHelper';
import badgeSvc from '../../services/badgeSvc';
export default {
@ -188,6 +201,16 @@ export default {
name: token.name,
scopes: ['api'],
})),
...Object.values(store.getters['data/customTokensBySub']).map(token => ({
token,
providerId: 'custom',
url: token.uploadUrl,
userId: token.name,
name: token.name,
customHeaders: token.customHeaders && JSON.stringify(token.customHeaders),
customParams: token.customParams && JSON.stringify(token.customParams),
scopes: ['upload'],
})),
];
},
},
@ -263,6 +286,12 @@ export default {
await smmsHelper.addAccount(proxyUrl, apiSecretToken);
} catch (e) { /* cancel */ }
},
async addCustomAccount() {
try {
const accountInfo = await store.dispatch('modal/open', { type: 'customAccount' });
await customHelper.addAccount(accountInfo);
} catch (e) { /* cancel */ }
},
},
};
</script>
@ -270,6 +299,18 @@ export default {
<style lang="scss">
@import '../../styles/variables.scss';
.line-entry {
word-break: break-word; /* 文本行的任意字内断开,就算是一个单词也会分开 */
word-wrap: break-word; /* IE */
white-space: -moz-pre-wrap; /* Mozilla */
white-space: -hp-pre-wrap; /* HP printers */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: pre; /* CSS2 */
white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
}
.account-entry {
margin: 1.5em 0;
height: auto;

View File

@ -13,18 +13,21 @@
<input slot="field" class="textfield" type="text" v-model.trim="url" @keydown.enter="resolve">
</form-entry>
<p>添加并选择图床后可实现粘贴/拖拽自动上传图片</p>
<menu-entry @click.native="checkedImgDest(token.sub, 'smms')" v-for="token in smmsTokens" :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-un v-if="checkedStorage.sub !== token.sub" slot="icon"></icon-check-circle-un>
<menu-item>
<icon-provider slot="icon" provider-id="smms"></icon-provider>
<icon-provider slot="icon" :provider-id="token.providerId"></icon-provider>
<div>
SM.MS图床
<button class="menu-item__button button" @click.stop="remove('smms', token)" v-title="'删除'">
{{ token.remark }}
<button class="menu-item__button button" @click.stop="remove(token.providerId, token)" v-title="'删除'">
<icon-delete></icon-delete>
</button>
</div>
<span>{{token.name}}</span>
<span class="line-entry" v-if="token.uploadUrl">上传地址{{token.uploadUrl}}</span>
<span class="line-entry" v-if="token.headers">自定义请求头{{token.headers}}</span>
<span class="line-entry" v-if="token.params">自定义Form参数{{token.params}}</span>
</menu-item>
</menu-entry>
<menu-entry @click.native="checkedImgDest(tokenStorage.sid, 'gitea')" v-for="tokenStorage in giteaTokensImgStorages" :key="tokenStorage.sid">
@ -44,6 +47,10 @@
<icon-provider slot="icon" provider-id="smms"></icon-provider>
<span>添加SM.MS图床账号</span>
</menu-entry>
<menu-entry @click.native="addCustomAccount">
<icon-provider slot="icon" provider-id="custom"></icon-provider>
<span>添加自定义图床账号</span>
</menu-entry>
<menu-entry @click.native="addGiteaImgStorage">
<icon-provider slot="icon" provider-id="gitea"></icon-provider>
<span>添加Gitea图床仓库</span>
@ -63,6 +70,7 @@ import MenuItem from '../menus/common/MenuItem';
import smmsHelper from '../../services/providers/helpers/smmsHelper';
import store from '../../store';
import giteaHelper from '../../services/providers/helpers/giteaHelper';
import customHelper from '../../services/providers/helpers/customHelper';
import utils from '../../services/utils';
export default modalTemplate({
@ -79,10 +87,21 @@ export default modalTemplate({
checkedStorage() {
return store.getters['img/getCheckedStorage'];
},
smmsTokens() {
const smmsTokensBySub = store.getters['data/smmsTokensBySub'];
return Object.values(smmsTokensBySub)
.sort((token1, token2) => token1.name.localeCompare(token2.name));
imageTokens() {
return [
...Object.values(store.getters['data/smmsTokensBySub']).map(token => ({
...token,
providerId: 'smms',
remark: 'SM.MS图床',
})),
...Object.values(store.getters['data/customTokensBySub']).map(token => ({
...token,
providerId: 'custom',
headers: token.customHeaders && JSON.stringify(token.customHeaders),
params: token.customParams && JSON.stringify(token.customParams),
remark: '自定义图床',
})),
];
},
giteaTokensImgStorages() {
const giteaTokensBySub = store.getters['data/giteaTokensBySub'];
@ -118,17 +137,22 @@ export default modalTemplate({
store.dispatch('notification/info', '暂无已选择的图床,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
return;
}
if (currStorage.provider === 'smms') {
const filterTokens = this.smmsTokens.filter(it => it.sub === currStorage.sub);
if (currStorage.provider === 'smms' || currStorage.provider === 'custom') {
const filterTokens = this.imageTokens.filter(it => it.sub === currStorage.sub);
if (!filterTokens.length) {
store.dispatch('notification/info', 'SMS图床已失效,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
store.dispatch('notification/info', '图床已失效,未自动上传图片!请选择图床后重新粘贴/拖拽图片!');
return;
}
const token = filterTokens[0];
this.url = await smmsHelper.uploadFile({
token,
file: imgFile,
});
const helper = currStorage.provider === 'smms' ? smmsHelper : customHelper;
try {
this.url = await helper.uploadFile({
token,
file: imgFile,
});
} catch (err) {
store.dispatch('notification/error', err);
}
} else if (currStorage.provider === 'gitea') {
const filterTokenStorages = this.giteaTokensImgStorages
.filter(it => it.sid === currStorage.sub);
@ -185,7 +209,7 @@ export default modalTemplate({
async remove(proivderId, item) {
try {
await store.dispatch('modal/open', 'imgStorageDeletion');
if (proivderId === 'smms') {
if (proivderId === 'smms' || proivderId === 'custom') {
const tokensBySub = utils.deepCopy(store.getters[`data/${proivderId}TokensBySub`]);
delete tokensBySub[item.sub];
//
@ -203,6 +227,10 @@ export default modalTemplate({
const { proxyUrl, apiSecretToken } = await store.dispatch('modal/open', { type: 'smmsAccount' });
await smmsHelper.addAccount(proxyUrl, apiSecretToken);
},
async addCustomAccount() {
const accountInfo = await store.dispatch('modal/open', { type: 'customAccount' });
await customHelper.addAccount(accountInfo);
},
async addGiteaImgStorage() {
try {
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'giteaAccount' });
@ -239,6 +267,18 @@ export default modalTemplate({
});
</script>
<style lang="scss">
.line-entry {
word-break: break-word; /* 文本行的任意字内断开,就算是一个单词也会分开 */
word-wrap: break-word; /* IE */
white-space: -moz-pre-wrap; /* Mozilla */
white-space: -hp-pre-wrap; /* HP printers */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: pre; /* CSS2 */
white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
}
.menu-item__button {
width: 30px;
height: 30px;

View File

@ -37,7 +37,7 @@
</div>
</div>
<div class="modal__info" v-if="publishLocations.length">
<b>Tip:</b> Removing a location won't delete any file.
<b>提示:</b> 删除位置不会删除任何文件
</div>
</div>
<div class="modal__button-bar">

View File

@ -0,0 +1,109 @@
<template>
<modal-inner aria-label="链接自定义图床账号">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="custom"></icon-provider>
</div>
<p>将您的<b>自定义图床</b>账号链接到<b>StackEdit</b></p>
<form-entry label="自定义标识" error="name">
<input slot="field" class="textfield" type="text" v-model.trim="name" @keydown.enter="resolve()">
<div class="form-entry__info">
自定义标识如果一样会覆盖之前的自定义图床账号
</div>
</form-entry>
<form-entry label="上传图片接口地址" error="uploadUrl">
<input slot="field" class="textfield" type="text" v-model.trim="uploadUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
填入您个人的图床上传接口地址上传接口仅支持POST提交
</div>
</form-entry>
<form-entry label="文件参数名" error="fileParamName">
<input slot="field" class="textfield" type="text" v-model.trim="fileParamName" @keydown.enter="resolve()">
<div class="form-entry__info">
文件参数名如file
</div>
</form-entry>
<form-entry label="自定义请求头配置" error="customHeaders">
<input slot="field" class="textfield" type="text" v-model.trim="customHeaders" @keydown.enter="resolve()">
<div class="form-entry__info">
非必填自定义请求头是JSON字符串格式{"token": "..."}
</div>
</form-entry>
<form-entry label="自定义FORM参数设置" error="customParams">
<input slot="field" class="textfield" type="text" v-model.trim="customParams" @keydown.enter="resolve()">
<div class="form-entry__info">
非必填自定义FORM参数是JSON字符串格式{"param1": "..."}
</div>
</form-entry>
<form-entry label="响应图片URL参数" error="resultUrlParam">
<input slot="field" class="textfield" type="text" v-model.trim="resultUrlParam" @keydown.enter="resolve()">
<div class="form-entry__info">
响应JSON中图片URL的路径 data.url
</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 modalTemplate from '../common/modalTemplate';
export default modalTemplate({
computedLocalSettings: {
name: 'name',
uploadUrl: 'uploadUrl',
fileParamName: 'fileParamName',
customHeaders: 'customHeaders',
customParams: 'customParams',
resultUrlParam: 'resultUrlParam',
},
methods: {
resolve() {
if (!this.name) {
this.setError('name');
}
if (!this.uploadUrl) {
this.setError('uploadUrl');
}
if (!this.fileParamName) {
this.setError('fileParamName');
}
if (!this.resultUrlParam) {
this.setError('resultUrlParam');
}
let customHeaders = null;
if (this.customHeaders) {
try {
customHeaders = JSON.parse(this.customHeaders);
} catch (err) {
this.setError('customHeaders');
return;
}
}
let customParams = null;
if (this.customParams) {
try {
customParams = JSON.parse(this.customParams);
} catch (err) {
this.setError('customParams');
return;
}
}
if (this.uploadUrl && this.fileParamName) {
this.config.resolve({
name: this.name,
uploadUrl: this.uploadUrl,
fileParamName: this.fileParamName,
resultUrlParam: this.resultUrlParam,
customHeaders,
customParams,
});
}
},
},
});
</script>

View File

@ -264,6 +264,11 @@ export default [
'SM.MS账号',
'将您的SM.MS账号链接到StackEdit中文版。',
),
new Feature(
'addCustomAccount',
'自定义图床账号',
'将您的自定义图床账号链接到StackEdit中文版。',
),
new Feature(
'removeAccount',
'移除账号',

View File

@ -102,4 +102,8 @@ export default {
.icon-provider--smms {
background-image: url(../assets/iconSmms.svg);
}
.icon-provider--custom {
background-image: url(../assets/iconCustom.svg);
}
</style>

View File

@ -0,0 +1,69 @@
import md5 from 'js-md5';
import networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
import utils from '../../utils';
/**
* 自定义账号前缀
*/
const subPrefix = 'cs';
export default {
subPrefix,
async addAccount({
name,
uploadUrl,
fileParamName,
customHeaders,
customParams,
resultUrlParam,
}) {
userSvc.addUserInfo({
id: `${subPrefix}:${utils.encodeBase64(name)}`,
name,
imageUrl: '',
});
// Build token object including sub
const token = {
uploadUrl,
fileParamName,
customHeaders,
customParams,
resultUrlParam,
name,
sub: utils.encodeBase64(name),
};
// Add token to smms tokens
store.dispatch('data/addCustomToken', token);
badgeSvc.addBadge('addCustomAccount');
return token;
},
async uploadFile({
token,
file,
}) {
const newFileName = `${md5(await utils.encodeFiletoBase64(file))}.${file.type.split('/')[1]}`;
const newfile = new File([file], newFileName, { type: file.type });
const headers = token.customHeaders || {};
const formData = token.formData || {};
formData[token.fileParamName] = newfile;
const { body } = await networkSvc.request({
method: 'POST',
url: token.uploadUrl,
headers,
formData,
});
const paramArray = token.resultUrlParam.split('.');
let result = body;
paramArray.forEach((paramName) => {
result = result[paramName];
if (!result) {
store.dispatch('notification/error', `自定义图床上传图片失败响应Body为${JSON.stringify(body)}`);
throw new Error(`自定义图床上传图片失败响应Body为${JSON.stringify(body)}`);
}
});
return result;
},
};

View File

@ -217,6 +217,7 @@ export default {
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
smmsTokensBySub: (state, { tokensByType }) => tokensByType.smms || {},
customTokensBySub: (state, { tokensByType }) => tokensByType.custom || {},
badgeCreations: getter('badgeCreations'),
badgeTree: (state, { badgeCreations }) => features
.map(feature => feature.toBadge(badgeCreations)),
@ -313,5 +314,6 @@ export default {
addZendeskToken: tokenAdder('zendesk'),
patchBadgeCreations: patcher('badgeCreations'),
addSmmsToken: tokenAdder('smms'),
addCustomToken: tokenAdder('custom'),
},
};

View File

@ -134,7 +134,10 @@ const store = new Vuex.Store({
hash: undefined,
}), true);
const extension = item.type === 'syncLocation' ? 'sync' : 'publish';
result[id] = `${pathsByItemId[item.fileId]}.${encodedItem}.${extension}`;
const path = pathsByItemId[item.fileId];
if (path) {
result[id] = `${path}.${encodedItem}.${extension}`;
}
}
});
return result;