主文档空间支持GitHub登录

This commit is contained in:
xiaoqi.cxq 2023-10-19 18:20:34 +08:00
parent 97b8d3c288
commit 39167fb193
25 changed files with 639 additions and 39 deletions

View File

@ -78,10 +78,10 @@ StackEdit中文版
- 支持分享文档2023-03-30 - 支持分享文档2023-03-30
- 支持ChatGPT生成内容2023-04-10 - 支持ChatGPT生成内容2023-04-10
- GitLab授权接口调整2023-08-26 - GitLab授权接口调整2023-08-26
- 主文档空间支持GitHub登录2023-10-19
## 国外开源版本弊端: ## 国外开源版本弊端:
- 作者已经不维护了 - 作者已经不维护了或很少维护了
- Github授权登录存在问题
- 不支持国内常用Gitee - 不支持国内常用Gitee
- 强依赖GoogleDrive而Google Drive在国内不能正常访问 - 强依赖GoogleDrive而Google Drive在国内不能正常访问

2
package-lock.json generated
View File

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

View File

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

View File

@ -72,6 +72,7 @@ module.exports = (app) => {
})); }));
// Serve share.html // Serve share.html
app.get('/share.html', (req, res) => res.sendFile(resolvePath('static/landing/share.html'))); app.get('/share.html', (req, res) => res.sendFile(resolvePath('static/landing/share.html')));
app.get('/gistshare.html', (req, res) => res.sendFile(resolvePath('static/landing/gistshare.html')));
// Serve static resources // Serve static resources
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {

View File

@ -10,6 +10,7 @@
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button> <button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
<button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button> <button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
<button v-for="(item, idx) in (simpleModal.resolveArray || [])" class="button button--resolve" @click="config.resolve(item.value)">{{item.text}}</button>
</div> </div>
</modal-inner> </modal-inner>
</div> </div>
@ -187,6 +188,7 @@ export default {
// User has to sign in // User has to sign in
await store.dispatch('modal/open', 'signInForSponsorship'); await store.dispatch('modal/open', 'signInForSponsorship');
await giteeHelper.signin(); await giteeHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync(); syncSvc.requestSync();
} }
if (!store.getters.isSponsor) { if (!store.getters.isSponsor) {

View File

@ -26,6 +26,7 @@ import store from '../store';
import DropdownMenu from './common/DropdownMenu'; import DropdownMenu from './common/DropdownMenu';
import publishSvc from '../services/publishSvc'; import publishSvc from '../services/publishSvc';
import giteeGistProvider from '../services/providers/giteeGistProvider'; import giteeGistProvider from '../services/providers/giteeGistProvider';
import gistProvider from '../services/providers/gistProvider';
export default { export default {
components: { components: {
@ -107,12 +108,15 @@ export default {
store.dispatch('notification/info', '登录主文档空间之后才可使用分享功能!'); store.dispatch('notification/info', '登录主文档空间之后才可使用分享功能!');
return; return;
} }
let giteeGistId = null; let tempGistId = null;
const filterLocations = this.publishLocations.filter(it => it.providerId === 'giteegist' && it.url && it.gistId); const isGithub = mainToken.providerId === 'githubAppData';
const gistProviderId = isGithub ? 'gist' : 'giteegist';
const filterLocations = this.publishLocations.filter(it => it.providerId === gistProviderId
&& it.url && it.gistId);
if (filterLocations.length > 0) { if (filterLocations.length > 0) {
giteeGistId = filterLocations[0].gistId; tempGistId = filterLocations[0].gistId;
} }
const location = giteeGistProvider.makeLocation( const location = (isGithub ? gistProvider : giteeGistProvider).makeLocation(
mainToken, mainToken,
`分享-${currentFile.name}`, `分享-${currentFile.name}`,
true, true,
@ -120,9 +124,10 @@ export default {
); );
location.templateId = 'styledHtmlWithTheme'; location.templateId = 'styledHtmlWithTheme';
location.fileId = currentFile.id; location.fileId = currentFile.id;
location.gistId = giteeGistId; location.gistId = tempGistId;
const { gistId } = await publishSvc.publishLocationAndStore(location); const { gistId } = await publishSvc.publishLocationAndStore(location);
const url = `${window.location.protocol}//${window.location.host}/share.html?id=${gistId}`; const sharePage = mainToken.providerId === 'githubAppData' ? 'gistshare.html' : 'share.html';
const url = `${window.location.protocol}//${window.location.host}/${sharePage}?id=${gistId}`;
await store.dispatch('modal/open', { type: 'shareHtml', name: currentFile.name, url }); await store.dispatch('modal/open', { type: 'shareHtml', name: currentFile.name, url });
} catch (err) { } catch (err) {
if (err) { if (err) {

View File

@ -8,7 +8,7 @@
</option> </option>
</select> </select>
</p> </p>
<p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Gitee</a> 以同步您的主文档空间</p> <p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Gitee</a> <a href="javascript:void(0)" @click="signinWithGithub">登录 GitHub</a> 以同步您的主文档空间</p>
<p v-else-if="loading">历史版本加载中</p> <p v-else-if="loading">历史版本加载中</p>
<p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> 没有历史版本.</p> <p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> 没有历史版本.</p>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else> <div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
@ -55,6 +55,7 @@ import EditorClassApplier from '../common/EditorClassApplier';
import PreviewClassApplier from '../common/PreviewClassApplier'; import PreviewClassApplier from '../common/PreviewClassApplier';
import utils from '../../services/utils'; import utils from '../../services/utils';
import giteeHelper from '../../services/providers/helpers/giteeHelper'; import giteeHelper from '../../services/providers/helpers/giteeHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc'; import badgeSvc from '../../services/badgeSvc';
@ -168,6 +169,16 @@ export default {
async signin() { async signin() {
try { try {
await giteeHelper.signin(); await giteeHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync();
} catch (e) {
// Cancel
}
},
async signinWithGithub() {
try {
await githubHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync(); syncSvc.requestSync();
} catch (e) { } catch (e) {
// Cancel // Cancel

View File

@ -14,6 +14,9 @@
<span v-if="currentWorkspace.providerId === 'giteeAppData'"> <span v-if="currentWorkspace.providerId === 'giteeAppData'">
<b>{{currentWorkspace.name}}</b> 与您的 Gitee 默认文档空间仓库同步 <b>{{currentWorkspace.name}}</b> 与您的 Gitee 默认文档空间仓库同步
</span> </span>
<span v-else-if="currentWorkspace.providerId === 'githubAppData'">
<b>{{currentWorkspace.name}}</b> 与您的 GitHub 默认文档空间仓库同步
</span>
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'"> <span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
<b>{{currentWorkspace.name}}</b> <a :href="workspaceLocationUrl" target="_blank">Google Drive 文件夹</a>同步 <b>{{currentWorkspace.name}}</b> <a :href="workspaceLocationUrl" target="_blank">Google Drive 文件夹</a>同步
</span> </span>
@ -45,6 +48,11 @@
<div>使用 Gitee 登录</div> <div>使用 Gitee 登录</div>
<span>同步您的主文档空间并解锁功能</span> <span>同步您的主文档空间并解锁功能</span>
</menu-entry> </menu-entry>
<menu-entry v-if="!loginToken" @click.native="signinWithGithub">
<icon-login slot="icon"></icon-login>
<div>使用 GitHub 登录</div>
<span>同步您的主文档空间并解锁功能</span>
</menu-entry>
<menu-entry @click.native="setPanel('workspaces')"> <menu-entry @click.native="setPanel('workspaces')">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>
<div><div class="menu-entry__label menu-entry__label--count" v-if="workspaceCount">{{workspaceCount}}</div> 文档空间</div> <div><div class="menu-entry__label menu-entry__label--count" v-if="workspaceCount">{{workspaceCount}}</div> 文档空间</div>
@ -142,6 +150,7 @@ import MenuEntry from './common/MenuEntry';
import providerRegistry from '../../services/providers/common/providerRegistry'; import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage'; import UserImage from '../UserImage';
import giteeHelper from '../../services/providers/helpers/giteeHelper'; import giteeHelper from '../../services/providers/helpers/giteeHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import userSvc from '../../services/userSvc'; import userSvc from '../../services/userSvc';
import store from '../../store'; import store from '../../store';
@ -194,6 +203,16 @@ export default {
async signin() { async signin() {
try { try {
await giteeHelper.signin(); await giteeHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync();
} catch (e) {
// Cancel
}
},
async signinWithGithub() {
try {
await githubHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync(); syncSvc.requestSync();
} catch (e) { } catch (e) {
// Cancel // Cancel

View File

@ -8,7 +8,8 @@
<hr> <hr>
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id"> <div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
<menu-entry :href="workspace.url" target="_blank"> <menu-entry :href="workspace.url" target="_blank">
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider> <icon-provider v-if="id === 'main' && !workspace.sub" slot="icon" :provider-id="'stackedit'"></icon-provider>
<icon-provider v-else slot="icon" :provider-id="workspace.providerId"></icon-provider>
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">当前</div>{{workspace.name}}</div> <div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">当前</div>{{workspace.name}}</div>
</menu-entry> </menu-entry>
</div> </div>

View File

@ -89,14 +89,14 @@ export default {
badgeSvc.addBadge('removePublishLocation'); badgeSvc.addBadge('removePublishLocation');
}, },
shareUrl(location) { shareUrl(location) {
if (location.providerId !== 'giteegist') { if (location.providerId !== 'giteegist' && location.providerId !== 'gist') {
return null; return null;
} }
if (!location.url) { if (!location.url || !location.gistId) {
return null; return null;
} }
const splitIndex = location.url.lastIndexOf('/'); const sharePage = location.providerId === 'gist' ? 'gistshare.html' : 'share.html';
return `${window.location.protocol}//${window.location.host}/share.html?id=${location.url.substr(splitIndex + 1)}`; return `${window.location.protocol}//${window.location.host}/${sharePage}?id=${location.gistId}`;
}, },
}, },
}; };

View File

@ -9,7 +9,8 @@
<div class="flex flex--column"> <div class="flex flex--column">
<div class="workspace-entry__header flex flex--row flex--align-center"> <div class="workspace-entry__header flex flex--row flex--align-center">
<div class="workspace-entry__icon"> <div class="workspace-entry__icon">
<icon-provider :provider-id="workspace.providerId"></icon-provider> <icon-provider v-if="id === 'main' && !workspace.sub" :provider-id="'stackedit'"></icon-provider>
<icon-provider v-else :provider-id="workspace.providerId"></icon-provider>
</div> </div>
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName"> <input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
<div class="workspace-entry__name" v-else>{{workspace.name}}</div> <div class="workspace-entry__name" v-else>{{workspace.name}}</div>
@ -17,7 +18,7 @@
<button class="workspace-entry__button button" @click="edit(id)" v-title="'编辑名称'"> <button class="workspace-entry__button button" @click="edit(id)" v-title="'编辑名称'">
<icon-pen></icon-pen> <icon-pen></icon-pen>
</button> </button>
<template v-if="workspace.providerId === 'giteeAppData' || workspace.providerId === 'githubWorkspace' <template v-if="workspace.providerId === 'giteeAppData' || workspace.providerId === 'githubAppData' || workspace.providerId === 'githubWorkspace'
|| workspace.providerId === 'giteeWorkspace' || workspace.providerId === 'gitlabWorkspace' || workspace.providerId === 'giteaWorkspace'"> || workspace.providerId === 'giteeWorkspace' || workspace.providerId === 'gitlabWorkspace' || workspace.providerId === 'giteaWorkspace'">
<button class="workspace-entry__button button" @click="stopAutoSync(id)" v-if="workspace.autoSync == undefined || workspace.autoSync" v-title="'关闭自动同步'"> <button class="workspace-entry__button button" @click="stopAutoSync(id)" v-if="workspace.autoSync == undefined || workspace.autoSync" v-title="'关闭自动同步'">
<icon-sync-auto></icon-sync-auto> <icon-sync-auto></icon-sync-auto>

View File

@ -68,6 +68,8 @@ shortcuts:
mod+shift+t: table mod+shift+t: table
mod+shift+u: ulist mod+shift+u: ulist
mod+shift+f: inlineformula mod+shift+f: inlineformula
# 切换编辑与预览模式
mod+shift+e: toggleeditor
'= = > space': '= = > space':
method: expand method: expand
params: params:

View File

@ -158,7 +158,19 @@ export default [
new Feature( new Feature(
'sponsor', 'sponsor',
'赞助', '赞助',
'使用 Google 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。(暂不支持赞助)', '使用 Gitee 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。(暂不支持赞助)',
),
],
),
new Feature(
'githubSignIn',
'登录',
'使用 Gitee 登录,同步您的主文档空间并解锁功能。',
[
new Feature(
'githubSyncMainWorkspace',
'主文档空间已同步',
'使用 GitHub 登录以将您的主文档空间与您的默认空间stackedit-app-data仓库数据同步。',
), ),
], ],
), ),

View File

@ -1,6 +1,7 @@
const simpleModal = (contentHtml, rejectText, resolveText) => ({ const simpleModal = (contentHtml, rejectText, resolveText, resolveArray) => ({
contentHtml: typeof contentHtml === 'function' ? contentHtml : () => contentHtml, contentHtml: typeof contentHtml === 'function' ? contentHtml : () => contentHtml,
rejectText, rejectText,
resolveArray,
resolveText, resolveText,
}); });
@ -65,21 +66,35 @@ export default {
'关闭窗口', '关闭窗口',
), ),
shareHtmlPre: simpleModal( shareHtmlPre: simpleModal(
config => `<p>将给文档 "${config.name}" 创建分享链接,创建后将会把文档公开发布到GiteeGist中。您确定吗</p>`, config => `<p>将给文档 "${config.name}" 创建分享链接,创建后将会把文档公开发布到默认空间账号的Gist中。您确定吗</p>`,
'取消', '取消',
'确认分享', '确认分享',
), ),
signInForComment: simpleModal( signInForComment: simpleModal(
`<p>您必须使用 Google 登录才能开始评论。</p> `<p>您必须使用 Gitee或GitHub 登录默认文档空间后才能开始评论。</p>
<div class="modal__info"><b>注意:</b> </div>`, <div class="modal__info"><b>注意:</b> </div>`,
'取消', '取消',
'确认登录', '',
[{
text: 'Gitee登录',
value: 'gitee',
}, {
text: 'GitHub登录',
value: 'github',
}],
), ),
signInForSponsorship: simpleModal( signInForSponsorship: simpleModal(
`<p>您必须使用 Google 登录才能赞助。</p> `<p>您必须使用 Gitee或GitHub 登录才能赞助。</p>
<div class="modal__info"><b>注意:</b> </div>`, <div class="modal__info"><b>注意:</b> </div>`,
'取消', '取消',
'确认登录', '',
[{
text: 'Gitee登录',
value: 'gitee',
}, {
text: 'GitHub登录',
value: 'github',
}],
), ),
sponsorOnly: simpleModal( sponsorOnly: simpleModal(
'<p>此功能仅限于赞助商,因为它依赖于服务器资源。</p>', '<p>此功能仅限于赞助商,因为它依赖于服务器资源。</p>',

View File

@ -15,6 +15,7 @@ export default {
return 'google-drive'; return 'google-drive';
case 'googlePhotos': case 'googlePhotos':
return 'google-photos'; return 'google-photos';
case 'githubAppData':
case 'githubWorkspace': case 'githubWorkspace':
return 'github'; return 'github';
case 'gist': case 'gist':
@ -31,6 +32,8 @@ export default {
case 'giteeWorkspace': case 'giteeWorkspace':
case 'giteegist': case 'giteegist':
return 'gitee'; return 'gitee';
case 'stackedit':
return 'stackedit';
default: default:
return this.providerId; return this.providerId;
} }

View File

@ -3,8 +3,8 @@ import store from '../../store';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
// Skip shortcuts if modal is open or editor is hidden // Skip shortcuts if modal is open
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] || !store.getters['content/isCurrentEditable']; Mousetrap.prototype.stopCallback = () => store.getters['modal/config'];
const pagedownHandler = name => () => { const pagedownHandler = name => () => {
editorSvc.pagedownEditor.uiManager.doClick(name); editorSvc.pagedownEditor.uiManager.doClick(name);
@ -20,6 +20,14 @@ const findReplaceOpener = type => () => {
return true; return true;
}; };
const toggleEditor = () => () => {
store.dispatch('data/toggleEditor', !store.getters['data/layoutSettings'].showEditor);
return true;
};
// 非编辑模式下支持的快捷键
const noEditableShortcutMethods = ['toggleeditor'];
const methods = { const methods = {
bold: pagedownHandler('bold'), bold: pagedownHandler('bold'),
italic: pagedownHandler('italic'), italic: pagedownHandler('italic'),
@ -36,6 +44,7 @@ const methods = {
inline: pagedownHandler('heading'), inline: pagedownHandler('heading'),
hr: pagedownHandler('hr'), hr: pagedownHandler('hr'),
inlineformula: pagedownHandler('inlineformula'), inlineformula: pagedownHandler('inlineformula'),
toggleeditor: toggleEditor(),
sync() { sync() {
if (syncSvc.isSyncPossible()) { if (syncSvc.isSyncPossible()) {
syncSvc.requestSync(); syncSvc.requestSync();
@ -80,7 +89,10 @@ store.watch(
} }
if (Object.prototype.hasOwnProperty.call(methods, method)) { if (Object.prototype.hasOwnProperty.call(methods, method)) {
try { try {
Mousetrap.bind(`${key}`, () => !methods[method].apply(null, params)); // editor is editable or 一些非编辑模式下支持的快捷键
if (store.getters['content/isCurrentEditable'] || noEditableShortcutMethods.indexOf(method) !== -1) {
Mousetrap.bind(`${key}`, () => !methods[method].apply(null, params));
}
} catch (e) { } catch (e) {
// Ignore // Ignore
} }

View File

@ -0,0 +1,292 @@
import store from '../../store';
import githubHelper from './helpers/githubHelper';
import Provider from './common/Provider';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
import userSvc from '../userSvc';
const appDataRepo = 'stackedit-app-data';
const appDataBranch = 'master';
export default new Provider({
id: 'githubAppData',
name: 'Gitee应用数据',
getToken() {
return store.getters['workspace/syncToken'];
},
getWorkspaceParams() {
// No param as it's the main workspace
return {};
},
getWorkspaceLocationUrl() {
// No direct link to app data
return null;
},
getSyncDataUrl() {
// No direct link to app data
return null;
},
getSyncDataDescription({ id }) {
return id;
},
async initWorkspace() {
// Nothing much to do since the main workspace isn't necessarily synchronized
// Return the main workspace
return store.getters['workspace/workspacesById'].main;
},
getChanges() {
const token = this.getToken();
return githubHelper.getTree({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
});
},
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 githubHelper.uploadFile({
owner: syncToken.name,
repo: appDataRepo,
branch: appDataBranch,
token: syncToken,
path: syncData.id,
content: '',
sha: gitWorkspaceSvc.shaByPath[syncData.id],
commitMessage: item.commitMessage,
});
// Return sync data to save
return { syncData };
},
async removeWorkspaceItem({ syncData }) {
if (gitWorkspaceSvc.shaByPath[syncData.id]) {
const syncToken = store.getters['workspace/syncToken'];
await githubHelper.removeFile({
owner: syncToken.name,
repo: appDataRepo,
branch: appDataBranch,
token: syncToken,
path: syncData.id,
sha: gitWorkspaceSvc.shaByPath[syncData.id],
});
}
},
async downloadWorkspaceContent({
token,
contentId,
contentSyncData,
fileSyncData,
}) {
const { sha, data } = await githubHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path: fileSyncData.id,
});
gitWorkspaceSvc.shaByPath[fileSyncData.id] = sha;
const content = Provider.parseContent(data, contentId);
return {
content,
contentSyncData: {
...contentSyncData,
hash: content.hash,
sha,
},
};
},
async downloadFile({ token, path }) {
const { sha, data } = await githubHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path,
isImg: true,
});
return {
content: data,
sha,
};
},
async downloadWorkspaceData({ token, syncData }) {
if (!syncData) {
return {};
}
const path = `.stackedit-data/${syncData.id}.json`;
// const path = store.getters.gitPathsByItemId[syncData.id];
// const path = syncData.id;
const { sha, data } = await githubHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path,
});
if (!sha) {
return {};
}
gitWorkspaceSvc.shaByPath[path] = sha;
const item = JSON.parse(data);
return {
item,
syncData: {
...syncData,
hash: item.hash,
sha,
type: 'data',
},
};
},
async uploadWorkspaceContent({
token,
content,
file,
commitMessage,
}) {
const isImg = file.type === 'img';
const path = !isImg ? store.getters.gitPathsByItemId[file.id] : file.path;
const res = await githubHelper.uploadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path,
content: !isImg ? Provider.serializeContent(content) : file.content,
sha: gitWorkspaceSvc.shaByPath[!isImg ? path : file.path],
isImg,
commitMessage,
});
if (isImg) {
return {
sha: res.content.sha,
};
}
// Return new sync data
return {
contentSyncData: {
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,
syncData,
}) {
const path = `.stackedit-data/${item.id}.json`;
// const path = store.getters.gitPathsByItemId[item.id];
// const path = syncData.id;
const res = await githubHelper.uploadFile({
token,
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
path,
content: JSON.stringify(item),
sha: gitWorkspaceSvc.shaByPath[path],
});
return {
syncData: {
...syncData,
type: item.type,
hash: item.hash,
data: item.data,
sha: res.content.sha,
},
};
},
async listFileRevisions({ token, fileSyncDataId }) {
const { owner, repo, branch } = {
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
};
const entries = await githubHelper.getCommits({
token,
owner,
repo,
sha: branch,
path: 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 = `${githubHelper.subPrefix}:${user.login}`;
if (user.avatar_url && user.avatar_url.endsWith('.png') && !user.avatar_url.endsWith('no_portrait.png')) {
user.avatar_url = `${user.avatar_url}!avatar60`;
}
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,
message: commit.message,
created: new Date(date).getTime(),
};
});
},
async loadFileRevision() {
// Revisions are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
fileSyncDataId,
revisionId,
}) {
const { data } = await githubHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: revisionId,
token,
path: fileSyncDataId,
});
return Provider.parseContent(data, contentId);
},
getFilePathUrl(path) {
const token = this.getToken();
if (!token) {
return null;
}
return `https://github.com/${token.name}/${appDataRepo}/blob/${appDataBranch}${path}`;
},
});

View File

@ -147,6 +147,7 @@ export default {
sub: `${user.login}`, sub: `${user.login}`,
}; };
if (isMain) { if (isMain) {
token.providerId = 'giteeAppData';
// 检查 stackedit-app-data 仓库是否已经存在 如果不存在则创建该仓库 // 检查 stackedit-app-data 仓库是否已经存在 如果不存在则创建该仓库
await this.checkAndCreateRepo(token); await this.checkAndCreateRepo(token);
} }

View File

@ -6,6 +6,8 @@ import badgeSvc from '../../badgeSvc';
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist']; const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
const appDataRepo = 'stackedit-app-data';
const request = (token, options) => networkSvc.request({ const request = (token, options) => networkSvc.request({
...options, ...options,
headers: { headers: {
@ -62,7 +64,7 @@ export default {
/** /**
* https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/ * https://developer.github.com/apps/building-oauth-apps/authorization-options-for-oauth-apps/
*/ */
async startOauth2(scopes, sub = null, silent = false) { async startOauth2(scopes, sub = null, silent = false, isMain) {
await networkSvc.getServerConf(); await networkSvc.getServerConf();
const clientId = store.getters['data/serverConf'].githubClientId; const clientId = store.getters['data/serverConf'].githubClientId;
@ -110,16 +112,26 @@ export default {
const token = { const token = {
scopes, scopes,
accessToken, accessToken,
// 主文档空间的登录 标识登录
isLogin: !!isMain || (oldToken && !!oldToken.isLogin),
name: user.login, name: user.login,
sub: `${user.id}`, sub: `${user.id}`,
imgStorages: oldToken && oldToken.imgStorages, imgStorages: oldToken && oldToken.imgStorages,
repoFullAccess: scopes.includes('repo'), repoFullAccess: scopes.includes('repo'),
}; };
if (isMain) {
token.providerId = 'githubAppData';
// check stackedit-app-data repo exist?
await this.checkAndCreateRepo(token);
}
// Add token to github tokens // Add token to github tokens
store.dispatch('data/addGithubToken', token); store.dispatch('data/addGithubToken', token);
return token; return token;
}, },
signin() {
return this.startOauth2(['repo', 'gist'], null, false, true);
},
async addAccount(repoFullAccess = false) { async addAccount(repoFullAccess = false) {
const token = await this.startOauth2(getScopes({ repoFullAccess })); const token = await this.startOauth2(getScopes({ repoFullAccess }));
badgeSvc.addBadge('addGitHubAccount'); badgeSvc.addBadge('addGitHubAccount');
@ -148,6 +160,30 @@ export default {
return tree; return tree;
}, },
async checkAndCreateRepo(token) {
const url = `https://api.github.com/repos/${encodeURIComponent(token.name)}/${encodeURIComponent(appDataRepo)}`;
try {
await request(token, { url });
} catch (err) {
// create
if (err.status === 404) {
await request(token, {
method: 'POST',
url: 'https://api.github.com/repos/mafgwo/stackeditplus-appdata-template/generate',
body: {
owner: token.name,
name: appDataRepo,
description: 'StackEdit中文版默认空间.',
include_all_branches: false,
private: true,
},
});
} else {
throw err;
}
}
},
/** /**
* https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
*/ */

View File

@ -6,6 +6,7 @@ import diffUtils from './diffUtils';
import networkSvc from './networkSvc'; import networkSvc from './networkSvc';
import providerRegistry from './providers/common/providerRegistry'; import providerRegistry from './providers/common/providerRegistry';
import giteeAppDataProvider from './providers/giteeAppDataProvider'; import giteeAppDataProvider from './providers/giteeAppDataProvider';
import githubAppDataProvider from './providers/githubAppDataProvider';
import './providers/couchdbWorkspaceProvider'; import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider'; import './providers/githubWorkspaceProvider';
import './providers/giteeWorkspaceProvider'; import './providers/giteeWorkspaceProvider';
@ -830,7 +831,7 @@ const syncWorkspace = async (skipContents = false) => {
} }
if (workspace.id === 'main') { if (workspace.id === 'main') {
badgeSvc.addBadge('syncMainWorkspace'); badgeSvc.addBadge(workspace.providerId === 'giteeAppData' ? 'syncMainWorkspace' : 'githubSyncMainWorkspace');
} }
} catch (err) { } catch (err) {
if (err && err.message === 'TOO_LATE') { if (err && err.message === 'TOO_LATE') {
@ -969,6 +970,15 @@ const requestSync = (addTriggerSyncBadge = false) => {
}); });
}; };
const afterSignIn = async () => {
if (store.getters['workspace/currentWorkspace'].id === 'main' && workspaceProvider) {
const mainToken = store.getters['workspace/mainWorkspaceToken'];
// Try to find a suitable workspace sync provider
workspaceProvider = mainToken.providerId === 'githubAppData' ? githubAppDataProvider : giteeAppDataProvider;
await workspaceProvider.initWorkspace();
}
};
export default { export default {
async init() { async init() {
// Load workspaces and tokens from localStorage // Load workspaces and tokens from localStorage
@ -980,10 +990,11 @@ export default {
await actionProvider.initAction(); await actionProvider.initAction();
} }
const mainToken = store.getters['workspace/mainWorkspaceToken'];
// Try to find a suitable workspace sync provider // Try to find a suitable workspace sync provider
workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId]; workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId];
if (!workspaceProvider || !workspaceProvider.initWorkspace) { if (!workspaceProvider || !workspaceProvider.initWorkspace) {
workspaceProvider = giteeAppDataProvider; workspaceProvider = mainToken && mainToken.providerId === 'githubAppData' ? githubAppDataProvider : giteeAppDataProvider;
} }
const workspace = await workspaceProvider.initWorkspace(); const workspace = await workspaceProvider.initWorkspace();
// Fix the URL hash // Fix the URL hash
@ -1041,6 +1052,7 @@ export default {
}, 5000); }, 5000);
} }
}, },
afterSignIn,
syncImg, syncImg,
isSyncPossible, isSyncPossible,
requestSync, requestSync,

View File

@ -1,5 +1,6 @@
import utils from '../services/utils'; import utils from '../services/utils';
import giteeHelper from '../services/providers/helpers/giteeHelper'; import giteeHelper from '../services/providers/helpers/giteeHelper';
import githubHelper from '../services/providers/helpers/githubHelper';
import syncSvc from '../services/syncSvc'; import syncSvc from '../services/syncSvc';
const idShifter = offset => (state, getters) => { const idShifter = offset => (state, getters) => {
@ -136,8 +137,13 @@ export default {
const loginToken = rootGetters['workspace/loginToken']; const loginToken = rootGetters['workspace/loginToken'];
if (!loginToken) { if (!loginToken) {
try { try {
await dispatch('modal/open', 'signInForComment', { root: true }); const signInWhere = await dispatch('modal/open', 'signInForComment', { root: true });
await giteeHelper.signin(); if (signInWhere === 'github') {
await githubHelper.signin();
} else {
await giteeHelper.signin();
}
await syncSvc.afterSignIn();
syncSvc.requestSync(); syncSvc.requestSync();
await dispatch('createNewDiscussion', selection); await dispatch('createNewDiscussion', selection);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }

View File

@ -22,7 +22,7 @@ export default {
Object.entries(rootGetters['data/workspaces']).forEach(([id, workspace]) => { Object.entries(rootGetters['data/workspaces']).forEach(([id, workspace]) => {
const sanitizedWorkspace = { const sanitizedWorkspace = {
id, id,
providerId: 'giteeAppData', providerId: (mainWorkspaceToken && mainWorkspaceToken.providerId) || 'giteeAppData',
sub: mainWorkspaceToken && mainWorkspaceToken.sub, sub: mainWorkspaceToken && mainWorkspaceToken.sub,
...workspace, ...workspace,
}; };
@ -47,17 +47,19 @@ export default {
|| currentWorkspace.providerId === 'giteeWorkspace' || currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace' || currentWorkspace.providerId === 'gitlabWorkspace'
|| currentWorkspace.providerId === 'giteaWorkspace' || currentWorkspace.providerId === 'giteaWorkspace'
|| currentWorkspace.providerId === 'giteeAppData', || currentWorkspace.providerId === 'giteeAppData'
|| currentWorkspace.providerId === 'githubAppData',
currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) => currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace' currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'giteeWorkspace' || currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace' || currentWorkspace.providerId === 'gitlabWorkspace'
|| currentWorkspace.providerId === 'giteaWorkspace' || currentWorkspace.providerId === 'giteaWorkspace'
|| currentWorkspace.providerId === 'giteeAppData', || currentWorkspace.providerId === 'giteeAppData'
|| currentWorkspace.providerId === 'githubAppData',
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`, lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`, lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
mainWorkspaceToken: (state, getters, rootState, rootGetters) => mainWorkspaceToken: (state, getters, rootState, rootGetters) =>
utils.someResult(Object.values(rootGetters['data/giteeTokensBySub']), (token) => { utils.someResult([...Object.values(rootGetters['data/giteeTokensBySub']), ...Object.values(rootGetters['data/githubTokensBySub'])], (token) => {
if (token.isLogin) { if (token.isLogin) {
return token; return token;
} }
@ -85,8 +87,10 @@ export default {
switch (currentWorkspace.providerId) { switch (currentWorkspace.providerId) {
case 'googleDriveWorkspace': case 'googleDriveWorkspace':
return 'google'; return 'google';
case 'githubAppData':
case 'githubWorkspace': case 'githubWorkspace':
return 'github'; return 'github';
case 'giteeAppData':
case 'giteeWorkspace': case 'giteeWorkspace':
default: default:
return 'gitee'; return 'gitee';

View File

@ -0,0 +1,165 @@
<!DOCTYPE html>
<html>
<head>
<title>文章分享 - StackEdit中文版</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="canonical" href="https://stackedit.cn/">
<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">
<meta name="keywords" content="Markdown编辑器,StackEdit中文版,StackEdit汉化版,StackEdit,在线Markdown,笔记利器,Markdown笔记">
<meta name="description"
content="支持直接将码云Gitee、GitHub、Gitea等仓库作为笔记存储仓库且支持拖拽/粘贴上传图片并且可以直接在页面编辑同步和管理的Markdown编辑器。">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="baidu-site-verification" content="code-tGpn2BT069" />
<meta name="msvalidate.01" content="90A9558158543277BD284CFA054E7F5B" />
<link rel="stylesheet" href="style.css">
<style>
.share-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #383c4a;
color: #fff;
padding: 10px;
box-sizing: border-box;
z-index: 99999;
}
.share-header .logo {
margin: 0 0 -8px 0;
}
.share-header nav {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.share-header nav ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
}
.share-header nav li {
margin: 0 10px;
}
.share-header nav a {
color: #fff;
text-decoration: none;
}
.share-header nav a:hover {
text-decoration: underline;
}
.share-content {
transform: translateY(50px);
height: 100vh;
}
</style>
<script type="text/javascript">
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}
function appendTagHtml(newdoc, tagName, targetParentEle) {
const tags = newdoc.getElementsByTagName(tagName);
if (!tags) {
return;
}
for (let i = 0; i < tags.length; i++) {
targetParentEle.append(tags[i]);
}
}
window.onload = function() {
const xhr = new XMLHttpRequest();
const gistId = getQueryString('id');
let accessToken = null;
const tokens = window.localStorage.getItem('data/tokens');
if (tokens) {
const tokensObj = JSON.parse(tokens);
if (tokensObj.data && tokensObj.data.github) {
const tokenArr = Object.keys(tokensObj.data.github).map(it => tokensObj.data.github[it]).filter(it => it && it.isLogin);
if (tokenArr.length > 0) {
accessToken = tokenArr[0].accessToken;
}
}
}
const url = `https://api.github.com/gists/${gistId}`;
xhr.open('GET', url);
if (accessToken) {
xhr.setRequestHeader('Authorization', `Bearer ${accessToken}`);
}
xhr.onload = function() {
if (xhr.status === 200) {
const newdoc = document.implementation.createHTMLDocument("");
const body = JSON.parse(xhr.responseText);
for (let key in body.files) {
newdoc.documentElement.innerHTML = body.files[key].content;
}
const currHead = document.head;
// head
appendTagHtml(newdoc, 'style', currHead);
// title
document.title = newdoc.title + ' - StackEdit中文版';
// 内容
const shareContent = document.getElementsByClassName('share-content')[0];
shareContent.innerHTML = newdoc.body.innerHTML;
document.body.className = newdoc.body.className;
} else if (xhr.status === 403) {
const rateLimit = xhr.responseText && xhr.responseText.indexOf('Rate Limit') >= 0;
const appUri = `${window.location.protocol}//${window.location.host}/app`;
document.getElementById('div_info').innerHTML = `${rateLimit ? "请求太过频繁" : "无权限访问"}请使用GitHub登录 <a href="${appUri}" target="_brank">主文档空间</a> 后再刷新此页面!`;
} else {
console.error('An error occurred: ' + xhr.status);
document.getElementById('div_info').innerHTML = `分享内容获取失败或已失效请使用GitHub登录 <a href="${appUri}" target="_brank">主文档空间</a> 后再刷新此页面!`;
}
};
xhr.send();
}
</script>
</head>
<body>
<div class="share-header">
<nav>
<a class="logo" href="https://stackedit.cn" target="_blank">
<img src="static/landing/logo.svg" height="30px"/>
</a>
<ul>
<li><a href="https://stackedit.cn" target="_blank">首页</a></li>
<li><a href="https://stackedit.cn/app" target="_blank">写笔记</a></li>
</ul>
</nav>
</div>
<div class="share-content stackedit">
<div id="div_info" style="text-align: center; height: 600px;">文章加载中......</div>
</div>
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<!-- built files will be auto injected -->
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?20a1e7a201b42702c49074c87a1f1035";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>

View File

@ -36,7 +36,7 @@ style.innerHTML = "/** activeblue 灵动蓝\n \
top: 0;\n \ top: 0;\n \
width: 60px;\n \ width: 60px;\n \
height: 60px;\n \ height: 60px;\n \
background: url(https://my-wechat.mdnice.com/ape_blue.svg);\n \ background: url(https://imgs.qicoder.com/stackedit/ape_blue.svg);\n \
background-size: 100% 100%;\n \ background-size: 100% 100%;\n \
opacity: .12;\n \ opacity: .12;\n \
}\n \ }\n \

View File

@ -100,7 +100,7 @@ style.innerHTML = "/* 草原绿 caoyuangreen\n \
width:30px;\n \ width:30px;\n \
height:30px;\n \ height:30px;\n \
display:block;\n \ display:block;\n \
background-image:url(https://files.mdnice.com/grass-green.png);\n \ background-image:url(https://imgs.qicoder.com/stackedit/grass-green.png);\n \
background-position:center;\n \ background-position:center;\n \
background-size:30px;\n \ background-size:30px;\n \
margin:auto;\n \ margin:auto;\n \