主空间修改为gitee

This commit is contained in:
xiaoqi.cxq 2022-06-04 03:07:10 +08:00
parent b94898960a
commit f0612d1120
36 changed files with 668 additions and 424 deletions

View File

@ -14,7 +14,7 @@ StackEdit的作者可能因为什么原因已经很久不维护了Github
### TODO: 关于后续的一些想法
- 支持**Gitea**、**Gogs**两个轻量级且适于自建的Git仓库毕竟Gitlab对机器配置要求较高。想支持这两个主要也是考虑到其实很多公司已经禁用了Github或Gitee仓库在公司都没法连上自己的Git仓库。
- 汉化,毕竟大家最熟悉的还是母语,并且该编辑器功能页面也不多,汉化工作量并不会很大。
- 替换主工作区为Gitee原版本主工作区是Google Drive国内只有fan墙才可以用
- 替换主文档空间为Gitee原版本主文档空间是Google Drive国内只有fan墙才可以用
- 引入mdnice右边预览增加mdnice预览选项主要含选主题(含mdnice常用20多个主题)、支持自定义主题、复制到公众号、复制到知乎、复制到稀土掘金等基本功能,便于喜欢写公众号、博客的同学可以更好更快的排版。
- ... 另外朋友们有好的想法也可以在Issue或者加我微信 qicoding 提给我。
@ -27,7 +27,7 @@ StackEdit的作者可能因为什么原因已经很久不维护了Github
**已汉化主要功能部分2022-06-01**
**接下来修改主工作区为Gitee**
**接下来修改主文档空间为Gitee**

View File

@ -1,6 +1,6 @@
{
"name": "StackEdit中文版",
"description": "浏览器内 Markdown 编辑器",
"description": "支持Gitee仓库的浏览器内 Markdown 编辑器",
"version": "1.0.13",
"manifest_version": 2,
"container" : "GOOGLE_DRIVE",

View File

@ -2,7 +2,6 @@
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
const conf = require('./conf');
const outputFormats = {
@ -42,108 +41,101 @@ exports.generate = (req, res) => {
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
? req.query.format
: 'pdf';
user.checkSponsor(req.query.idToken)
.then((isSponsor) => {
if (!isSponsor) {
throw new Error('unauthorized');
}
return new Promise((resolve, reject) => {
tmp.file({
postfix: `.${outputFormat}`,
}, (err, filePath, fd, cleanupCallback) => {
if (err) {
reject(err);
} else {
resolve({
filePath,
cleanupCallback,
});
}
});
});
})
.then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
const options = readJson(req.query.options);
const metadata = readJson(req.query.metadata);
const params = [];
params.push('--latex-engine=xelatex');
params.push('--webtex=http://chart.apis.google.com/chart?cht=tx&chf=bg,s,FFFFFF00&chco=000000&chl=');
if (options.toc) {
params.push('--toc');
}
options.tocDepth = parseInt(options.tocDepth, 10);
if (!Number.isNaN(options.tocDepth)) {
params.push('--toc-depth', options.tocDepth);
}
options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
params.push('--highlight-style', options.highlightStyle);
Object.keys(metadata).forEach((key) => {
params.push('-M', `${key}=${metadata[key]}`);
});
let finished = false;
function onError(error) {
finished = true;
cleanupCallback();
reject(error);
}
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
params.push('-f', 'json', '-t', format, '-o', filePath);
const pandoc = spawn(conf.values.pandocPath, params, {
stdio: [
'pipe',
'ignore',
'pipe',
],
});
let timeoutId = setTimeout(() => {
timeoutId = null;
pandoc.kill();
}, 50000);
pandoc.on('error', onError);
pandoc.stdin.on('error', onError);
pandoc.stderr.on('data', (data) => {
pandocError += `${data}`;
});
pandoc.on('close', (code) => {
if (!finished) {
clearTimeout(timeoutId);
if (!timeoutId) {
res.statusCode = 408;
cleanupCallback();
reject(new Error('timeout'));
} else if (code) {
cleanupCallback();
reject();
} else {
res.set('Content-Type', outputFormats[outputFormat]);
const readStream = fs.createReadStream(filePath);
readStream.on('open', () => readStream.pipe(res));
readStream.on('close', () => cleanupCallback());
readStream.on('error', () => {
cleanupCallback();
reject();
});
}
}
});
req.pipe(pandoc.stdin);
}))
.catch((err) => {
const message = err && err.message;
if (message === 'unauthorized') {
res.statusCode = 401;
res.end('Unauthorized.');
} else if (message === 'timeout') {
res.statusCode = 408;
res.end('Request timeout.');
new Promise((resolve, reject) => {
tmp.file({
postfix: `.${outputFormat}`,
}, (err, filePath, fd, cleanupCallback) => {
if (err) {
reject(err);
} else {
res.statusCode = 400;
res.end(pandocError || 'Unknown error.');
resolve({
filePath,
cleanupCallback,
});
}
});
}).then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
const options = readJson(req.query.options);
const metadata = readJson(req.query.metadata);
const params = [];
params.push('--pdf-engine=xelatex');
params.push('--webtex=http://chart.apis.google.com/chart?cht=tx&chf=bg,s,FFFFFF00&chco=000000&chl=');
if (options.toc) {
params.push('--toc');
}
options.tocDepth = parseInt(options.tocDepth, 10);
if (!Number.isNaN(options.tocDepth)) {
params.push('--toc-depth', options.tocDepth);
}
options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
params.push('--highlight-style', options.highlightStyle);
Object.keys(metadata).forEach((key) => {
params.push('-M', `${key}=${metadata[key]}`);
});
let finished = false;
function onError(error) {
finished = true;
cleanupCallback();
reject(error);
}
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
params.push('-f', 'json', '-t', format, '-o', filePath);
const pandoc = spawn(conf.values.pandocPath, params, {
stdio: [
'pipe',
'ignore',
'pipe',
],
});
let timeoutId = setTimeout(() => {
timeoutId = null;
pandoc.kill();
}, 50000);
pandoc.on('error', onError);
pandoc.stdin.on('error', onError);
pandoc.stderr.on('data', (data) => {
pandocError += `${data}`;
});
pandoc.on('close', (code) => {
if (!finished) {
clearTimeout(timeoutId);
if (!timeoutId) {
res.statusCode = 408;
cleanupCallback();
reject(new Error('timeout'));
} else if (code) {
cleanupCallback();
reject();
} else {
res.set('Content-Type', outputFormats[outputFormat]);
const readStream = fs.createReadStream(filePath);
readStream.on('open', () => readStream.pipe(res));
readStream.on('close', () => cleanupCallback());
readStream.on('error', () => {
cleanupCallback();
reject();
});
}
}
});
req.pipe(pandoc.stdin);
}))
.catch((err) => {
console.error(err);
const message = err && err.message;
if (message === 'unauthorized') {
res.statusCode = 401;
res.end('Unauthorized.');
} else if (message === 'timeout') {
res.statusCode = 408;
res.end('Request timeout.');
} else {
res.statusCode = 400;
res.end(pandocError || 'Unknown error.');
}
});
};

View File

@ -2,7 +2,6 @@
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
const conf = require('./conf');
/* eslint-disable no-var, prefer-arrow-callback, func-names */
@ -51,135 +50,129 @@ const readJson = (str) => {
exports.generate = (req, res) => {
let wkhtmltopdfError = '';
user.checkSponsor(req.query.idToken)
.then((isSponsor) => {
if (!isSponsor) {
throw new Error('unauthorized');
}
return new Promise((resolve, reject) => {
tmp.file((err, filePath, fd, cleanupCallback) => {
if (err) {
reject(err);
} else {
resolve({
filePath,
cleanupCallback,
});
}
});
});
})
.then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
let finished = false;
function onError(err) {
finished = true;
cleanupCallback();
new Promise((resolve, reject) => {
tmp.file((err, filePath, fd, cleanupCallback) => {
if (err) {
reject(err);
}
const options = readJson(req.query.options);
const params = [];
// Margins
const marginTop = parseInt(`${options.marginTop}`, 10);
params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
const marginRight = parseInt(`${options.marginRight}`, 10);
params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
const marginBottom = parseInt(`${options.marginBottom}`, 10);
params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
const marginLeft = parseInt(`${options.marginLeft}`, 10);
params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);
// Header
if (options.headerCenter) {
params.push('--header-center', `${options.headerCenter}`);
}
if (options.headerLeft) {
params.push('--header-left', `${options.headerLeft}`);
}
if (options.headerRight) {
params.push('--header-right', `${options.headerRight}`);
}
if (options.headerFontName) {
params.push('--header-font-name', `${options.headerFontName}`);
}
if (options.headerFontSize) {
params.push('--header-font-size', `${options.headerFontSize}`);
}
// Footer
if (options.footerCenter) {
params.push('--footer-center', `${options.footerCenter}`);
}
if (options.footerLeft) {
params.push('--footer-left', `${options.footerLeft}`);
}
if (options.footerRight) {
params.push('--footer-right', `${options.footerRight}`);
}
if (options.footerFontName) {
params.push('--footer-font-name', `${options.footerFontName}`);
}
if (options.footerFontSize) {
params.push('--footer-font-size', `${options.footerFontSize}`);
}
// Page size
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
params.push('--window-status', 'done');
const wkhtmltopdf = spawn(conf.values.wkhtmltopdfPath, params.concat('-', filePath), {
stdio: [
'pipe',
'ignore',
'pipe',
],
});
let timeoutId = setTimeout(function () {
timeoutId = null;
wkhtmltopdf.kill();
}, 50000);
wkhtmltopdf.on('error', onError);
wkhtmltopdf.stdin.on('error', onError);
wkhtmltopdf.stderr.on('data', (data) => {
wkhtmltopdfError += `${data}`;
});
wkhtmltopdf.on('close', (code) => {
if (!finished) {
clearTimeout(timeoutId);
if (!timeoutId) {
cleanupCallback();
reject(new Error('timeout'));
} else if (code) {
cleanupCallback();
reject();
} else {
res.set('Content-Type', 'application/pdf');
const readStream = fs.createReadStream(filePath);
readStream.on('open', () => readStream.pipe(res));
readStream.on('close', () => cleanupCallback());
readStream.on('error', () => {
cleanupCallback();
reject();
});
}
}
});
req.pipe(wkhtmltopdf.stdin);
}))
.catch((err) => {
const message = err && err.message;
if (message === 'unauthorized') {
res.statusCode = 401;
res.end('Unauthorized.');
} else if (message === 'timeout') {
res.statusCode = 408;
res.end('Request timeout.');
} else {
res.statusCode = 400;
res.end(wkhtmltopdfError || 'Unknown error.');
resolve({
filePath,
cleanupCallback,
});
}
});
}).then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
let finished = false;
function onError(err) {
finished = true;
cleanupCallback();
reject(err);
}
const options = readJson(req.query.options);
const params = [];
// Margins
const marginTop = parseInt(`${options.marginTop}`, 10);
params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
const marginRight = parseInt(`${options.marginRight}`, 10);
params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
const marginBottom = parseInt(`${options.marginBottom}`, 10);
params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
const marginLeft = parseInt(`${options.marginLeft}`, 10);
params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);
// Header
if (options.headerCenter) {
params.push('--header-center', `${options.headerCenter}`);
}
if (options.headerLeft) {
params.push('--header-left', `${options.headerLeft}`);
}
if (options.headerRight) {
params.push('--header-right', `${options.headerRight}`);
}
if (options.headerFontName) {
params.push('--header-font-name', `${options.headerFontName}`);
}
if (options.headerFontSize) {
params.push('--header-font-size', `${options.headerFontSize}`);
}
// Footer
if (options.footerCenter) {
params.push('--footer-center', `${options.footerCenter}`);
}
if (options.footerLeft) {
params.push('--footer-left', `${options.footerLeft}`);
}
if (options.footerRight) {
params.push('--footer-right', `${options.footerRight}`);
}
if (options.footerFontName) {
params.push('--footer-font-name', `${options.footerFontName}`);
}
if (options.footerFontSize) {
params.push('--footer-font-size', `${options.footerFontSize}`);
}
// Page size
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
params.push('--window-status', 'done');
const wkhtmltopdf = spawn(conf.values.wkhtmltopdfPath, params.concat('-', filePath), {
stdio: [
'pipe',
'ignore',
'pipe',
],
});
let timeoutId = setTimeout(function () {
timeoutId = null;
wkhtmltopdf.kill();
}, 50000);
wkhtmltopdf.on('error', onError);
wkhtmltopdf.stdin.on('error', onError);
wkhtmltopdf.stderr.on('data', (data) => {
wkhtmltopdfError += `${data}`;
});
wkhtmltopdf.on('close', (code) => {
if (!finished) {
clearTimeout(timeoutId);
if (!timeoutId) {
cleanupCallback();
reject(new Error('timeout'));
} else if (code) {
cleanupCallback();
reject();
} else {
res.set('Content-Type', 'application/pdf');
const readStream = fs.createReadStream(filePath);
readStream.on('open', () => readStream.pipe(res));
readStream.on('close', () => cleanupCallback());
readStream.on('error', () => {
cleanupCallback();
reject();
});
}
}
});
req.pipe(wkhtmltopdf.stdin);
}))
.catch((err) => {
console.error(err);
const message = err && err.message;
if (message === 'unauthorized') {
res.statusCode = 401;
res.end('Unauthorized.');
} else if (message === 'timeout') {
res.statusCode = 408;
res.end('Request timeout.');
} else {
res.statusCode = 400;
res.end(wkhtmltopdfError || 'Unknown error.');
}
});
};

View File

@ -1,9 +1,9 @@
<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">
<!-- <div class="modal__sponsor-banner" v-if="!isSponsor">
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>
</div> -->
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
@ -20,7 +20,7 @@ import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import googleHelper from '../services/providers/helpers/googleHelper';
import giteeHelper from '../services/providers/helpers/giteeHelper';
import store from '../store';
import ModalInner from './modals/common/ModalInner';
@ -168,7 +168,7 @@ export default {
if (!store.getters['workspace/sponsorToken']) {
// User has to sign in
await store.dispatch('modal/open', 'signInForSponsorship');
await googleHelper.signin();
await giteeHelper.signin();
syncSvc.requestSync();
}
if (!store.getters.isSponsor) {

View File

@ -47,14 +47,14 @@ import store from '../store';
const panelNames = {
menu: '菜单',
workspaces: '工作区',
workspaces: '文档空间',
help: 'Markdown 帮助',
toc: '目录',
sync: '同步',
publish: '发布',
history: '文件历史',
importExport: '导入/导出',
workspaceBackups: '工作区备份',
workspaceBackups: '文档空间备份',
};
export default {

View File

@ -8,7 +8,7 @@
<span v-for="stat in textStats" :key="stat.id">
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
</span>
<span class="stat-panel__value">Ln {{line}}, Col {{column}}</span>
<span class="stat-panel__value">{{line}} , {{column}} </span>
</div>
<div class="stat-panel__block stat-panel__block--right">
<span class="stat-panel__block-name">
@ -43,14 +43,13 @@ export default {
line: 0,
column: 0,
textStats: [
new Stat('bytes', '[\\s\\S]'),
new Stat('words', '\\S+'),
new Stat('lines', '\n'),
new Stat('字符', '[\\s\\S]'),
new Stat('字数', '\\S'),
new Stat('行数', '\n'),
],
htmlStats: [
new Stat('characters', '\\S'),
new Stat('words', '\\S+'),
new Stat('paragraphs', '\\S.*'),
new Stat('字数', '\\S'),
new Stat('段落', '\\S.*'),
],
}),
computed: mapGetters('layout', [

View File

@ -21,7 +21,7 @@
</div>
<div class="tour-step__inner" v-else-if="step === 'explorer'">
<h2>文件资源管理器</h2>
<p>StackEdit可以管理工作区中的多个文件和文件夹</p>
<p>StackEdit可以管理文档空间中的多个文件和文件夹</p>
<p>点击 <icon-folder></icon-folder> </p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">跳过</button>
@ -30,7 +30,7 @@
</div>
<div class="tour-step__inner" v-else-if="step === 'menu'">
<h2>更多</h2>
<p>StackEdit还可以同步和发布文件管理协作工作区...</p>
<p>StackEdit还可以同步和发布文件管理协作文档空间...</p>
<p>点击 <icon-provider provider-id="stackedit"></icon-provider> </p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">跳过</button>

View File

@ -8,7 +8,7 @@
</option>
</select>
</p>
<p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Google</a> 以同步您的主工作区</p>
<p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Gitee</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>
@ -53,7 +53,7 @@ import UserName from '../UserName';
import EditorClassApplier from '../common/EditorClassApplier';
import PreviewClassApplier from '../common/PreviewClassApplier';
import utils from '../../services/utils';
import googleHelper from '../../services/providers/helpers/googleHelper';
import giteeHelper from '../../services/providers/helpers/giteeHelper';
import syncSvc from '../../services/syncSvc';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
@ -166,7 +166,7 @@ export default {
]),
async signin() {
try {
await googleHelper.signin();
await giteeHelper.signin();
syncSvc.requestSync();
} catch (e) {
// Cancel

View File

@ -33,14 +33,14 @@
</menu-entry>
<menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">赞助商</div> 导出为 HTML PDF</div>
<div>导出为 HTML PDF</div>
<span>从HTML模板生成PDF</span>
</menu-entry>
<menu-entry @click.native="exportPandoc">
<!-- <menu-entry @click.native="exportPandoc">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">赞助商</div> 导出为 HTML Pandoc</div>
<div>导出为 HTML Pandoc</div>
<span>转换为PDFWordEPUB...</span>
</menu-entry>
</menu-entry> -->
</div>
</template>

View File

@ -11,8 +11,8 @@
<div class="menu-entry__icon menu-entry__icon--image">
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
</div>
<span v-if="currentWorkspace.providerId === 'googleDriveAppData'">
<b>{{currentWorkspace.name}}</b> 与您的 Google Drive 应用数据文件夹同步
<span v-if="currentWorkspace.providerId === 'giteeAppData'">
<b>{{currentWorkspace.name}}</b> 与您的 Gitee 默认文档空间仓库同步
</span>
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
<b>{{currentWorkspace.name}}</b> <a :href="workspaceLocationUrl" target="_blank">Google Drive 文件夹</a>同步
@ -42,13 +42,13 @@
</div>
<menu-entry v-if="!loginToken" @click.native="signin">
<icon-login slot="icon"></icon-login>
<div>使用 Google 登录</div>
<span>同步您的主工作区并解锁功能</span>
<div>使用 Gitee 登录</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>
<span>切换到另一个工作区</span>
<div><div class="menu-entry__label menu-entry__label--count" v-if="workspaceCount">{{workspaceCount}}</div> 文档空间</div>
<span>切换到另一个文档空间</span>
</menu-entry>
<hr>
<menu-entry @click.native="setPanel('sync')">
@ -113,7 +113,7 @@
<hr>
<menu-entry @click.native="setPanel('workspaceBackups')">
<icon-content-save slot="icon"></icon-content-save>
工作区备份
文档空间备份
</menu-entry>
<menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout>
@ -131,7 +131,7 @@ import { mapGetters, mapActions } from 'vuex';
import MenuEntry from './common/MenuEntry';
import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper';
import giteeHelper from '../../services/providers/helpers/giteeHelper';
import syncSvc from '../../services/syncSvc';
import userSvc from '../../services/userSvc';
import store from '../../store';
@ -183,7 +183,7 @@ export default {
}),
async signin() {
try {
await googleHelper.signin();
await giteeHelper.signin();
syncSvc.requestSync();
} catch (e) {
// Cancel

View File

@ -6,12 +6,12 @@
<icon-content-save></icon-content-save>
</div>
<div class="flex flex--column">
导入工作区备份
导入文档空间备份
</div>
</label>
<menu-entry @click.native="exportWorkspace">
<icon-content-save slot="icon"></icon-content-save>
导出工作区备份
导出文档空间备份
</menu-entry>
</div>
</template>

View File

@ -2,8 +2,8 @@
<div class="side-bar__panel side-bar__panel--menu">
<menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database>
<div><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> 管理工作区</div>
<span>列出重命名删除工作区</span>
<div><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> 管理文档空间</div>
<span>列出重命名删除文档空间</span>
</menu-entry>
<hr>
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
@ -15,27 +15,27 @@
<hr>
<menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<span>新增 <b>GitHub</b> 工作区</span>
<span>新增 <b>GitHub</b> 文档空间</span>
</menu-entry>
<menu-entry @click.native="addGiteeWorkspace">
<icon-provider slot="icon" provider-id="giteeWorkspace"></icon-provider>
<span>新增 <b>Gitee</b> 工作区</span>
<span>新增 <b>Gitee</b> 文档空间</span>
</menu-entry>
<menu-entry @click.native="addGitlabWorkspace">
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
<span>新增 <b>GitLab</b> 工作区</span>
<span>新增 <b>GitLab</b> 文档空间</span>
</menu-entry>
<menu-entry @click.native="addGiteaWorkspace">
<icon-provider slot="icon" provider-id="giteaWorkspace"></icon-provider>
<span>新增 <b>Gitea</b> 工作区</span>
<span>新增 <b>Gitea</b> 文档空间</span>
</menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>新增 <b>Google Drive</b> 工作区</span>
<span>新增 <b>Google Drive</b> 文档空间</span>
</menu-entry>
<menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<span>新增 <b>CouchDB</b> 工作区</span>
<span>新增 <b>CouchDB</b> 文档空间</span>
</menu-entry>
</div>
</template>

View File

@ -29,7 +29,6 @@
import FileSaver from 'file-saver';
import networkSvc from '../../services/networkSvc';
import editorSvc from '../../services/editorSvc';
import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
@ -45,15 +44,11 @@ export default modalTemplate({
const currentContent = store.getters['content/current'];
const { selectedFormat } = this;
store.dispatch('queue/enqueue', async () => {
const tokenToRefresh = store.getters['workspace/sponsorToken'];
const sponsorToken = tokenToRefresh && await googleHelper.refreshToken(tokenToRefresh);
try {
const { body } = await networkSvc.request({
method: 'POST',
url: 'pandocExport',
params: {
idToken: sponsorToken && sponsorToken.idToken,
format: selectedFormat,
options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
metadata: JSON.stringify(currentContent.properties),
@ -65,12 +60,8 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
badgeSvc.addBadge('exportPandoc');
} catch (err) {
if (err.status === 401) {
store.dispatch('modal/open', 'sponsorOnly');
} else {
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
}
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
}
});
},

View File

@ -24,7 +24,6 @@
import FileSaver from 'file-saver';
import exportSvc from '../../services/exportSvc';
import networkSvc from '../../services/networkSvc';
import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
@ -38,24 +37,17 @@ export default modalTemplate({
this.config.resolve();
const currentFile = store.getters['file/current'];
store.dispatch('queue/enqueue', async () => {
const [sponsorToken, html] = await Promise.all([
Promise.resolve().then(() => {
const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}),
exportSvc.applyTemplate(
currentFile.id,
this.allTemplatesById[this.selectedTemplate],
true,
),
]);
const html = await exportSvc.applyTemplate(
currentFile.id,
this.allTemplatesById[this.selectedTemplate],
true,
);
try {
const { body } = await networkSvc.request({
method: 'POST',
url: 'pdfExport',
params: {
idToken: sponsorToken && sponsorToken.idToken,
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
},
body: html,
@ -65,12 +57,8 @@ export default modalTemplate({
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
badgeSvc.addBadge('exportPdf');
} catch (err) {
if (err.status === 401) {
store.dispatch('modal/open', 'sponsorOnly');
} else {
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
}
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
}
});
},

View File

@ -4,8 +4,8 @@
<div class="modal__image">
<icon-sync></icon-sync>
</div>
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> 与以下位置同步</p>
<p v-else><b>{{currentFileName}}</b>尚未同步</p>
<div>
<div class="sync-entry flex flex--column" v-for="location in syncLocations" :key="location.id">
<div class="sync-entry__header flex flex--row flex--align-center">
@ -23,7 +23,7 @@
</div>
<div class="sync-entry__row flex flex--row flex--align-center">
<div class="sync-entry__url">
{{location.url || 'Google Drive app data'}}
{{location.url || 'Gitee app data'}}
</div>
<div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
<button class="sync-entry__button button" v-clipboard="location.url" @click="info('位置URL复制到剪贴板')" v-title="'复制URL'">
@ -37,11 +37,11 @@
</div>
</div>
<div class="modal__info" v-if="syncLocations.length">
<b>Tip:</b> Removing a location won't delete any file.
<b>提示:</b> 删除位置不会删除任何文件
</div>
</div>
<div class="modal__button-bar">
<button class="button button--resolve" @click="config.resolve()">Close</button>
<button class="button button--resolve" @click="config.resolve()">关闭</button>
</div>
</modal-inner>
</template>

View File

@ -1,10 +1,10 @@
<template>
<modal-inner class="modal__inner-1--workspace-management" aria-label="管理工作区">
<modal-inner class="modal__inner-1--workspace-management" aria-label="管理文档空间">
<div class="modal__content">
<div class="modal__image">
<icon-database></icon-database>
</div>
<p><br>可以访问以下工作区</p>
<p><br>可以访问以下文档空间</p>
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
<div class="flex flex--column">
<div class="workspace-entry__header flex flex--row flex--align-center">
@ -27,10 +27,10 @@
{{workspace.url}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.url" @click="info('工作区URL已复制到剪贴板!')" v-title="'复制URL'">
<button class="workspace-entry__button button" v-clipboard="workspace.url" @click="info('文档空间URL已复制到剪贴板!')" v-title="'复制URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.url" target="_blank" v-title="'打开工作区'">
<a class="workspace-entry__button button" :href="workspace.url" target="_blank" v-title="'打开文档空间'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
@ -40,10 +40,10 @@
{{workspace.locationUrl}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.locationUrl" @click="info('工作区URL已复制到剪贴板!')" v-title="'复制URL'">
<button class="workspace-entry__button button" v-clipboard="workspace.locationUrl" @click="info('文档空间URL已复制到剪贴板!')" v-title="'复制URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.locationUrl" target="_blank" v-title="'打开工作区位置'">
<a class="workspace-entry__button button" :href="workspace.locationUrl" target="_blank" v-title="'打开文档空间位置'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
@ -117,9 +117,9 @@ export default {
},
async remove(id) {
if (id === this.mainWorkspace.id) {
this.info('您的主工作区无法删除。');
this.info('您的主文档空间无法删除。');
} else if (id === this.currentWorkspace.id) {
this.info('请先关闭工作区,然后再将其删除。');
this.info('请先关闭文档空间,然后再将其删除。');
} else {
try {
await store.dispatch('modal/open', 'removeWorkspace');

View File

@ -1,10 +1,10 @@
<template>
<modal-inner aria-label="增加CouchDB工作区">
<modal-inner aria-label="增加CouchDB文档空间">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="couchdb"></icon-provider>
</div>
<p>创建一个与<b>CouchDB</b>数据库同步的工作区</p>
<p>创建一个与<b>CouchDB</b>数据库同步的文档空间</p>
<form-entry label="Database URL" error="dbUrl">
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="gitea"></icon-provider>
</div>
<p>创建一个与<b> Gitea </b>项目文件夹同步的工作区</p>
<p>创建一个与<b> Gitea </b>项目文件夹同步的文档空间</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="gitee"></icon-provider>
</div>
<p>创建一个与<b>Gitee</b>仓库文件夹同步的工作区</p>
<p>创建一个与<b>Gitee</b>仓库文件夹同步的文档空间</p>
<form-entry label="仓库URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>创建一个与<b>GitHub</b>仓库文件夹同步的工作区</p>
<p>创建一个与<b>GitHub</b>仓库文件夹同步的文档空间</p>
<form-entry label="仓库URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="gitlab"></icon-provider>
</div>
<p>创建一个与<b>GitLab</b>仓库文件夹同步的工作区</p>
<p>创建一个与<b>GitLab</b>仓库文件夹同步的文档空间</p>
<form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -1,10 +1,10 @@
<template>
<modal-inner aria-label="添加Google Drive工作区">
<modal-inner aria-label="添加Google Drive文档空间">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>创建一个与<b> Google Drive </b>文件夹同步的工作区</p>
<p>创建一个与<b> Google Drive </b>文件夹同步的文档空间</p>
<form-entry label="Folder ID" info="可选的">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">

View File

@ -1,7 +1,7 @@
export default () => ({
main: {
id: 'main',
name: '主工作区',
name: '主文档空间',
// The rest will be filled by the workspace/workspacesById getter
},
});

View File

@ -1,8 +1,8 @@
**我的数据存储在哪里?**
如果您的工作区没有同步,则文件存储在浏览器中,无处可寻。
如果您的文档空间没有同步,则文件存储在浏览器中,无处可寻。
我们建议同步您的工作区以确保在清除浏览器数据的情况下不会丢失文件。自托管Gitea后端非常适合保证隐私。
我们建议同步您的文档空间以确保在清除浏览器数据的情况下不会丢失文件。自托管Gitea后端非常适合保证隐私。
**StackEdit可以访问我的数据而不告诉我吗**

View File

@ -54,22 +54,22 @@ export default [
new Feature(
'explorer',
'资源管理器',
'使用文件资源管理器管理工作区中的文件和文件夹。',
'使用文件资源管理器管理文档空间中的文件和文件夹。',
[
new Feature(
'createFile',
'文件创建',
'使用文件资源管理器在工作区中创建一个新文件。',
'使用文件资源管理器在文档空间中创建一个新文件。',
),
new Feature(
'switchFile',
'文件切换',
'使用文件资源管理器在工作区中从一个文件切换到另一个文件。',
'使用文件资源管理器在文档空间中从一个文件切换到另一个文件。',
),
new Feature(
'createFolder',
'文件夹创建',
'使用文件资源管理器在工作区中创建一个新文件夹。',
'使用文件资源管理器在文档空间中创建一个新文件夹。',
),
new Feature(
'moveFile',
@ -84,22 +84,22 @@ export default [
new Feature(
'renameFile',
'文件重命名',
'使用文件资源管理器重命名工作区中的文件。',
'使用文件资源管理器重命名文档空间中的文件。',
),
new Feature(
'renameFolder',
'文件夹重命名',
'使用文件资源管理器重命名工作区中的文件夹。',
'使用文件资源管理器重命名文档空间中的文件夹。',
),
new Feature(
'removeFile',
'文件删除',
'使用文件资源管理器删除工作区中的文件。',
'使用文件资源管理器删除文档空间中的文件。',
),
new Feature(
'removeFolder',
'文件夹删除',
'使用文件资源管理器删除工作区中的文件夹。',
'使用文件资源管理器删除文档空间中的文件夹。',
),
],
),
@ -143,64 +143,64 @@ export default [
new Feature(
'signIn',
'登录',
'使用 Google 登录,同步您的主工作区并解锁功能。',
'使用 Gitee 登录,同步您的主文档空间并解锁功能。',
[
new Feature(
'syncMainWorkspace',
'主工作区已同步',
'使用 Google 登录以将您的主工作区与您的 Google Drive 应用数据文件夹同步。',
'主文档空间已同步',
'使用 Gitee 登录以将您的主文档空间与您的默认空间stackedit-app-data仓库数据同步。',
),
new Feature(
'sponsor',
'赞助',
'使用 Google 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。',
'使用 Google 登录并赞助 StackEdit 以解锁 PDF 和 Pandoc 导出。(暂不支持赞助)',
),
],
),
new Feature(
'workspaces',
'工作区菜单',
'使用工作区菜单创建各种工作区并对其进行管理。',
'文档空间菜单',
'使用文档空间菜单创建各种文档空间并对其进行管理。',
[
new Feature(
'addCouchdbWorkspace',
'创建CouchDB工作区',
'使用工作区菜单创建CouchDB工作区。',
'创建CouchDB文档空间',
'使用文档空间菜单创建CouchDB文档空间。',
),
new Feature(
'addGithubWorkspace',
'创建GitHub工作区',
'使用工作区菜单创建GitHub工作区。',
'创建GitHub文档空间',
'使用文档空间菜单创建GitHub文档空间。',
),
new Feature(
'addGiteeWorkspace',
'创建Gitee工作区',
'使用工作区菜单创建Gitee工作区。',
'创建Gitee文档空间',
'使用文档空间菜单创建Gitee文档空间。',
),
new Feature(
'addGitlabWorkspace',
'创建Gitlab工作区',
'使用工作区菜单创建GitLab工作区。',
'创建Gitlab文档空间',
'使用文档空间菜单创建GitLab文档空间。',
),
new Feature(
'addGiteaWorkspace',
'创建Gitea工作区',
'使用工作区菜单创建Gitea工作区。',
'创建Gitea文档空间',
'使用文档空间菜单创建Gitea文档空间。',
),
new Feature(
'addGoogleDriveWorkspace',
'创建Google Drive工作区',
'使用工作区菜单创建Google Drive工作区。',
'创建Google Drive文档空间',
'使用文档空间菜单创建Google Drive文档空间。',
),
new Feature(
'renameWorkspace',
'工作区重命名',
'使用“管理工作区”对话框重命名工作区。',
'文档空间重命名',
'使用“管理文档空间”对话框重命名文档空间。',
),
new Feature(
'removeWorkspace',
'工作区删除',
'使用“管理工作区”对话框在本地删除工作区。',
'文档空间删除',
'使用“管理文档空间”对话框在本地删除文档空间。',
),
],
),

View File

@ -7,33 +7,33 @@ const simpleModal = (contentHtml, rejectText, resolveText) => ({
/* eslint sort-keys: "error" */
export default {
commentDeletion: simpleModal(
'<p>You are about to delete a comment. Are you sure?</p>',
'No',
'Yes, delete',
'<p>您将要删除评论。你确定吗?</p>',
'取消',
'确认删除',
),
discussionDeletion: simpleModal(
'<p>You are about to delete a discussion. Are you sure?</p>',
'No',
'Yes, delete',
'<p>您将要删除讨论。你确定吗?</p>',
'取消',
'确认删除',
),
fileRestoration: simpleModal(
'<p>You are about to revert some changes. Are you sure?</p>',
'No',
'Yes, revert',
'<p>您将要恢复一些更改。你确定吗?</p>',
'取消',
'确认恢复',
),
folderDeletion: simpleModal(
config => `<p>You are about to delete the folder <b>${config.item.name}</b>. Its files will be moved to Trash. Are you sure?</p>`,
'No',
'Yes, delete',
config => `<p>您将删除文件夹<b>${config.item.name}</b>。它的文件将移至回收站。你确定吗?</p>`,
'取消',
'确认删除',
),
pathConflict: simpleModal(
config => `<p><b>${config.item.name}</b> already exists. Do you want to add a suffix?</p>`,
'No',
'Yes, add suffix',
config => `<p><b>${config.item.name}</b>已经存在。您要添加后缀吗?</p>`,
'取消',
'确认添加',
),
paymentSuccess: simpleModal(
'<h3>Thank you for your payment!</h3><p>Your sponsorship will be active in a minute.</p>',
'Ok',
'<h3>感谢您的付款!</h3> <p>您的赞助将在一分钟内活跃。</p>',
'好的',
),
providerRedirection: simpleModal(
config => `<p>您将跳转到 <b>${config.name}</b> 授权页面。</p>`,
@ -41,57 +41,57 @@ export default {
'确认跳转',
),
removeWorkspace: simpleModal(
'<p>You are about to remove a workspace locally. Are you sure?</p>',
'No',
'Yes, remove',
'<p>您将要在本地删除文档空间ß。你确定吗?</p>',
'取消',
'确认删除',
),
reset: simpleModal(
'<p>这将在本地清理所有工作区,你确定吗?</p>',
'<p>这将在本地清理所有文档空间,你确定吗?</p>',
'取消',
'确认清理',
),
signInForComment: simpleModal(
`<p>您必须使用 Google 登录才能开始评论。</p>
<div class="modal__info"><b>注意:</b> </div>`,
<div class="modal__info"><b>注意:</b> </div>`,
'取消',
'确认登录',
),
signInForSponsorship: simpleModal(
`<p>您必须使用 Google 登录才能赞助。</p>
<div class="modal__info"><b>注意:</b> </div>`,
<div class="modal__info"><b>注意:</b> </div>`,
'取消',
'确认登录',
),
sponsorOnly: simpleModal(
'<p>This feature is restricted to sponsors as it relies on server resources.</p>',
'Ok, I understand',
'<p>此功能仅限于赞助商,因为它依赖于服务器资源。</p>',
'好的,我明白了',
),
stripName: simpleModal(
config => `<p><b>${config.item.name}</b> contains illegal characters. Do you want to strip them?</p>`,
'No',
'Yes, strip',
config => `<p><b>${config.item.name}</b>包含非法字符。你想剥离它们吗?</p>`,
'取消',
'确认剥离',
),
tempFileDeletion: simpleModal(
config => `<p>You are about to permanently delete the temporary file <b>${config.item.name}</b>. Are you sure?</p>`,
'No',
'Yes, delete',
config => `<p>您将永久删除临时文件<b>${config.item.name}</b>。你确定吗?</p>`,
'取消',
'确认删除',
),
tempFolderDeletion: simpleModal(
'<p>You are about to permanently delete all the temporary files. Are you sure?</p>',
'No',
'Yes, delete all',
'<p>您将永久删除所有临时文件。你确定吗?</p>',
'取消',
'确认删除',
),
trashDeletion: simpleModal(
'<p>Files in the trash are automatically deleted after 7 days of inactivity.</p>',
'Ok',
'<p>回收站中的文件在不活动7天后会自动删除。</p>',
'好的',
),
unauthorizedName: simpleModal(
config => `<p><b>${config.item.name}</b> is an unauthorized name.</p>`,
'Ok',
config => `<p><b>${config.item.name}</b>>是未经授权的名称。</p>`,
'好的',
),
workspaceGoogleRedirection: simpleModal(
'<p>StackEdit needs full Google Drive access to open this workspace.</p>',
'Cancel',
'Ok, grant',
'<p>StackEdit需要完整的Google Drive访问才能打开此文档空间。</p>',
'取消',
'确认授权',
),
};

View File

@ -30,23 +30,23 @@ StackEdit 将您的文件存储在您的浏览器中,这意味着您的所有
# 同步
同步是 StackEdit 的最大特点之一。它使您可以将工作区中的任何文件与存储在**Gitee** 和 **GitHub** 账号中的其他文件同步。这使您可以继续在其他设备上写作,与您共享文件的人协作,轻松集成到您的工作流程中......同步机制在后台每分钟发生一次,下载、合并和上传文件修改。
同步是 StackEdit 的最大特点之一。它使您可以将文档空间中的任何文件与存储在**Gitee** 和 **GitHub** 账号中的其他文件同步。这使您可以继续在其他设备上写作,与您共享文件的人协作,轻松集成到您的工作流程中......同步机制在后台每分钟发生一次,下载、合并和上传文件修改。
有两种类型的同步,它们可以相互补充:
- 工作区同步将自动同步您的所有文件、文件夹和设置。这将允许您在任何其他设备上获取您的工作区
> 要开始同步您的工作区,只需在菜单中使用 Google 登录。
- 文档空间同步将自动同步您的所有文件、文件夹和设置。这将允许您在任何其他设备上获取您的文档空间
> 要开始同步您的文档空间,只需在菜单中使用 Google 登录。
- 文件同步将保持工作区的一个文件与**Gitee**或**GitHub**中的一个或多个文件同步。
- 文件同步将保持文档空间的一个文件与**Gitee**或**GitHub**中的一个或多个文件同步。
> 在开始同步文件之前,您必须在**同步**子菜单中链接一个账号。
## 打开一个文件
您可以通过打开 **同步** 子菜单并单击 **Open from** 从**Gitee** 或 **GitHub** 打开文件。在工作区中打开后,文件中的任何修改都将自动同步。
您可以通过打开 **同步** 子菜单并单击 **Open from** 从**Gitee** 或 **GitHub** 打开文件。在文档空间中打开后,文件中的任何修改都将自动同步。
## 保存文件
您可以通过打开 **同步** 子菜单并单击 **Save on**工作区的任何文件保存到**Gitee** 或 **GitHub**。即使工作区中的文件已经同步,您也可以将其保存到另一个位置。 StackEdit 可以将一个文件与多个位置和账号同步。
您可以通过打开 **同步** 子菜单并单击 **Save on**文档空间的任何文件保存到**Gitee** 或 **GitHub**。即使文档空间中的文件已经同步,您也可以将其保存到另一个位置。 StackEdit 可以将一个文件与多个位置和账号同步。
##同步文件

View File

@ -11,7 +11,6 @@ export default {
classState() {
switch (this.providerId) {
case 'googleDrive':
case 'googleDriveAppData':
case 'googleDriveWorkspace':
return 'google-drive';
case 'googlePhotos':
@ -28,6 +27,7 @@ export default {
return 'blogger';
case 'couchdbWorkspace':
return 'couchdb';
case 'giteeAppData':
case 'giteeWorkspace':
return 'gitee';
default:

View File

@ -0,0 +1,256 @@
import store from '../../store';
import giteeHelper from './helpers/giteeHelper';
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: 'giteeAppData',
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 giteeHelper.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 giteeHelper.uploadFile({
owner: syncToken.name,
repo: appDataRepo,
branch: appDataBranch,
token: syncToken,
path: syncData.id,
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({
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 giteeHelper.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 downloadWorkspaceData({ token, syncData }) {
if (!syncData) {
return {};
}
const { sha, data } = await giteeHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path: syncData.id,
});
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 res = await giteeHelper.uploadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path,
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];
if (!path) {
return {
syncData: {
type: item.type,
hash: item.hash,
},
};
}
const syncData = {
id: path,
type: item.type,
hash: item.hash,
};
const res = await giteeHelper.uploadFile({
token,
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
path,
content: JSON.stringify(item),
sha: gitWorkspaceSvc.shaByPath[path],
});
return {
syncData: {
...syncData,
sha: res.content.sha,
},
};
},
async listFileRevisions({ token, fileSyncDataId }) {
const { owner, repo, branch } = {
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
};
const entries = await giteeHelper.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 = `${giteeHelper.subPrefix}:${user.login}`;
if (user.avatar_url && user.avatar_url.endsWith('.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,
created: new Date(date).getTime(),
};
});
},
async loadFileRevision() {
// Revisions are already loaded
return false;
},
async getFileRevisionContent({
token,
contentId,
fileSyncDataId,
}) {
const { data } = await giteeHelper.downloadFile({
owner: token.name,
repo: appDataRepo,
branch: appDataBranch,
token,
path: fileSyncDataId,
});
return Provider.parseContent(data, contentId);
},
});

View File

@ -87,7 +87,7 @@ export default new Provider({
});
}
badgeSvc.addBadge('addGithubWorkspace');
badgeSvc.addBadge('addGiteeWorkspace');
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {

View File

@ -7,6 +7,8 @@ import constants from '../../../data/constants';
const tokenExpirationMargin = 5 * 60 * 1000;
const appDataRepo = 'stackedit-app-data';
const request = (token, options) => networkSvc.request({
...options,
headers: {
@ -125,6 +127,8 @@ export default {
sub: `${user.login}`,
};
// 检查 stackedit-app-data 仓库是否已经存在 如果不存在则创建该仓库
await this.checkAndCreateRepo(token);
// Add token to gitee tokens
store.dispatch('data/addGiteeToken', token);
return token;
@ -162,6 +166,9 @@ export default {
return this.startOauth2();
}
},
signin() {
return this.startOauth2();
},
async addAccount() {
const token = await this.startOauth2();
badgeSvc.addBadge('addGiteeAccount');
@ -191,6 +198,27 @@ export default {
return tree;
},
async checkAndCreateRepo(token) {
const url = `https://gitee.com/api/v5/repos/${encodeURIComponent(token.name)}/${encodeURIComponent(appDataRepo)}`;
try {
await request(token, { url });
} catch (err) {
// 不存在则创建
if (err.status === 404) {
await request(token, {
method: 'POST',
url: 'https://gitee.com/api/v5/user/repos',
params: {
name: appDataRepo,
auto_init: true,
},
});
} else {
throw err;
}
}
},
/**
* https://developer.gitee.com/v3/repos/commits/#list-commits-on-a-repository
*/

View File

@ -4,7 +4,7 @@ import utils from './utils';
import diffUtils from './diffUtils';
import networkSvc from './networkSvc';
import providerRegistry from './providers/common/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import giteeAppDataProvider from './providers/giteeAppDataProvider';
import './providers/couchdbWorkspaceProvider';
import './providers/githubWorkspaceProvider';
import './providers/giteeWorkspaceProvider';
@ -821,7 +821,7 @@ const requestSync = (addTriggerSyncBadge = false) => {
clearInterval(intervalId);
if (!isSyncPossible()) {
// Cancel sync
throw new Error('Sync not possible.');
throw new Error('无法同步。');
}
// Determine if we have to clean files
@ -888,7 +888,7 @@ export default {
// Try to find a suitable workspace sync provider
workspaceProvider = providerRegistry.providersById[utils.queryParams.providerId];
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
workspaceProvider = googleDriveAppDataProvider;
workspaceProvider = giteeAppDataProvider;
}
const workspace = await workspaceProvider.initWorkspace();
// Fix the URL hash

View File

@ -1,5 +1,5 @@
import utils from '../services/utils';
import googleHelper from '../services/providers/helpers/googleHelper';
import giteeHelper from '../services/providers/helpers/giteeHelper';
import syncSvc from '../services/syncSvc';
const idShifter = offset => (state, getters) => {
@ -137,7 +137,7 @@ export default {
if (!loginToken) {
try {
await dispatch('modal/open', 'signInForComment', { root: true });
await googleHelper.signin();
await giteeHelper.signin();
syncSvc.requestSync();
await dispatch('createNewDiscussion', selection);
} catch (e) { /* cancel */ }

View File

@ -22,7 +22,7 @@ export default {
Object.entries(rootGetters['data/workspaces']).forEach(([id, workspace]) => {
const sanitizedWorkspace = {
id,
providerId: 'googleDriveAppData',
providerId: 'giteeAppData',
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
...workspace,
};
@ -46,21 +46,18 @@ export default {
currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace'
|| currentWorkspace.providerId === 'giteaWorkspace',
|| currentWorkspace.providerId === 'giteaWorkspace'
|| currentWorkspace.providerId === 'giteeAppData',
currentWorkspaceHasUniquePaths: (state, { currentWorkspace }) =>
currentWorkspace.providerId === 'githubWorkspace'
|| currentWorkspace.providerId === 'giteeWorkspace'
|| currentWorkspace.providerId === 'gitlabWorkspace'
|| currentWorkspace.providerId === 'giteaWorkspace',
|| currentWorkspace.providerId === 'giteaWorkspace'
|| currentWorkspace.providerId === 'giteeAppData',
lastSyncActivityKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastSyncActivity`,
lastFocusKey: (state, { currentWorkspace }) => `${currentWorkspace.id}/lastWindowFocus`,
mainWorkspaceToken: (state, getters, rootState, rootGetters) =>
utils.someResult(Object.values(rootGetters['data/googleTokensBySub']), (token) => {
if (token.isLogin) {
return token;
}
return null;
}),
utils.someResult(Object.values(rootGetters['data/giteeTokensBySub']), token => token),
syncToken: (state, { currentWorkspace, mainWorkspaceToken }, rootState, rootGetters) => {
switch (currentWorkspace.providerId) {
case 'googleDriveWorkspace':
@ -82,11 +79,11 @@ export default {
loginType: (state, { currentWorkspace }) => {
switch (currentWorkspace.providerId) {
case 'googleDriveWorkspace':
default:
return 'google';
case 'githubWorkspace':
return 'github';
case 'giteeWorkspace':
default:
return 'gitee';
case 'gitlabWorkspace':
return 'gitlab';

View File

@ -353,7 +353,7 @@
<div class="column">
<div class="feature">
<h3>协作</h3>
<p>借助 StackEdit您可以共享协作工作空间,这要归功于同步机制。 如果两个协作者同时处理同一个文件StackEdit 会负责合并更改。</p>
<p>借助 StackEdit您可以共享协作文档空间,这要归功于同步机制。 如果两个协作者同时处理同一个文件StackEdit 会负责合并更改。</p>
</div>
<img class="image" width="300" src="static/landing/workspace.png">
</div>