支持Gitee 修复Github Oauth2授权bug

This commit is contained in:
Jacky Chen 2022-05-23 11:29:54 +08:00
parent 46383b5b6a
commit e7e335d958
45 changed files with 1306 additions and 33 deletions

View File

@ -1,4 +1,4 @@
FROM benweet/stackedit-base
FROM node:11.15.0
RUN mkdir -p /opt/stackedit
WORKDIR /opt/stackedit

View File

@ -1,10 +1,22 @@
# StackEdit
从 [StackEdit 官方](https://github.com/benweet/stackedit) fork出来然后加上了 **Gitee** 的支持并且已经重新打了镜像以下官方的部署方式除了Docker镜像地址不同其他均一致。
Fork出来修改的原因Stackedit的作者可能因为什么原因已经很久不维护了Github授权登录很早之前就登录不了了并且还没发支持国内常用的Gitee比较蛋疼所以想到Fork出来改大概花了周末一整天终于改好了。
新的Docker镜像在中央仓库为mafgwo/stackedit当前最新版本为5.15.1(延续原有版本号)
并增加了以下三个环境变量:
- `GITEE_CLIENT_ID` Gitee 的 Client ID
- `GITEE_CLIENT_SECRET` Gitee 的 Client Secret
- `GITEE_CALLBACK` Gitee的回调地址Gitee授权获取token时还需要传入回调地址格式是 http[s]://[hostname]:[port]/oauth2/callback
[![Build Status](https://img.shields.io/travis/benweet/stackedit.svg?style=flat)](https://travis-ci.org/benweet/stackedit) [![NPM version](https://img.shields.io/npm/v/stackedit.svg?style=flat)](https://www.npmjs.org/package/stackedit)
> Full-featured, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites.
https://stackedit.io/
https://edit.qicoder.com/
### Ecosystem

View File

@ -8,6 +8,8 @@ googleClientId: ""
googleApiKey: ""
githubClientId: ""
githubClientSecret: ""
giteeClientId: ""
giteeClientSecret: ""
wordpressClientId: ""
wordpressSecret: ""
paypalReceiverEmail: ""

View File

@ -15,10 +15,10 @@
},
"app": {
"urls": [
"https://stackedit.io/"
"https://edit.qicoder.com/"
],
"launch": {
"web_url": "https://stackedit.io/app"
"web_url": "https://edit.qicoder.com/app"
}
},
"offline_enabled": true,

View File

@ -23,7 +23,7 @@ module.exports = {
},
dev: {
env: require('./dev.env'),
port: 8080,
port: 80,
autoOpenBrowser: false,
assetsSubDirectory: 'static',
assetsPublicPath: '/',

View File

@ -3,15 +3,26 @@
<head>
<meta charset="utf-8">
<title>StackEdit</title>
<link rel="canonical" href="https://stackedit.io/app">
<link rel="canonical" href="https://edit.qicoder.com/app">
<meta name="description" content="Free, open-source, full-featured Markdown editor.">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<script>
var _hmt = _hmt || [];
</script>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script>
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?6e5d2dbd2eeb7bba778f1056fba280d1";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>

View File

@ -1,11 +1,11 @@
{
"name": "stackedit",
"version": "5.14.10",
"version": "5.15.1",
"description": "Free, open-source, full-featured Markdown editor",
"author": "Benoit Schweblin",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/benweet/stackedit/issues"
"url": "https://github.com/mafgwo/stackedit/issues"
},
"main": "index.js",
"scripts": {

View File

@ -8,6 +8,9 @@ const dropboxAppKey = process.env.DROPBOX_APP_KEY;
const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
const githubClientId = process.env.GITHUB_CLIENT_ID;
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
const giteeClientId = process.env.GITEE_CLIENT_ID;
const giteeClientSecret = process.env.GITEE_CLIENT_SECRET;
const giteeCallback = process.env.GITEE_CALLBACK;
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleApiKey = process.env.GOOGLE_API_KEY;
const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;
@ -22,6 +25,9 @@ exports.values = {
dropboxAppKeyFull,
githubClientId,
githubClientSecret,
giteeClientId,
giteeClientSecret,
giteeCallback,
googleClientId,
googleApiKey,
wordpressClientId,
@ -31,6 +37,7 @@ exports.publicValues = {
dropboxAppKey,
dropboxAppKeyFull,
githubClientId,
giteeClientId,
googleClientId,
googleApiKey,
wordpressClientId,

45
server/gitee.js Normal file
View File

@ -0,0 +1,45 @@
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
const request = require('request');
const conf = require('./conf');
function giteeToken(clientId, code) {
console.log('clientId: ' + clientId);
console.log('code: ' + code);
console.log('client_secret: ' + conf.values.giteeClientSecret);
console.log('redirect_uri: ' + conf.values.giteeCallback);
return new Promise((resolve, reject) => {
request({
method: 'POST',
url: 'https://gitee.com/oauth/token',
form: {
client_id: clientId,
client_secret: conf.values.giteeClientSecret,
code,
grant_type: 'authorization_code',
scope: 'authorization_code',
redirect_uri: conf.values.giteeCallback,
},
json: true
}, (err, res, body) => {
if (err) {
reject(err);
}
const token = body.access_token;
if (token) {
resolve(token);
} else {
reject(res.statusCode + ',body:' + JSON.stringify(body));
}
});
});
}
exports.giteeToken = (req, res) => {
giteeToken(req.query.clientId, req.query.code)
.then(
token => res.send(token),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
};

View File

@ -4,6 +4,7 @@ const bodyParser = require('body-parser');
const path = require('path');
const user = require('./user');
const github = require('./github');
const gitee = require('./gitee');
const pdf = require('./pdf');
const pandoc = require('./pandoc');
const conf = require('./conf');
@ -25,6 +26,7 @@ module.exports = (app) => {
}
app.get('/oauth2/githubToken', github.githubToken);
app.get('/oauth2/giteeToken', gitee.giteeToken);
app.get('/conf', (req, res) => res.send(conf.publicValues));
app.get('/userInfo', user.userInfo);
app.post('/pdfExport', pdf.generate);
@ -37,6 +39,8 @@ module.exports = (app) => {
app.get('/', (req, res) => res.sendFile(resolvePath('static/landing/index.html')));
// Serve sitemap.xml
app.get('/sitemap.xml', (req, res) => res.sendFile(resolvePath('static/sitemap.xml')));
// Serve google-api.js
app.get('/google-api.js', (req, res) => res.sendFile(resolvePath('static/google-api.js')));
// Serve callback.html
app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
// Google Drive action receiver

5
src/assets/iconGitee.svg Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1652950823759" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2991" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><style type="text/css"></style></defs>
<path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" fill="#C71D23" p-id="2992"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,7 +1,7 @@
<template>
<div class="modal" v-if="config" @keydown.esc.stop="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<div class="modal__sponsor-banner" v-if="!isSponsor">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/mafgwo/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div>
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
@ -56,6 +56,11 @@ import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal';
import GiteeAccountModal from './modals/providers/GiteeAccountModal';
import GiteeOpenModal from './modals/providers/GiteeOpenModal';
import GiteeSaveModal from './modals/providers/GiteeSaveModal';
import GiteeWorkspaceModal from './modals/providers/GiteeWorkspaceModal';
import GiteePublishModal from './modals/providers/GiteePublishModal';
import GitlabAccountModal from './modals/providers/GitlabAccountModal';
import GitlabOpenModal from './modals/providers/GitlabOpenModal';
import GitlabPublishModal from './modals/providers/GitlabPublishModal';
@ -107,6 +112,11 @@ export default {
GithubPublishModal,
GistSyncModal,
GistPublishModal,
GiteeAccountModal,
GiteeOpenModal,
GiteeSaveModal,
GiteeWorkspaceModal,
GiteePublishModal,
GitlabAccountModal,
GitlabOpenModal,
GitlabPublishModal,

View File

@ -23,6 +23,9 @@
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'giteeWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">Gitee repo</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'gitlabWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitLab project</a>.
</span>

View File

@ -52,6 +52,13 @@
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in giteeTokens" :key="token.sub">
<menu-entry @click.native="publishGitee(token)">
<icon-provider slot="icon" provider-id="gitee"></icon-provider>
<div>Publish to Gitee</div>
<span>{{token.name}}</span>
</menu-entry>
</div>
<div v-for="token in gitlabTokens" :key="token.sub">
<menu-entry @click.native="publishGitlab(token)">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
@ -93,6 +100,10 @@
<icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span>
</menu-entry>
<menu-entry @click.native="addGiteeAccount">
<icon-provider slot="icon" provider-id="gitee"></icon-provider>
<span>Add Gitee account</span>
</menu-entry>
<menu-entry @click.native="addGitlabAccount">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<span>Add GitLab account</span>
@ -119,6 +130,7 @@ import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import giteeHelper from '../../services/providers/helpers/giteeHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
@ -168,6 +180,9 @@ export default {
githubTokens() {
return tokensToArray(store.getters['data/githubTokensBySub']);
},
giteeTokens() {
return tokensToArray(store.getters['data/giteeTokensBySub']);
},
gitlabTokens() {
return tokensToArray(store.getters['data/gitlabTokensBySub']);
},
@ -218,6 +233,12 @@ export default {
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
async addGiteeAccount() {
try {
await store.dispatch('modal/open', { type: 'giteeAccount' });
await giteeHelper.addAccount();
} catch (e) { /* cancel */ }
},
async addGitlabAccount() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
@ -245,6 +266,7 @@ export default {
publishBloggerPage: publishModalOpener('bloggerPagePublish', 'publishToBloggerPage'),
publishDropbox: publishModalOpener('dropboxPublish', 'publishToDropbox'),
publishGithub: publishModalOpener('githubPublish', 'publishToGithub'),
publishGitee: publishModalOpener('giteePublish', 'publishToGitee'),
publishGist: publishModalOpener('gistPublish', 'publishToGist'),
publishGitlab: publishModalOpener('gitlabPublish', 'publishToGitlab'),
publishGoogleDrive: publishModalOpener('googleDrivePublish', 'publishToGoogleDrive'),

View File

@ -83,6 +83,10 @@
<icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span>
</menu-entry>
<menu-entry @click.native="addGiteeAccount">
<icon-provider slot="icon" provider-id="gitee"></icon-provider>
<span>Add Gitee account</span>
</menu-entry>
<menu-entry @click.native="addGitlabAccount">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<span>Add GitLab account</span>
@ -101,6 +105,7 @@ import MenuEntry from './common/MenuEntry';
import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import giteeHelper from '../../services/providers/helpers/giteeHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import googleDriveProvider from '../../services/providers/googleDriveProvider';
import dropboxProvider from '../../services/providers/dropboxProvider';
@ -148,6 +153,9 @@ export default {
githubTokens() {
return tokensToArray(store.getters['data/githubTokensBySub']);
},
giteeTokens() {
return tokensToArray(store.getters['data/giteeTokensBySub']);
},
gitlabTokens() {
return tokensToArray(store.getters['data/gitlabTokensBySub']);
},
@ -157,7 +165,8 @@ export default {
noToken() {
return !this.googleDriveTokens.length
&& !this.dropboxTokens.length
&& !this.githubTokens.length;
&& !this.githubTokens.length
&& !this.giteeTokens.length;
},
},
methods: {
@ -183,6 +192,12 @@ export default {
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
async addGiteeAccount() {
try {
await store.dispatch('modal/open', { type: 'giteeAccount' });
await giteeHelper.addAccount();
} catch (e) { /* cancel */ }
},
async addGitlabAccount() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });

View File

@ -21,6 +21,10 @@
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<span>Add a <b>GitHub</b> workspace</span>
</menu-entry>
<menu-entry @click.native="addGiteeWorkspace">
<icon-provider slot="icon" provider-id="giteeWorkspace"></icon-provider>
<span>Add a <b>Gitee</b> workspace</span>
</menu-entry>
<menu-entry @click.native="addGitlabWorkspace">
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
<span>Add a <b>GitLab</b> workspace</span>
@ -67,6 +71,13 @@ export default {
});
} catch (e) { /* Cancel */ }
},
async addGiteeWorkspace() {
try {
store.dispatch('modal/open', {
type: 'giteeWorkspace',
});
} catch (e) { /* Cancel */ }
},
async addGitlabWorkspace() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });

View File

@ -2,9 +2,9 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content">
<div class="logo-background"></div>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
StackEdit on <a target="_blank" href="https://github.com/mafgwo/stackedit/">GitHub</a>
<br>
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a>
<a target="_blank" href="https://github.com/mafgwo/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/mafgwo/stackedit/releases">Changelog</a>
<br>
<a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha">Chrome extension</a>
<br>
@ -12,7 +12,7 @@
<br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<hr>
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
<small>© 2013-2022 Dock5 Software Ltd.<br>v{{version}}</small>
<h3>FAQ</h3>
<div class="faq" v-html="faq"></div>
<div class="modal__info">

View File

@ -49,6 +49,10 @@
<icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span>
</menu-entry>
<menu-entry @click.native="addGiteeAccount">
<icon-provider slot="icon" provider-id="gitee"></icon-provider>
<span>Add Gitee account</span>
</menu-entry>
<menu-entry @click.native="addGitlabAccount">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<span>Add GitLab account</span>
@ -85,6 +89,7 @@ import utils from '../../services/utils';
import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import giteeHelper from '../../services/providers/helpers/giteeHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
@ -128,6 +133,13 @@ export default {
name: token.name,
scopes: token.scopes,
})),
...Object.values(store.getters['data/giteeTokensBySub']).map(token => ({
token,
providerId: 'gitee',
userId: token.sub,
name: token.name,
scopes: ['projects', 'pull_requests'],
})),
...Object.values(store.getters['data/gitlabTokensBySub']).map(token => ({
token,
providerId: 'gitlab',
@ -180,6 +192,12 @@ export default {
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
async addGiteeAccount() {
try {
await store.dispatch('modal/open', { type: 'giteeAccount' });
await giteeHelper.addAccount();
} catch (e) { /* cancel */ }
},
async addGitlabAccount() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });

View File

@ -72,7 +72,7 @@
<span class="token key atrule">katex</span><span class="token punctuation">:</span>
<span class="token key atrule">enabled</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
</code></pre>
<p>For the full list of options, see <a href="https://github.com/benweet/stackedit/blob/master/src/data/presets.js" target="_blank">here</a>.</p>
<p>For the full list of options, see <a href="https://github.com/mafgwo/stackedit/blob/master/src/data/presets.js" target="_blank">here</a>.</p>
</div>
</div>
</div>

View File

@ -45,7 +45,7 @@ export default modalTemplate({
},
methods: {
resolve(evt) {
evt.preventDefault(); // Fixes https://github.com/benweet/stackedit/issues/1503
evt.preventDefault(); // Fixes https://github.com/mafgwo/stackedit/issues/1503
if (!this.url) {
this.setError('url');
} else {

View File

@ -22,7 +22,7 @@ export default modalTemplate({
}),
methods: {
resolve(evt) {
evt.preventDefault(); // Fixes https://github.com/benweet/stackedit/issues/1503
evt.preventDefault(); // Fixes https://github.com/mafgwo/stackedit/issues/1503
if (!this.url) {
this.setError('url');
} else {

View File

@ -0,0 +1,31 @@
<template>
<modal-inner aria-label="Link Gitee account">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>Link your <b>Gitee</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
<input type="checkbox" v-model="repoFullAccess"> Grant access to your private repositories
</label>
</div>
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="config.resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
computedLocalSettings: {
repoFullAccess: 'giteeRepoFullAccess',
},
});
</script>

View File

@ -0,0 +1,70 @@
<template>
<modal-inner aria-label="Synchronize with Gitee">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>Open a file from your <b>Gitee</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://gitee.com/owner/my-repo
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import giteeProvider from '../../../services/providers/giteeProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'giteeRepoUrl',
},
methods: {
resolve() {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
}
if (!this.path) {
this.setError('path');
}
if (parsedRepo && this.path) {
// Return new location
const location = giteeProvider.makeLocation(
this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,86 @@
<template>
<modal-inner aria-label="Publish to Gitee">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>Publish <b>{{currentFileName}}</b> to your <b>Gitee</b> repository.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://gitee.com/owner/my-repo
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
<div class="form-entry__actions">
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import giteeProvider from '../../../services/providers/giteeProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'giteeRepoUrl',
selectedTemplate: 'giteePublishTemplate',
},
created() {
this.path = `${this.currentFileName}.md`;
},
methods: {
resolve() {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
}
if (!this.path) {
this.setError('path');
}
if (parsedRepo && this.path) {
// Return new location
const location = giteeProvider.makeLocation(
this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,73 @@
<template>
<modal-inner aria-label="Synchronize with Gitee">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>Save <b>{{currentFileName}}</b> to your <b>Gitee</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://gitee.com/owner/my-repo
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import giteeProvider from '../../../services/providers/giteeProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'giteeRepoUrl',
},
created() {
this.path = `${this.currentFileName}.md`;
},
methods: {
resolve() {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
}
if (!this.path) {
this.setError('path');
}
if (parsedRepo && this.path) {
const location = giteeProvider.makeLocation(
this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
},
},
});
</script>

View File

@ -0,0 +1,65 @@
<template>
<modal-inner aria-label="Synchronize with Gitee">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>Create a workspace synced with a <b>Gitee</b> repository folder.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://gitee.com/owner/my-repo
</div>
</form-entry>
<form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the root folder will be used.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'giteeWorkspaceRepoUrl',
},
methods: {
resolve() {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
const path = this.path && this.path.replace(/^\//, '');
const url = utils.addQueryParams('app', {
...parsedRepo,
providerId: 'giteeWorkspace',
branch: this.branch || 'master',
path: path || undefined,
}, true);
this.config.resolve();
window.open(url);
}
},
},
});
</script>

View File

@ -19,6 +19,8 @@ export default () => ({
githubPublishTemplate: 'jekyllSite',
gistIsPublic: false,
gistPublishTemplate: 'plainText',
giteeRepoUrl: '',
giteeWorkspaceRepoUrl: '',
gitlabServerUrl: '',
gitlabApplicationId: '',
gitlabProjectUrl: '',

View File

@ -79,16 +79,16 @@ turndown:
# GitHub/GitLab commit messages
git:
createFileMessage: '{{path}} created from https://stackedit.io/'
updateFileMessage: '{{path}} updated from https://stackedit.io/'
deleteFileMessage: '{{path}} deleted from https://stackedit.io/'
createFileMessage: '{{path}} created from https://edit.qicoder.com/'
updateFileMessage: '{{path}} updated from https://edit.qicoder.com/'
deleteFileMessage: '{{path}} deleted from https://edit.qicoder.com/'
# Default content for new files
newFileContent: |
> Written with [StackEdit](https://stackedit.io/).
> Written with [StackEdit](https://edit.qicoder.com/).
# Default properties for new files
newFileProperties: |

View File

@ -6,4 +6,4 @@ We recommend syncing your workspace to make sure files won't be lost in case you
**Can StackEdit access my data without telling me?**
StackEdit is a browser-based application. The access tokens issued by Google, Dropbox, GitHub... are stored in your browser and are not sent to any kind of backend or 3^rd^ party so your data won't be accessed by anyone.
StackEdit is a browser-based application. The access tokens issued by Google, Dropbox, GitHub, Gitee... are stored in your browser and are not sent to any kind of backend or 3^rd^ party so your data won't be accessed by anyone.

View File

@ -172,6 +172,11 @@ export default [
'GitHub workspace creator',
'Use the workspace menu to create a GitHub workspace.',
),
new Feature(
'addGiteeWorkspace',
'GitHub workspace creator',
'Use the workspace menu to create a Gitee workspace.',
),
new Feature(
'addGitlabWorkspace',
'GitLab workspace creator',

View File

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{files.0.name}}</title>
<link rel="stylesheet" href="https://stackedit.io/style.css" />
<link rel="stylesheet" href="https://edit.qicoder.com/style.css" />
</head>
{{#if pdf}}

View File

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{files.0.name}}</title>
<link rel="stylesheet" href="https://stackedit.io/style.css" />
<link rel="stylesheet" href="https://edit.qicoder.com/style.css" />
</head>
{{#if pdf}}

View File

@ -26,6 +26,8 @@ export default {
return 'blogger';
case 'couchdbWorkspace':
return 'couchdb';
case 'giteeWorkspace':
return 'gitee';
default:
return this.providerId;
}
@ -86,4 +88,8 @@ export default {
.icon-provider--couchdb {
background-image: url(../assets/iconCouchdb.svg);
}
.icon-provider--gitee {
background-image: url(../assets/iconGitee.svg);
}
</style>

View File

@ -69,7 +69,7 @@ export default {
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
script.src = `https://www.gstatic.cn/charts/loader.js?${Date.now()}`;
try {
document.head.appendChild(script); // This can fail with bad network
timeout = setTimeout(reject, networkTimeout);

View File

@ -0,0 +1,165 @@
import store from '../../store';
import giteeHelper from './helpers/giteeHelper';
import Provider from './common/Provider';
import utils from '../utils';
import workspaceSvc from '../workspaceSvc';
import userSvc from '../userSvc';
const savedSha = {};
export default new Provider({
id: 'gitee',
name: 'Gitee',
getToken({ sub }) {
return store.getters['data/giteeTokensBySub'][sub];
},
getLocationUrl({
owner,
repo,
branch,
path,
}) {
return `https://gitee.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
},
getLocationDescription({ path }) {
return path;
},
async downloadContent(token, syncLocation) {
const { sha, data } = await giteeHelper.downloadFile({
...syncLocation,
token,
});
savedSha[syncLocation.id] = sha;
return Provider.parseContent(data, `${syncLocation.fileId}/content`);
},
async uploadContent(token, content, syncLocation) {
if (!savedSha[syncLocation.id]) {
try {
// Get the last sha
await this.downloadContent(token, syncLocation);
} catch (e) {
// Ignore error
}
}
const sha = savedSha[syncLocation.id];
delete savedSha[syncLocation.id];
await giteeHelper.uploadFile({
...syncLocation,
token,
content: Provider.serializeContent(content),
sha,
});
return syncLocation;
},
async publish(token, html, metadata, publishLocation) {
try {
// Get the last sha
await this.downloadContent(token, publishLocation);
} catch (e) {
// Ignore error
}
const sha = savedSha[publishLocation.id];
delete savedSha[publishLocation.id];
await giteeHelper.uploadFile({
...publishLocation,
token,
content: html,
sha,
});
return publishLocation;
},
async openFile(token, syncLocation) {
// Check if the file exists and open it
if (!Provider.openFileWithLocation(syncLocation)) {
// Download content from Gitee
let content;
try {
content = await this.downloadContent(token, syncLocation);
} catch (e) {
store.dispatch('notification/error', `Could not open file ${syncLocation.path}.`);
return;
}
// Create the file
let name = syncLocation.path;
const slashPos = name.lastIndexOf('/');
if (slashPos > -1 && slashPos < name.length - 1) {
name = name.slice(slashPos + 1);
}
const dotPos = name.lastIndexOf('.');
if (dotPos > 0 && slashPos < name.length) {
name = name.slice(0, dotPos);
}
const item = await workspaceSvc.createFile({
name,
parentId: store.getters['file/current'].parentId,
text: content.text,
properties: content.properties,
discussions: content.discussions,
comments: content.comments,
}, true);
store.commit('file/setCurrentId', item.id);
workspaceSvc.addSyncLocation({
...syncLocation,
fileId: item.id,
});
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Gitee.`);
}
},
makeLocation(token, owner, repo, branch, path) {
return {
providerId: this.id,
sub: token.sub,
owner,
repo,
branch,
path,
};
},
async listFileRevisions({ token, syncLocation }) {
const entries = await giteeHelper.getCommits({
...syncLocation,
token,
});
return entries.map(({
author,
committer,
commit,
sha,
}) => {
let user;
if (author && author.login) {
user = author;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${giteeHelper.subPrefix}:${user.login}`;
userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date);
return {
id: sha,
sub,
created: date ? new Date(date).getTime() : 1,
};
});
},
async loadFileRevision() {
// Revision are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
syncLocation,
revisionId,
}) {
const { data } = await giteeHelper.downloadFile({
...syncLocation,
token,
branch: revisionId,
});
return Provider.parseContent(data, contentId);
},
});

View File

@ -0,0 +1,281 @@
import store from '../../store';
import giteeHelper from './helpers/giteeHelper';
import Provider from './common/Provider';
import utils from '../utils';
import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
import badgeSvc from '../badgeSvc';
const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
export default new Provider({
id: 'giteeWorkspace',
name: 'Gitee',
getToken() {
return store.getters['workspace/syncToken'];
},
getWorkspaceParams({
owner,
repo,
branch,
path,
}) {
return {
providerId: this.id,
owner,
repo,
branch,
path,
};
},
getWorkspaceLocationUrl({
owner,
repo,
branch,
path,
}) {
return `https://gitee.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(path)}`;
},
getSyncDataUrl({ id }) {
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
return `https://gitee.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(branch)}/${utils.encodeUrlPath(getAbsolutePath({ id }))}`;
},
getSyncDataDescription({ id }) {
return getAbsolutePath({ id });
},
async initWorkspace() {
const { owner, repo, branch } = utils.queryParams;
const workspaceParams = this.getWorkspaceParams({ owner, repo, branch });
if (!branch) {
workspaceParams.branch = 'master';
}
// Extract path param
const path = (utils.queryParams.path || '')
.trim()
.replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, '/'); // Add trailing `/`
if (path !== '/') {
workspaceParams.path = path;
}
const workspaceId = utils.makeWorkspaceId(workspaceParams);
const workspace = store.getters['workspace/workspacesById'][workspaceId];
// See if we already have a token
let token;
if (workspace) {
// Token sub is in the workspace
token = store.getters['data/giteeTokensBySub'][workspace.sub];
}
if (!token) {
await store.dispatch('modal/open', { type: 'giteeAccount' });
token = await giteeHelper.addAccount();
}
if (!workspace) {
const pathEntries = (path || '').split('/');
const name = pathEntries[pathEntries.length - 2] || repo; // path ends with `/`
store.dispatch('workspace/patchWorkspacesById', {
[workspaceId]: {
...workspaceParams,
id: workspaceId,
sub: token.sub,
name,
},
});
}
badgeSvc.addBadge('addGithubWorkspace');
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {
return giteeHelper.getTree({
...store.getters['workspace/currentWorkspace'],
token: this.getToken(),
});
},
prepareChanges(tree) {
return gitWorkspaceSvc.makeChanges(tree);
},
async saveWorkspaceItem({ item }) {
const syncData = {
id: store.getters.gitPathsByItemId[item.id],
type: item.type,
hash: item.hash,
};
// Files and folders are not in git, only contents
if (item.type === 'file' || item.type === 'folder') {
return { syncData };
}
// locations are stored as paths, so we upload an empty file
const syncToken = store.getters['workspace/syncToken'];
await giteeHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
path: getAbsolutePath(syncData),
content: '',
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
// Return sync data to save
return { syncData };
},
async removeWorkspaceItem({ syncData }) {
if (gitWorkspaceSvc.shaByPath[syncData.id]) {
const syncToken = store.getters['workspace/syncToken'];
await giteeHelper.removeFile({
...store.getters['workspace/currentWorkspace'],
token: syncToken,
path: getAbsolutePath(syncData),
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
}
},
async downloadWorkspaceContent({
token,
contentId,
contentSyncData,
fileSyncData,
}) {
const { sha, data } = await giteeHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(fileSyncData),
});
gitWorkspaceSvc.shaByPath[fileSyncData.id] = sha;
const content = Provider.parseContent(data, contentId);
return {
content,
contentSyncData: {
...contentSyncData,
hash: content.hash,
sha,
},
};
},
async downloadWorkspaceData({ token, syncData }) {
if (!syncData) {
return {};
}
const { sha, data } = await giteeHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(syncData),
});
gitWorkspaceSvc.shaByPath[syncData.id] = sha;
const item = JSON.parse(data);
return {
item,
syncData: {
...syncData,
hash: item.hash,
sha,
},
};
},
async uploadWorkspaceContent({ token, content, file }) {
const path = store.getters.gitPathsByItemId[file.id];
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
const res = await giteeHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: absolutePath,
content: Provider.serializeContent(content),
sha: gitWorkspaceSvc.shaByPath[path],
});
// Return new sync data
return {
contentSyncData: {
id: store.getters.gitPathsByItemId[content.id],
type: content.type,
hash: content.hash,
sha: res.content.sha,
},
fileSyncData: {
id: path,
type: 'file',
hash: file.hash,
},
};
},
async uploadWorkspaceData({ token, item }) {
const path = store.getters.gitPathsByItemId[item.id];
const syncData = {
id: path,
type: item.type,
hash: item.hash,
};
const res = await giteeHelper.uploadFile({
...store.getters['workspace/currentWorkspace'],
token,
path: getAbsolutePath(syncData),
content: JSON.stringify(item),
sha: gitWorkspaceSvc.shaByPath[path],
});
return {
syncData: {
...syncData,
sha: res.content.sha,
},
};
},
async listFileRevisions({ token, fileSyncDataId }) {
const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
const entries = await giteeHelper.getCommits({
token,
owner,
repo,
sha: branch,
path: getAbsolutePath({ id: fileSyncDataId }),
});
return entries.map(({
author,
committer,
commit,
sha,
}) => {
let user;
if (author && author.login) {
user = author;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${giteeHelper.subPrefix}:${user.login}`;
userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date)
|| 1;
return {
id: sha,
sub,
created: new Date(date).getTime(),
};
});
},
async loadFileRevision() {
// Revisions are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
fileSyncDataId,
revisionId,
}) {
const { data } = await giteeHelper.downloadFile({
...store.getters['workspace/currentWorkspace'],
token,
branch: revisionId,
path: getAbsolutePath({ id: fileSyncDataId }),
});
return Provider.parseContent(data, contentId);
},
});

View File

@ -0,0 +1,315 @@
import utils from '../../utils';
import networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const request = (token, options) => networkSvc.request({
...options,
headers: {
...options.headers || {},
Authorization: `token ${token.accessToken}`,
},
params: {
...options.params || {},
t: Date.now(), // Prevent from caching
},
});
const repoRequest = (token, owner, repo, options) => request(token, {
...options,
url: `https://gitee.com/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.url}`,
})
.then(res => res.body);
const getCommitMessage = (name, path) => {
const message = store.getters['data/computedSettings'].git[name];
return message.replace(/{{path}}/g, path);
};
/**
* Getting a user from its userId is not feasible with API v3.
* Using an undocumented endpoint...
*/
const subPrefix = 'ge';
userSvc.setInfoResolver('gitee', subPrefix, async (sub) => {
try {
const user = (await networkSvc.request({
url: `https://gitee.com/api/v5/users/${sub}`,
params: {
t: Date.now(), // Prevent from caching
},
})).body;
return {
id: `${subPrefix}:${user.login}`,
name: user.login,
imageUrl: user.avatar_url || '',
};
} catch (err) {
if (err.status !== 404) {
throw new Error('RETRY');
}
throw err;
}
});
export default {
subPrefix,
/**
* https://developer.gitee.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
*/
async startOauth2(scopes, sub = null, silent = false) {
const clientId = store.getters['data/serverConf'].giteeClientId;
// Get an OAuth2 code
const { code } = await networkSvc.startOauth2(
'https://gitee.com/oauth/authorize',
{
client_id: clientId,
scope: 'projects pull_requests',
response_type: 'code',
},
silent,
);
// Exchange code with token
const accessToken = (await networkSvc.request({
method: 'GET',
url: 'oauth2/giteeToken',
params: {
clientId,
code,
},
})).body;
// Call the user info endpoint
const user = (await networkSvc.request({
method: 'GET',
url: 'https://gitee.com/api/v5/user',
params: {
access_token: accessToken,
},
})).body;
userSvc.addUserInfo({
id: `${subPrefix}:${user.login}`,
name: user.login,
imageUrl: user.avatar_url || '',
});
// Check the returned sub consistency
if (sub && `${user.login}` !== sub) {
throw new Error('Gitee account ID not expected.');
}
// Build token object including scopes and sub
const token = {
scopes,
accessToken,
name: user.login,
sub: `${user.login}`,
};
// Add token to gitee tokens
store.dispatch('data/addGiteeToken', token);
return token;
},
async addAccount() {
const token = await this.startOauth2();
badgeSvc.addBadge('addGiteeAccount');
return token;
},
/**
* https://developer.gitee.com/v3/repos/commits/#get-a-single-commit
* https://developer.gitee.com/v3/git/trees/#get-a-tree
*/
async getTree({
token,
owner,
repo,
branch,
}) {
const { commit } = await repoRequest(token, owner, repo, {
url: `commits/${encodeURIComponent(branch)}`,
});
const { tree, truncated } = await repoRequest(token, owner, repo, {
url: `git/trees/${encodeURIComponent(commit.tree.sha)}?recursive=1`,
});
if (truncated) {
throw new Error('Git tree too big. Please remove some files in the repository.');
}
return tree;
},
/**
* https://developer.gitee.com/v3/repos/commits/#list-commits-on-a-repository
*/
async getCommits({
token,
owner,
repo,
sha,
path,
}) {
return repoRequest(token, owner, repo, {
url: 'commits',
params: { sha, path },
});
},
/**
* https://developer.gitee.com/v3/repos/contents/#create-a-file
* https://developer.gitee.com/v3/repos/contents/#update-a-file
*/
async uploadFile({
token,
owner,
repo,
branch,
path,
content,
sha,
}) {
return repoRequest(token, owner, repo, {
method: sha ? 'PUT' : 'POST',
url: `contents/${encodeURIComponent(path)}`,
body: {
message: getCommitMessage(sha ? 'updateFileMessage' : 'createFileMessage', path),
content: utils.encodeBase64(content),
sha,
branch,
},
});
},
/**
* https://developer.gitee.com/v3/repos/contents/#delete-a-file
*/
async removeFile({
token,
owner,
repo,
branch,
path,
sha,
}) {
return repoRequest(token, owner, repo, {
method: 'DELETE',
url: `contents/${encodeURIComponent(path)}`,
body: {
message: getCommitMessage('deleteFileMessage', path),
sha,
branch,
},
});
},
/**
* https://developer.gitee.com/v3/repos/contents/#get-contents
*/
async downloadFile({
token,
owner,
repo,
branch,
path,
}) {
const { sha, content } = await repoRequest(token, owner, repo, {
url: `contents/${encodeURIComponent(path)}`,
params: { ref: branch },
});
return {
sha,
data: utils.decodeBase64(content),
};
},
/**
* https://developer.gitee.com/v3/gists/#create-a-gist
* https://developer.gitee.com/v3/gists/#edit-a-gist
*/
async uploadGist({
token,
description,
filename,
content,
isPublic,
gistId,
}) {
const { body } = await request(token, gistId ? {
method: 'PATCH',
url: `https://gitee.com/api/v5/gists/${gistId}`,
body: {
description,
files: {
[filename]: {
content,
},
},
},
} : {
method: 'POST',
url: 'https://gitee.com/api/v5/gists',
body: {
description,
files: {
[filename]: {
content,
},
},
public: isPublic,
},
});
return body;
},
/**
* https://developer.gitee.com/v3/gists/#get-a-single-gist
*/
async downloadGist({
token,
gistId,
filename,
}) {
const result = (await request(token, {
url: `https://gitee.com/api/v5/gists/${gistId}`,
})).body.files[filename];
if (!result) {
throw new Error('Gist file not found.');
}
return result.content;
},
/**
* https://developer.gitee.com/v3/gists/#list-gist-commits
*/
async getGistCommits({
token,
gistId,
}) {
const { body } = await request(token, {
url: `https://gitee.com/api/v5/gists/${gistId}/commits`,
});
return body;
},
/**
* https://developer.gitee.com/v3/gists/#get-a-specific-revision-of-a-gist
*/
async downloadGistRevision({
token,
gistId,
filename,
sha,
}) {
const result = (await request(token, {
url: `https://gitee.com/api/v5/gists/${gistId}/${sha}`,
})).body.files[filename];
if (!result) {
throw new Error('Gist file not found.');
}
return result.content;
},
};

View File

@ -89,8 +89,8 @@ export default {
const user = (await networkSvc.request({
method: 'GET',
url: 'https://api.github.com/user',
params: {
access_token: accessToken,
headers: {
Authorization: `Bearer ${accessToken}`,
},
})).body;
userSvc.addUserInfo({

View File

@ -625,7 +625,7 @@ export default {
async openPicker(token, type = 'doc') {
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
if (!window.google) {
await networkSvc.loadScript('https://apis.google.com/js/api.js');
await networkSvc.loadScript('https://www.gstatic.cn/charts/loader.js');
await new Promise((resolve, reject) => window.gapi.load('picker', {
callback: resolve,
onerror: reject,

View File

@ -7,6 +7,7 @@ import providerRegistry from './providers/common/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider';
import './providers/giteeWorkspaceProvider';
import './providers/gitlabWorkspaceProvider';
import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc';

View File

@ -211,6 +211,7 @@ export default {
couchdbTokensBySub: (state, { tokensByType }) => tokensByType.couchdb || {},
dropboxTokensBySub: (state, { tokensByType }) => tokensByType.dropbox || {},
githubTokensBySub: (state, { tokensByType }) => tokensByType.github || {},
giteeTokensBySub: (state, { tokensByType }) => tokensByType.gitee || {},
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
@ -303,6 +304,7 @@ export default {
addCouchdbToken: tokenAdder('couchdb'),
addDropboxToken: tokenAdder('dropbox'),
addGithubToken: tokenAdder('github'),
addGiteeToken: tokenAdder('gitee'),
addGitlabToken: tokenAdder('gitlab'),
addWordpressToken: tokenAdder('wordpress'),
addZendeskToken: tokenAdder('zendesk'),

View File

@ -44,9 +44,11 @@ export default {
workspacesById[currentWorkspaceId] || mainWorkspace,
currentWorkspaceIsGit: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace',
currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace',
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
@ -63,6 +65,8 @@ export default {
return rootGetters['data/googleTokensBySub'][currentWorkspace.sub];
case 'githubWorkspace':
return rootGetters['data/githubTokensBySub'][currentWorkspace.sub];
case 'giteeWorkspace':
return rootGetters['data/giteeTokensBySub'][currentWorkspace.sub];
case 'gitlabWorkspace':
return rootGetters['data/gitlabTokensBySub'][currentWorkspace.sub];
case 'couchdbWorkspace':
@ -78,6 +82,8 @@ export default {
return 'google';
case 'githubWorkspace':
return 'github';
case 'giteeWorkspace':
return 'gitee';
case 'gitlabWorkspace':
return 'gitlab';
}

View File

@ -3,7 +3,7 @@
<head>
<title>StackEdit In-browser Markdown editor</title>
<link rel="canonical" href="https://stackedit.io/">
<link rel="canonical" href="https://edit.qicoder.com/">
<link rel="icon" href="static/landing/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="static/landing/favicon.ico" type="image/x-icon">
<meta charset="UTF-8">
@ -13,7 +13,7 @@
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="msvalidate.01" content="5E47EE6F67B069C17E3CDD418351A612">
<meta name="google-site-verification" content="iWDn0T2r2_bDQWp_nW23MGePbO9X0M8wQSzbOU70pFQ" />
<link rel="stylesheet" href="https://stackedit.io/style.css">
<link rel="stylesheet" href="https://edit.qicoder.com/style.css">
<style>
body {
background-color: #fbfbfb;

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://stackedit.io/</loc>
<loc>https://edit.qicoder.com/</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://stackedit.io/app</loc>
<loc>https://edit.qicoder.com/app</loc>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
@ -16,7 +16,7 @@
<priority>0.8</priority>
</url>
<url>
<loc>https://stackedit.io/privacy_policy.html</loc>
<loc>https://edit.qicoder.com/privacy_policy.html</loc>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>