mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-11-16 11:42:23 +08:00
主文档空间支持GitHub登录
This commit is contained in:
parent
97b8d3c288
commit
39167fb193
|
@ -78,10 +78,10 @@ StackEdit中文版
|
|||
- 支持分享文档(2023-03-30)
|
||||
- 支持ChatGPT生成内容(2023-04-10)
|
||||
- GitLab授权接口调整(2023-08-26)
|
||||
- 主文档空间支持GitHub登录(2023-10-19)
|
||||
|
||||
## 国外开源版本弊端:
|
||||
- 作者已经不维护了
|
||||
- Github授权登录存在问题
|
||||
- 作者已经不维护了或很少维护了
|
||||
- 不支持国内常用Gitee
|
||||
- 强依赖GoogleDrive,而Google Drive在国内不能正常访问
|
||||
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stackedit",
|
||||
"version": "5.15.20",
|
||||
"version": "5.15.21",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stackedit",
|
||||
"version": "5.15.20",
|
||||
"version": "5.15.21",
|
||||
"description": "免费, 开源, 功能齐全的 Markdown 编辑器",
|
||||
"author": "Benoit Schweblin, 豆萁",
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -72,6 +72,7 @@ module.exports = (app) => {
|
|||
}));
|
||||
// Serve 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
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<div class="modal__button-bar">
|
||||
<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 v-for="(item, idx) in (simpleModal.resolveArray || [])" class="button button--resolve" @click="config.resolve(item.value)">{{item.text}}</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</div>
|
||||
|
@ -187,6 +188,7 @@ export default {
|
|||
// User has to sign in
|
||||
await store.dispatch('modal/open', 'signInForSponsorship');
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
}
|
||||
if (!store.getters.isSponsor) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import store from '../store';
|
|||
import DropdownMenu from './common/DropdownMenu';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import giteeGistProvider from '../services/providers/giteeGistProvider';
|
||||
import gistProvider from '../services/providers/gistProvider';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -107,12 +108,15 @@ export default {
|
|||
store.dispatch('notification/info', '登录主文档空间之后才可使用分享功能!');
|
||||
return;
|
||||
}
|
||||
let giteeGistId = null;
|
||||
const filterLocations = this.publishLocations.filter(it => it.providerId === 'giteegist' && it.url && it.gistId);
|
||||
let tempGistId = null;
|
||||
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) {
|
||||
giteeGistId = filterLocations[0].gistId;
|
||||
tempGistId = filterLocations[0].gistId;
|
||||
}
|
||||
const location = giteeGistProvider.makeLocation(
|
||||
const location = (isGithub ? gistProvider : giteeGistProvider).makeLocation(
|
||||
mainToken,
|
||||
`分享-${currentFile.name}`,
|
||||
true,
|
||||
|
@ -120,9 +124,10 @@ export default {
|
|||
);
|
||||
location.templateId = 'styledHtmlWithTheme';
|
||||
location.fileId = currentFile.id;
|
||||
location.gistId = giteeGistId;
|
||||
location.gistId = tempGistId;
|
||||
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 });
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</option>
|
||||
</select>
|
||||
</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="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> 没有历史版本.</p>
|
||||
<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 utils from '../../services/utils';
|
||||
import giteeHelper from '../../services/providers/helpers/giteeHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
@ -168,6 +169,16 @@ export default {
|
|||
async signin() {
|
||||
try {
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
async signinWithGithub() {
|
||||
try {
|
||||
await githubHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
<span v-if="currentWorkspace.providerId === 'giteeAppData'">
|
||||
<b>{{currentWorkspace.name}}</b> 与您的 Gitee 默认文档空间仓库同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'githubAppData'">
|
||||
<b>{{currentWorkspace.name}}</b> 与您的 GitHub 默认文档空间仓库同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">Google Drive 文件夹</a>同步。
|
||||
</span>
|
||||
|
@ -45,6 +48,11 @@
|
|||
<div>使用 Gitee 登录</div>
|
||||
<span>同步您的主文档空间并解锁功能。</span>
|
||||
</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')">
|
||||
<icon-database slot="icon"></icon-database>
|
||||
<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 UserImage from '../UserImage';
|
||||
import giteeHelper from '../../services/providers/helpers/giteeHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import userSvc from '../../services/userSvc';
|
||||
import store from '../../store';
|
||||
|
@ -194,6 +203,16 @@ export default {
|
|||
async signin() {
|
||||
try {
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
async signinWithGithub() {
|
||||
try {
|
||||
await githubHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
<hr>
|
||||
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
|
||||
<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>
|
||||
</menu-entry>
|
||||
</div>
|
||||
|
|
|
@ -89,14 +89,14 @@ export default {
|
|||
badgeSvc.addBadge('removePublishLocation');
|
||||
},
|
||||
shareUrl(location) {
|
||||
if (location.providerId !== 'giteegist') {
|
||||
if (location.providerId !== 'giteegist' && location.providerId !== 'gist') {
|
||||
return null;
|
||||
}
|
||||
if (!location.url) {
|
||||
if (!location.url || !location.gistId) {
|
||||
return null;
|
||||
}
|
||||
const splitIndex = location.url.lastIndexOf('/');
|
||||
return `${window.location.protocol}//${window.location.host}/share.html?id=${location.url.substr(splitIndex + 1)}`;
|
||||
const sharePage = location.providerId === 'gist' ? 'gistshare.html' : 'share.html';
|
||||
return `${window.location.protocol}//${window.location.host}/${sharePage}?id=${location.gistId}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
<div class="flex flex--column">
|
||||
<div class="workspace-entry__header flex flex--row flex--align-center">
|
||||
<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>
|
||||
<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>
|
||||
|
@ -17,7 +18,7 @@
|
|||
<button class="workspace-entry__button button" @click="edit(id)" v-title="'编辑名称'">
|
||||
<icon-pen></icon-pen>
|
||||
</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'">
|
||||
<button class="workspace-entry__button button" @click="stopAutoSync(id)" v-if="workspace.autoSync == undefined || workspace.autoSync" v-title="'关闭自动同步'">
|
||||
<icon-sync-auto></icon-sync-auto>
|
||||
|
|
|
@ -68,6 +68,8 @@ shortcuts:
|
|||
mod+shift+t: table
|
||||
mod+shift+u: ulist
|
||||
mod+shift+f: inlineformula
|
||||
# 切换编辑与预览模式
|
||||
mod+shift+e: toggleeditor
|
||||
'= = > space':
|
||||
method: expand
|
||||
params:
|
||||
|
|
|
@ -158,7 +158,19 @@ export default [
|
|||
new Feature(
|
||||
'sponsor',
|
||||
'赞助',
|
||||
'使用 Google 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。(暂不支持赞助)',
|
||||
'使用 Gitee 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。(暂不支持赞助)',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'githubSignIn',
|
||||
'登录',
|
||||
'使用 Gitee 登录,同步您的主文档空间并解锁功能。',
|
||||
[
|
||||
new Feature(
|
||||
'githubSyncMainWorkspace',
|
||||
'主文档空间已同步',
|
||||
'使用 GitHub 登录以将您的主文档空间与您的默认空间stackedit-app-data仓库数据同步。',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const simpleModal = (contentHtml, rejectText, resolveText) => ({
|
||||
const simpleModal = (contentHtml, rejectText, resolveText, resolveArray) => ({
|
||||
contentHtml: typeof contentHtml === 'function' ? contentHtml : () => contentHtml,
|
||||
rejectText,
|
||||
resolveArray,
|
||||
resolveText,
|
||||
});
|
||||
|
||||
|
@ -65,21 +66,35 @@ export default {
|
|||
'关闭窗口',
|
||||
),
|
||||
shareHtmlPre: simpleModal(
|
||||
config => `<p>将给文档 "${config.name}" 创建分享链接,创建后将会把文档公开发布到GiteeGist中。您确定吗?</p>`,
|
||||
config => `<p>将给文档 "${config.name}" 创建分享链接,创建后将会把文档公开发布到默认空间账号的Gist中。您确定吗?</p>`,
|
||||
'取消',
|
||||
'确认分享',
|
||||
),
|
||||
signInForComment: simpleModal(
|
||||
`<p>您必须使用 Google 登录才能开始评论。</p>
|
||||
`<p>您必须使用 Gitee或GitHub 登录默认文档空间后才能开始评论。</p>
|
||||
<div class="modal__info"><b>注意:</b> 这将同步您的主文档空间。</div>`,
|
||||
'取消',
|
||||
'确认登录',
|
||||
'',
|
||||
[{
|
||||
text: 'Gitee登录',
|
||||
value: 'gitee',
|
||||
}, {
|
||||
text: 'GitHub登录',
|
||||
value: 'github',
|
||||
}],
|
||||
),
|
||||
signInForSponsorship: simpleModal(
|
||||
`<p>您必须使用 Google 登录才能赞助。</p>
|
||||
`<p>您必须使用 Gitee或GitHub 登录才能赞助。</p>
|
||||
<div class="modal__info"><b>注意:</b> 这将同步您的主文档空间。</div>`,
|
||||
'取消',
|
||||
'确认登录',
|
||||
'',
|
||||
[{
|
||||
text: 'Gitee登录',
|
||||
value: 'gitee',
|
||||
}, {
|
||||
text: 'GitHub登录',
|
||||
value: 'github',
|
||||
}],
|
||||
),
|
||||
sponsorOnly: simpleModal(
|
||||
'<p>此功能仅限于赞助商,因为它依赖于服务器资源。</p>',
|
||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
|||
return 'google-drive';
|
||||
case 'googlePhotos':
|
||||
return 'google-photos';
|
||||
case 'githubAppData':
|
||||
case 'githubWorkspace':
|
||||
return 'github';
|
||||
case 'gist':
|
||||
|
@ -31,6 +32,8 @@ export default {
|
|||
case 'giteeWorkspace':
|
||||
case 'giteegist':
|
||||
return 'gitee';
|
||||
case 'stackedit':
|
||||
return 'stackedit';
|
||||
default:
|
||||
return this.providerId;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import store from '../../store';
|
|||
import editorSvc from '../../services/editorSvc';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
// Skip shortcuts if modal is open or editor is hidden
|
||||
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] || !store.getters['content/isCurrentEditable'];
|
||||
// Skip shortcuts if modal is open
|
||||
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'];
|
||||
|
||||
const pagedownHandler = name => () => {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
|
@ -20,6 +20,14 @@ const findReplaceOpener = type => () => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const toggleEditor = () => () => {
|
||||
store.dispatch('data/toggleEditor', !store.getters['data/layoutSettings'].showEditor);
|
||||
return true;
|
||||
};
|
||||
|
||||
// 非编辑模式下支持的快捷键
|
||||
const noEditableShortcutMethods = ['toggleeditor'];
|
||||
|
||||
const methods = {
|
||||
bold: pagedownHandler('bold'),
|
||||
italic: pagedownHandler('italic'),
|
||||
|
@ -36,6 +44,7 @@ const methods = {
|
|||
inline: pagedownHandler('heading'),
|
||||
hr: pagedownHandler('hr'),
|
||||
inlineformula: pagedownHandler('inlineformula'),
|
||||
toggleeditor: toggleEditor(),
|
||||
sync() {
|
||||
if (syncSvc.isSyncPossible()) {
|
||||
syncSvc.requestSync();
|
||||
|
@ -80,7 +89,10 @@ store.watch(
|
|||
}
|
||||
if (Object.prototype.hasOwnProperty.call(methods, method)) {
|
||||
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) {
|
||||
// Ignore
|
||||
}
|
||||
|
|
292
src/services/providers/githubAppDataProvider.js
Normal file
292
src/services/providers/githubAppDataProvider.js
Normal 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}`;
|
||||
},
|
||||
});
|
|
@ -147,6 +147,7 @@ export default {
|
|||
sub: `${user.login}`,
|
||||
};
|
||||
if (isMain) {
|
||||
token.providerId = 'giteeAppData';
|
||||
// 检查 stackedit-app-data 仓库是否已经存在 如果不存在则创建该仓库
|
||||
await this.checkAndCreateRepo(token);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import badgeSvc from '../../badgeSvc';
|
|||
|
||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||
|
||||
const appDataRepo = 'stackedit-app-data';
|
||||
|
||||
const request = (token, options) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
|
@ -62,7 +64,7 @@ export default {
|
|||
/**
|
||||
* 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();
|
||||
const clientId = store.getters['data/serverConf'].githubClientId;
|
||||
|
||||
|
@ -110,16 +112,26 @@ export default {
|
|||
const token = {
|
||||
scopes,
|
||||
accessToken,
|
||||
// 主文档空间的登录 标识登录
|
||||
isLogin: !!isMain || (oldToken && !!oldToken.isLogin),
|
||||
name: user.login,
|
||||
sub: `${user.id}`,
|
||||
imgStorages: oldToken && oldToken.imgStorages,
|
||||
repoFullAccess: scopes.includes('repo'),
|
||||
};
|
||||
|
||||
if (isMain) {
|
||||
token.providerId = 'githubAppData';
|
||||
// check stackedit-app-data repo exist?
|
||||
await this.checkAndCreateRepo(token);
|
||||
}
|
||||
// Add token to github tokens
|
||||
store.dispatch('data/addGithubToken', token);
|
||||
return token;
|
||||
},
|
||||
signin() {
|
||||
return this.startOauth2(['repo', 'gist'], null, false, true);
|
||||
},
|
||||
async addAccount(repoFullAccess = false) {
|
||||
const token = await this.startOauth2(getScopes({ repoFullAccess }));
|
||||
badgeSvc.addBadge('addGitHubAccount');
|
||||
|
@ -148,6 +160,30 @@ export default {
|
|||
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
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@ import diffUtils from './diffUtils';
|
|||
import networkSvc from './networkSvc';
|
||||
import providerRegistry from './providers/common/providerRegistry';
|
||||
import giteeAppDataProvider from './providers/giteeAppDataProvider';
|
||||
import githubAppDataProvider from './providers/githubAppDataProvider';
|
||||
import './providers/couchdbWorkspaceProvider';
|
||||
import './providers/githubWorkspaceProvider';
|
||||
import './providers/giteeWorkspaceProvider';
|
||||
|
@ -830,7 +831,7 @@ const syncWorkspace = async (skipContents = false) => {
|
|||
}
|
||||
|
||||
if (workspace.id === 'main') {
|
||||
badgeSvc.addBadge('syncMainWorkspace');
|
||||
badgeSvc.addBadge(workspace.providerId === 'giteeAppData' ? 'syncMainWorkspace' : 'githubSyncMainWorkspace');
|
||||
}
|
||||
} catch (err) {
|
||||
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 {
|
||||
async init() {
|
||||
// Load workspaces and tokens from localStorage
|
||||
|
@ -980,10 +990,11 @@ export default {
|
|||
await actionProvider.initAction();
|
||||
}
|
||||
|
||||
const mainToken = store.getters['workspace/mainWorkspaceToken'];
|
||||
// Try to find a suitable workspace sync provider
|
||||
workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId];
|
||||
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
||||
workspaceProvider = giteeAppDataProvider;
|
||||
workspaceProvider = mainToken && mainToken.providerId === 'githubAppData' ? githubAppDataProvider : giteeAppDataProvider;
|
||||
}
|
||||
const workspace = await workspaceProvider.initWorkspace();
|
||||
// Fix the URL hash
|
||||
|
@ -1041,6 +1052,7 @@ export default {
|
|||
}, 5000);
|
||||
}
|
||||
},
|
||||
afterSignIn,
|
||||
syncImg,
|
||||
isSyncPossible,
|
||||
requestSync,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import utils from '../services/utils';
|
||||
import giteeHelper from '../services/providers/helpers/giteeHelper';
|
||||
import githubHelper from '../services/providers/helpers/githubHelper';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
|
||||
const idShifter = offset => (state, getters) => {
|
||||
|
@ -136,8 +137,13 @@ export default {
|
|||
const loginToken = rootGetters['workspace/loginToken'];
|
||||
if (!loginToken) {
|
||||
try {
|
||||
await dispatch('modal/open', 'signInForComment', { root: true });
|
||||
await giteeHelper.signin();
|
||||
const signInWhere = await dispatch('modal/open', 'signInForComment', { root: true });
|
||||
if (signInWhere === 'github') {
|
||||
await githubHelper.signin();
|
||||
} else {
|
||||
await giteeHelper.signin();
|
||||
}
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
await dispatch('createNewDiscussion', selection);
|
||||
} catch (e) { /* cancel */ }
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
Object.entries(rootGetters['data/workspaces']).forEach(([id, workspace]) => {
|
||||
const sanitizedWorkspace = {
|
||||
id,
|
||||
providerId: 'giteeAppData',
|
||||
providerId: (mainWorkspaceToken && mainWorkspaceToken.providerId) || 'giteeAppData',
|
||||
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
|
||||
...workspace,
|
||||
};
|
||||
|
@ -47,17 +47,19 @@ export default {
|
|||
|| currentWorkspace.providerId === 'giteeWorkspace'
|
||||
|| currentWorkspace.providerId === 'gitlabWorkspace'
|
||||
|| currentWorkspace.providerId === 'giteaWorkspace'
|
||||
|| currentWorkspace.providerId === 'giteeAppData',
|
||||
|| currentWorkspace.providerId === 'giteeAppData'
|
||||
|| currentWorkspace.providerId === 'githubAppData',
|
||||
currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) =>
|
||||
currentWorkspace.providerId === 'githubWorkspace'
|
||||
|| currentWorkspace.providerId === 'giteeWorkspace'
|
||||
|| currentWorkspace.providerId === 'gitlabWorkspace'
|
||||
|| currentWorkspace.providerId === 'giteaWorkspace'
|
||||
|| currentWorkspace.providerId === 'giteeAppData',
|
||||
|| currentWorkspace.providerId === 'giteeAppData'
|
||||
|| currentWorkspace.providerId === 'githubAppData',
|
||||
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
|
||||
lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
|
||||
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) {
|
||||
return token;
|
||||
}
|
||||
|
@ -85,8 +87,10 @@ export default {
|
|||
switch (currentWorkspace.providerId) {
|
||||
case 'googleDriveWorkspace':
|
||||
return 'google';
|
||||
case 'githubAppData':
|
||||
case 'githubWorkspace':
|
||||
return 'github';
|
||||
case 'giteeAppData':
|
||||
case 'giteeWorkspace':
|
||||
default:
|
||||
return 'gitee';
|
||||
|
|
165
static/landing/gistshare.html
Normal file
165
static/landing/gistshare.html
Normal 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>
|
|
@ -36,7 +36,7 @@ style.innerHTML = "/** activeblue 灵动蓝\n \
|
|||
top: 0;\n \
|
||||
width: 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 \
|
||||
opacity: .12;\n \
|
||||
}\n \
|
||||
|
|
|
@ -100,7 +100,7 @@ style.innerHTML = "/* 草原绿 caoyuangreen\n \
|
|||
width:30px;\n \
|
||||
height:30px;\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-size:30px;\n \
|
||||
margin:auto;\n \
|
||||
|
|
Loading…
Reference in New Issue
Block a user