Compare commits

..

No commits in common. "master" and "v5.0.4" have entirely different histories.

469 changed files with 16289 additions and 58086 deletions

View File

@ -8,7 +8,7 @@
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
"plugins": [ "istanbul" ]
}
}
}

View File

@ -1,9 +0,0 @@
node_modules
.git
dist
.history
images
docs
Dockerfile
README.md
build.sh

5
.gitignore vendored
View File

@ -2,9 +2,8 @@
node_modules/
dist/
.history
.idea
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode
stackedit_v4
chrome-app/*.zip
/test/unit/coverage/

View File

@ -1,22 +0,0 @@
language: node_js
node_js:
- "12"
services:
- docker
before_deploy:
# Run docker build
- docker build -t benweet/stackedit .
# Install Helm
- curl -SL -o /tmp/get_helm.sh https://git.io/get_helm.sh
- chmod 700 /tmp/get_helm.sh
- /tmp/get_helm.sh
- helm init --client-only
deploy:
provider: script
script: bash build/deploy.sh
on:
tags: true

View File

@ -1,16 +0,0 @@
FROM mafgwo/wkhtmltopdf-nodejs:11.15.0
WORKDIR /opt/stackedit
COPY package*json /opt/stackedit/
COPY gulpfile.js /opt/stackedit/
RUN npm install --unsafe-perm \
&& npm cache clean --force
COPY . /opt/stackedit
ENV NODE_ENV production
RUN npm run build
EXPOSE 8080
CMD [ "node", "." ]

201
LICENSE
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

186
README.md
View File

@ -1,187 +1,15 @@
<h1 align="center" style="text-align:center;">
<img src="chrome-app/icon-512.png" width="128" />
<br />
StackEdit中文版
</h1>
<p align="center">
<strong>笔记利器在线Markdown编辑器。</strong><br>
项目clone自<a href="https://gitee.com/mafgwo/stackedit" target="_blank" title="豆萁">豆萁/stackedit</a>如果你喜欢该项目请过去点一下Star您的肯定是作者最大的动力
</p>
<p align="center">
<a href="https://stackedit.cn/">https://stackedit.cn</a>
</p>
<p align="center">
<a target="_blank" href="https://www.apache.org/licenses/LICENSE-2.0.txt">
<img src="https://img.shields.io/:license-Apache2-blue.svg" alt="Apache 2" />
</a>
<a target="_blank" href="https://hub.docker.com/r/mafgwo/stackedit">
<img src="https://img.shields.io/docker/pulls/mafgwo/stackedit.svg" alt="Docker Pulls" />
</a>
<a target="_blank" href='https://gitee.com/mafgwo/stackedit/stargazers'>
<img src='https://gitee.com/mafgwo/stackedit/badge/star.svg' alt='gitee star'/>
</a>
</p>
<br/>
<hr />
1 笔记支持Gitee、GitHub、Gitea等Git仓库存储。<br>
2 支持直接上传图片也支持多种外部图床GitHub、Gitea、SM.MS、自定义图床粘贴或拖拽上传。<br>
3 编辑区域支持选择主题或自定义,总有你喜欢的主题。<br>
4 支持历史版本管理,不用担心编辑覆盖后无法回滚。<br>
5 支持ChatGPT辅助写作。<br>
6 支持KaTeX数学表达式、Mermaid UML图、乐谱等扩展。
<hr />
# my-project
## 说明
> A Vue.js project
本项目为本人clone修改自用如果你也喜欢请至原作者处获取及交流。
## Build Setup
## 截图
**亮暗主题切换、编辑主题切换**
![](./images/theme.gif)
**支持的文档空间**
![](./images/workspace.png)
**拖拽粘贴上传图片**
![](./images/uploadimg.gif)
**支持文档搜索**
![](./images/search.gif)
**ChatGPT集成协助写作**
![](./images/chatgpt.gif)
## 相比国外开源版本的区别:
- 修复了Github授权登录问题
- 支持了Gitee仓库2022-05-25
- 支持了Gitea仓库2022-05-25
- 汉化2022-06-01
- 主文档空间从GoogleDrive切换为Gitee2022-06-04
- 支持SM.MS图床粘贴/拖拽图片自动上传2022-07-01
- 支持Gitea图床粘贴/拖拽图片自动上传2022-07-02
- 支持自定义图床粘贴/拖拽图片自动上传2022-07-04
- 支持GitHub图床粘贴/拖拽图片自动上传2022-07-31
- 支持了右上角一键切换主题补全了深色主题的样式2022-08-07
- 编辑与预览区域样式优化2022-08-10
- 左边栏文件资源管理支持搜索文件2022-08-17
- 支持[TOC]目录2022-09-04
- 发布支持填写提交信息[针对Gitee、GitHub、Gitea、Gitlab]2022-09-10
- 支持文档空间关闭自动同步[针对Gitee、GitHub、Gitea、Gitlab]关闭后可自定义提交信息2022-09-23
- Gitea支持后端配置指定应用ID和Secret2022-10-03
- 支持编辑区域选择主题样式2022-10-06
- 支持图片直接存储到当前文档空间2022-10-29
- 支持MD文档之间链接跳转2022-11-20
- 支持预览区域选择主题样式2022-12-04
- Gitlab的支持优化2023-02-23
- 导出HTML、PDF支持带预览主题导出2023-02-26
- 支持分享文档2023-03-30
- 支持ChatGPT生成内容2023-04-10
- GitLab授权接口调整2023-08-26
- 主文档空间支持GitHub登录2023-10-19
## 国外开源版本弊端:
- 作者已经不维护了或很少维护了
- 不支持国内常用Gitee
- 强依赖GoogleDrive而Google Drive在国内不能正常访问
## 部署说明
> 建议docker-compose方式部署其他部署方式如遇到问题欢迎提issue。
docker官方仓库下载太慢可以使用阿里云的镜像仓库镜像仓库地址registry.cn-hangzhou.aliyuncs.com/mafgwo/stackedit:【版本号】
`docker-compose.yml`如下:
```yaml
version: "3.7"
services:
stackedit:
image: mafgwo/stackedit:【docker中央仓库找到最新版本】
container_name: stackedit
environment:
- LISTENING_PORT=8080
- ROOT_URL=/
- USER_BUCKET_NAME=root
- DROPBOX_APP_KEY=【不需要支持则删掉】
- DROPBOX_APP_KEY_FULL=【不需要支持则删掉】
- GITHUB_CLIENT_ID=【不需要支持则删掉】
- GITHUB_CLIENT_SECRET=【不需要支持则删掉】
- GITEE_CLIENT_ID=【不需要支持则删掉】
- GITEE_CLIENT_SECRET=【不需要支持则删掉】
- GOOGLE_CLIENT_ID=【不需要支持则删掉】
- GOOGLE_API_KEY=【不需要支持则删掉】
- GITEA_CLIENT_ID=【不需要支持则删掉】
- GITEA_CLIENT_SECRET=【不需要支持则删掉】
- GITEA_URL=【不需要支持则删掉】
- GITLAB_CLIENT_ID=【不需要支持则删掉】
- GITLAB_CLIENT_SECRET=【不需要支持则删掉】
- GITLAB_URL=【不需要支持则删掉】
ports:
- 8080:8080/tcp
network_mode: bridge
restart: always
```
docker-compose方式的启动或停止命令
```bash
# 在 docker-compose.yml 文件目录下 启动命令
docker-compose up -d
# 在 docker-compose.yml 文件目录下 停止命令
docker-compose down
# 更新镜像只需要修改docker-compose.yml中镜像版本执行再停止、启动命令即可
```
或者可以直接通过Docker命名直接启动命令如下
```bash
docker run -itd --name stackedit \
-p 8080:8080 \
-e LISTENING_PORT=8080 \
-e ROOT_URL=/ \
-e USER_BUCKET_NAME=root \
-e DROPBOX_APP_KEY=【不需要支持则删掉】 \
-e DROPBOX_APP_KEY_FULL=【不需要支持则删掉】 \
-e GITHUB_CLIENT_ID=【不需要支持则删掉】 \
-e GITHUB_CLIENT_SECRET=【不需要支持则删掉】 \
-e GITEE_CLIENT_ID=【不需要支持则删掉】 \
-e GITEE_CLIENT_SECRET=【不需要支持则删掉】 \
-e GOOGLE_CLIENT_ID=【不需要支持则删掉】 \
-e GOOGLE_API_KEY=【不需要支持则删掉】 \
-e GITEA_CLIENT_ID=【不需要支持则删掉】 \
-e GITEA_CLIENT_SECRET=【不需要支持则删掉】 \
-e GITEA_URL=【不需要支持则删掉】 \
-e GITLAB_CLIENT_ID=【不需要支持则删掉】 \
-e GITLAB_CLIENT_SECRET=【不需要支持则删掉】 \
-e GITLAB_URL=【不需要支持则删掉】 \
mafgwo/stackedit:【docker中央仓库找到最新版本】
```
## 如何创建三方平台应用
> 部署时如果需要支持Gitee或GitHub则需要自行到对应三方平台创建应用获取到应用ID和秘钥替换到以上的环境变量中再启动应用。
- Gitee的环境变量GITEE_CLIENT_ID、GITEE_CLIENT_SECRET**[如何创建Gitee应用](./docs/部署之Gitee应用创建.md)**
- GitHub的环境变量GITHUB_CLIENT_ID、GITEE_CLIENT_SECRET**[如何创建GitHub应用](./docs/部署之GitHub应用创建.md)**
- Gitea可选择性配置环境变量未配置则在关联时前端指定有配置则仅允许配置的应用信息GITEA_CLIENT_ID、GITEA_CLIENT_SECRET、GITEA_URL**[如何创建Gitea应用](./docs/部署之Gitea应用创建.md)**
- Gitlab可选择性配置环境变量未配置则在关联时前端指定有配置则仅允许配置的应用信息GITLAB_CLIENT_ID、GITLAB_CLIENT_SECRET、GITLAB_URL **如何创建Gitlab应用(待补充文档)**
特别说明自建的Gitea、Gitlab要能接入stackedit必须支持跨域
## 编译与运行
> 编译运行的nodejs版本选择11.15.0版本
```bash
# 安装依赖
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm start
npm run dev
# build for production with minification
npm run build
@ -189,3 +17,5 @@ npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

View File

@ -1,37 +0,0 @@
#!/bin/bash
# 检查参数是否提供版本号
if [ -z "$1" ]; then
echo "请提供版本号作为参数"
exit 1
fi
# 定义版本号变量
VERSION="$1"
IMAGE_NAME="mafgwo/stackedit"
# 构建 Docker 镜像
build_image() {
docker build -t "$IMAGE_NAME" .
}
# 标记 Docker 镜像
tag_image() {
docker tag "$IMAGE_NAME" "$IMAGE_NAME:$VERSION"
docker tag "$IMAGE_NAME" "registry.cn-hangzhou.aliyuncs.com/$IMAGE_NAME:$VERSION"
}
# 推送 Docker 镜像
push_image() {
docker push "$IMAGE_NAME"
docker push "registry.cn-hangzhou.aliyuncs.com/$IMAGE_NAME"
docker push "$IMAGE_NAME:$VERSION"
docker push "registry.cn-hangzhou.aliyuncs.com/$IMAGE_NAME:$VERSION"
}
# 执行构建、标记和推送
build_image
tag_image
push_image
echo "操作完成"

View File

@ -1,24 +0,0 @@
#!/bin/bash
set -e
# Tag and push docker image
docker login -u benweet -p "$DOCKER_PASSWORD"
docker tag benweet/stackedit "benweet/stackedit:$TRAVIS_TAG"
docker push benweet/stackedit:$TRAVIS_TAG
docker tag benweet/stackedit:$TRAVIS_TAG benweet/stackedit:latest
docker push benweet/stackedit:latest
# Build the chart
cd "$TRAVIS_BUILD_DIR"
npm run chart
# Add chart to helm repository
git clone --branch master "https://benweet:$GITHUB_TOKEN@github.com/benweet/stackedit-charts.git" /tmp/charts
cd /tmp/charts
helm package "$TRAVIS_BUILD_DIR/dist/stackedit"
helm repo index --url https://benweet.github.io/stackedit-charts/ .
git config user.name "Benoit Schweblin"
git config user.email "benoit.schweblin@gmail.com"
git add .
git commit -m "Added $TRAVIS_TAG"
git push origin master

View File

@ -1,11 +1,9 @@
require('./check-versions')()
var config = require('../config')
Object.keys(config.dev.env).forEach((key) => {
if (!process.env[key]) {
process.env[key] = JSON.parse(config.dev.env[key]);
}
});
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')

View File

@ -2,7 +2,6 @@ var path = require('path')
var webpack = require('webpack')
var utils = require('./utils')
var config = require('../config')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin')
@ -14,10 +13,6 @@ module.exports = {
entry: {
app: './src/'
},
node: {
// For mermaid
fs: 'empty' // jison generated code requires 'fs'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
@ -47,34 +42,10 @@ module.exports = {
loader: 'vue-loader',
options: vueLoaderConfig
},
// We can't pass graphlibrary to babel
{
test: /\.js$/,
loader: 'string-replace-loader',
include: [
resolve('node_modules/graphlibrary')
],
options: {
search: '^\\s*(?:let|const) ',
replace: 'var ',
flags: 'gm'
}
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [
resolve('src'),
resolve('test'),
resolve('node_modules/mermaid')
],
exclude: [
resolve('node_modules/mermaid/src/diagrams/class/parser'),
resolve('node_modules/mermaid/src/diagrams/flowchart/parser'),
resolve('node_modules/mermaid/src/diagrams/gantt/parser'),
resolve('node_modules/mermaid/src/diagrams/git/parser'),
resolve('node_modules/mermaid/src/diagrams/sequence/parser')
],
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
@ -85,9 +56,13 @@ module.exports = {
}
},
{
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
loader: 'file-loader',
test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader'
},
{
test: /\.woff(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
},
@ -98,7 +73,6 @@ module.exports = {
]
},
plugins: [
new VueLoaderPlugin(),
new StylelintPlugin({
files: ['**/*.vue', '**/*.scss']
}),

View File

@ -9,12 +9,6 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var OfflinePlugin = require('offline-plugin');
var WebpackPwaManifest = require('webpack-pwa-manifest')
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
var env = config.build.env
@ -34,9 +28,7 @@ var webpackConfig = merge(baseWebpackConfig, {
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
NODE_ENV: env.NODE_ENV,
GOOGLE_CLIENT_ID: env.GOOGLE_CLIENT_ID,
GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID
NODE_ENV: env.NODE_ENV
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
@ -100,31 +92,13 @@ var webpackConfig = merge(baseWebpackConfig, {
ignore: ['.*']
}
]),
new FaviconsWebpackPlugin({
logo: resolve('src/assets/favicon.png'),
title: 'StackEdit',
}),
new WebpackPwaManifest({
name: 'StackEdit',
description: 'Full-featured, open-source Markdown editor',
display: 'standalone',
orientation: 'any',
start_url: 'app',
background_color: '#ffffff',
crossorigin: 'use-credentials',
icons: [{
src: resolve('src/assets/favicon.png'),
sizes: [96, 128, 192, 256, 384, 512]
}]
}),
new OfflinePlugin({
ServiceWorker: {
events: true
},
AppCache: true,
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
externals: ['/', '/app', '/oauth2/callback']
}),
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html'],
externals: ['/app', '/oauth2/callback']
})
]
})

View File

@ -1,56 +0,0 @@
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
style: './src/styles/'
},
module: {
rules: [{
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
loader: 'file-loader',
options: {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}]
.concat(utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})),
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: config.build.assetsPublicPath
},
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: '[name].css',
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
]
}

View File

@ -1,22 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -1,5 +0,0 @@
apiVersion: v1
appVersion: vSTACKEDIT_VERSION
description: In-browser Markdown editor
name: stackedit
version: STACKEDIT_VERSION

View File

@ -1,21 +0,0 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stackedit.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "stackedit.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stackedit.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "stackedit.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80
{{- end }}

View File

@ -1,45 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "stackedit.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "stackedit.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "stackedit.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "stackedit.labels" -}}
app.kubernetes.io/name: {{ include "stackedit.name" . }}
helm.sh/chart: {{ include "stackedit.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

View File

@ -1,87 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "stackedit.fullname" . }}
labels:
{{ include "stackedit.labels" . | indent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "stackedit.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "stackedit.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
volumeMounts:
- mountPath: /run
name: run-volume
- mountPath: /tmp
name: tmp-volume
env:
- name: PORT
value: "80"
- name: PAYPAL_RECEIVER_EMAIL
value: {{ .Values.paypalReceiverEmail }}
- name: AWS_ACCESS_KEY_ID
value: {{ .Values.awsAccessKeyId }}
- name: AWS_SECRET_ACCESS_KEY
value: {{ .Values.awsSecretAccessKey }}
- name: DROPBOX_APP_KEY
value: {{ .Values.dropboxAppKey }}
- name: DROPBOX_APP_KEY_FULL
value: {{ .Values.dropboxAppKeyFull }}
- name: GOOGLE_CLIENT_ID
value: {{ .Values.googleClientId }}
- name: GOOGLE_API_KEY
value: {{ .Values.googleApiKey }}
- name: GITHUB_CLIENT_ID
value: {{ .Values.githubClientId }}
- name: GITHUB_CLIENT_SECRET
value: {{ .Values.githubClientSecret }}
- name: WORDPRESS_CLIENT_ID
value: {{ .Values.wordpressClientId }}
- name: WORDPRESS_SECRET
value: {{ .Values.wordpressSecret }}
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: run-volume
emptyDir: {}
- name: tmp-volume
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -1,39 +0,0 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "stackedit.fullname" . -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{ include "stackedit.labels" . | indent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ . }}
pathType: Prefix
backend:
service:
name: {{ $fullName }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}

View File

@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "stackedit.fullname" . }}
labels:
{{ include "stackedit.labels" . | indent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "stackedit.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}

View File

@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "stackedit.fullname" . }}-test-connection"
labels:
{{ include "stackedit.labels" . | indent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "stackedit.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@ -1,71 +0,0 @@
# Default values for stackedit.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
dropboxAppKey: ""
dropboxAppKeyFull: ""
googleClientId: ""
googleApiKey: ""
githubClientId: ""
githubClientSecret: ""
giteeClientId: ""
giteeClientSecret: ""
wordpressClientId: ""
wordpressSecret: ""
paypalReceiverEmail: ""
awsAccessKeyId: ""
awsSecretAccessKey: ""
giteaClientId: ""
giteaClientSecret: ""
giteaUrl: ""
gitlabClientId: ""
gitlabUrl: ""
replicaCount: 1
image:
repository: benweet/stackedit
tag: vSTACKEDIT_VERSION
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations:
# kubernetes.io/ingress.class: nginx
# certmanager.k8s.io/issuer: letsencrypt-prod
# certmanager.k8s.io/acme-challenge-type: http01
hosts: []
# - host: stackedit.example.com
# paths:
# - /
tls: []
# - secretName: stackedit-tls
# hosts:
# - stackedit.example.com
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -1,28 +0,0 @@
{
"name": "StackEdit中文版",
"description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器",
"version": "5.15.17",
"manifest_version": 2,
"container": "GITEE",
"api_console_project_id": "241271498917",
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"64": "icon-64.png",
"128": "icon-128.png",
"256": "icon-256.png",
"512": "icon-512.png"
},
"app": {
"urls": [
"https://md.jonylee.top/"
],
"launch": {
"web_url": "https://md.jonylee.top/app"
}
},
"offline_enabled": true,
"permissions": [
"unlimitedStorage"
]
}

View File

@ -2,17 +2,5 @@ var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
// 以下配置是开发临时用的配置 随时可能失效 请替换为自己的
GITHUB_CLIENT_ID: '"845b8f75df48f2ee0563"',
GITHUB_CLIENT_SECRET: '"80df676597abded1450926861965cc3f9bead6a0"',
GITEE_CLIENT_ID: '"925ba7c78b85dec984f7877e4aca5cab10ae333c6d68e761bdb0b9dfb8f55672"',
GITEE_CLIENT_SECRET: '"f05731066e42d307339dc8ebbb037a103881dafc7207a359a393b87749f1c562"',
CLIENT_ID: '"thF3qCGLN39OtafjGnqHyj6n02WwE6xD"',
// GITEA_CLIENT_ID: '"fe30f8f9-b1e8-4531-8f72-c1a5d3912805"',
// GITEA_CLIENT_SECRET: '"lus7oMnb3H6M1hsChndphArE20Txr7erwJLf7SDBQWTw"',
// GITEA_URL: '"https://gitea.test.com"',
GITLAB_CLIENT_ID: '"074cd5103c62dea0f479dac861039656ac80935e304c8113a02cc64c629496ae"',
GITLAB_CLIENT_SECRET: '"6f406f24216b686d55d28313dec1913c2a8e599afdb08380d5e8ce838e16e41e"',
GITLAB_URL: '"http://gitlab.qicoder.com"',
})
NODE_ENV: '"development"'
})

View File

@ -23,8 +23,8 @@ module.exports = {
},
dev: {
env: require('./dev.env'),
port: 80,
autoOpenBrowser: false,
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
@ -33,7 +33,6 @@ module.exports = {
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
// cssSourceMap: false
cssSourceMap: true
cssSourceMap: false
}
}

View File

@ -1,10 +0,0 @@
# 大文档导出PDF方式说明
> 由于大文档导出PDF需要消费非常多的服务器资源而且很容易导致导出超时故导出PDF的MD文档过大时可以使用 **[wkhtmltopdf](https://wkhtmltopdf.org/downloads.html)** 工具导出。
# 操作步骤
- 先在 **[StackEdit中文版](https://stackedit.cn/app)** 中使用 `导出为HTML` 功能导出MD文档导出后可以得到一个HTML文档。
- 到 **[wkhtmltopdf](https://wkhtmltopdf.org/downloads.html)** 官网下载安装程序。
- 使用 wkhtmltopdf 的导出PDF的命令 `wkhtmltopdf [GLOBAL OPTION]... [OBJECT]... <output file>` 把HTML导出为PDF如简单的导出命令`wkhtmltopdf test.html test.pdf`,具体的 `GLOBAL OPTION` 参数说明可以通过 `wkhtmltopdf -H` 查看帮助文档。

View File

@ -1,20 +0,0 @@
# GitHub应用配置说明
> StackEdit中文版部署如果需要支持GitHub则需要到GitHub创建一个应用并复制其中的clientId和clientSecret填充到环境变量 GITHUB_CLIENT_ID 和 GITHUB_CLIENT_SECRET 中。
# 如何创建GitHub应用
按下面图的指示创建
![](../images/github/github01.png)
![](../images/github/github02.png)
![](../images/github/github03.png)
![](../images/github/github04.png)
![](../images/github/github05.png)

View File

@ -1,35 +0,0 @@
# Gitea应用配置说明
> StackEdit中文版支持Gitea则需要到Gitea创建一个应用在StackEdit中文版绑定Gitea账号的时候填入。
# 如何创建Gitea应用
按下面图的指示创建
![](../images/gitea/gitea01.png)
![](../images/gitea/gitea02.png)
![](../images/gitea/gitea03.png)
![](../images/gitea/gitea04.png)
创建成功后即可看到应用ID 和 应用秘钥。
# Gitea跨域问题
由于StackEdit中文版是从浏览器直接访问Gitea接口故个人部署的Gitea需要支持跨域至于如何支持跨域请参考官方文档https://docs.gitea.io/en-us/config-cheat-sheet/#cors-cors (官方跨域的支持好像存在问题我个人包括很多网友通过这个配置支持跨域都失败了如果你也失败了可以试试用nginx代理实现跨域)
nginx配置实现跨域的配置如下
```
add_header Access-Control-Allow-Headers *;
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
if ($request_method = 'OPTIONS') {
return 204;
}
```

View File

@ -1,19 +0,0 @@
# Gitee应用配置说明
> StackEdit中文版部署如果需要支持Gitee则需要到Gitee创建一个应用并复制其中的clientId和clientSecret填充到环境变量 GITEE_CLIENT_ID 和 GITEE_CLIENT_SECRET 中。
# 如何创建Gitee应用
按下面图的指示创建
![](../images/gitee/gitee01.png)
![](../images/gitee/gitee02.png)
![](../images/gitee/gitee03.png)
![](../images/gitee/gitee04.png)
创建成功后即可看到client id 和 client secret。

View File

@ -10,7 +10,6 @@ const prismScripts = [
'prismjs/components/prism-javascript',
'prismjs/components/prism-css',
'prismjs/components/prism-ruby',
'prismjs/components/prism-cpp',
].map(require.resolve);
prismScripts.push(
path.join(path.dirname(require.resolve('prismjs/components/prism-core')), 'prism-!(*.min).js'));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

View File

@ -1,31 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<head>
<meta charset="utf-8">
<title>Markdown编辑器-JonyLee的设计导航</title>
<link rel="canonical" href="https://md.jonylee.top">
<meta name="description" content="StackEdit中文版免费开源功能全面的Markdown编辑器。">
<title>StackEdit</title>
<link rel="canonical" href="https://stackedit.io/app">
<!-- <link rel="icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
<!-- <link rel="icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
<!-- <link rel="shortcut icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
<!-- <link rel="shortcut icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
<!-- <link rel="apple-touch-icon-precomposed" sizes="152x152" href="res-min/img/logo-ipad-retina.png"> -->
<meta name="description" content="Free, open-source, full-featured Markdown editor.">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>
<body>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<!-- baidu统计 -->
<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?dad4b4383b13eedea1ab45ee323df1c3";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- baidu统计结束 -->
</body>
</html>
</body>
</html>

View File

@ -1,27 +1,29 @@
const env = require('./config/prod.env');
Object.keys(env).forEach((key) => {
if (!process.env[key]) {
process.env[key] = JSON.parse(env[key]);
}
});
process.env.NODE_ENV = 'production';
const http = require('http');
const https = require('https');
const path = require('path');
const express = require('express');
const fs = require('fs');
const app = express();
require('./server')(app);
require('./server')(app, process.env.SERVE_V4);
const port = parseInt(process.env.PORT || 8080, 10);
let port = parseInt(process.env.PORT || 8080, 10);
if (port === 443) {
const credentials = {
key: fs.readFileSync(path.join(__dirname, 'ssl.key'), 'utf8'),
cert: fs.readFileSync(path.join(__dirname, 'ssl.crt'), 'utf8'),
ca: fs.readFileSync(path.join(__dirname, 'ssl.ca'), 'utf8').split('\n\n'),
};
const httpsServer = https.createServer(credentials, app);
httpsServer.listen(port, null, () => {
console.log('HTTPS server started: https://localhost');
});
port = 80;
}
const httpServer = http.createServer(app);
httpServer.listen(port, null, () => {
console.log(`HTTP server started: http://localhost:${port}`);
});
// Handle graceful shutdown
process.on('SIGTERM', () => {
httpServer.close(() => {
process.exit(0);
});
});

22028
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,141 +1,116 @@
{
"name": "stackedit",
"version": "5.15.21",
"description": "免费, 开源, 功能齐全的 Markdown 编辑器",
"author": "Benoit Schweblin, 豆萁",
"version": "5.0.4",
"description": "Free, open-source, full-featured Markdown editor",
"author": "Benoit Schweblin",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/mafgwo/stackedit/issues"
"url": "https://github.com/benweet/stackedit/issues"
},
"main": "index.js",
"scripts": {
"postinstall": "gulp build-prism",
"start": "node build/dev-server.js",
"build": "node build/build.js && npm run build-style",
"build-style": "webpack --config build/webpack.style.conf.js",
"lint": "eslint --ext .js,.vue src server",
"unit": "jest --config test/unit/jest.conf.js --runInBand",
"unit-with-coverage": "jest --config test/unit/jest.conf.js --runInBand --coverage",
"test": "npm run lint && npm run unit",
"preversion": "npm run test",
"build": "node build/build.js",
"lint": "eslint --ext .js,.vue src",
"preversion": "npm run lint",
"postversion": "git push origin master --tags && npm publish",
"patch": "npm version patch -m \"Tag v%s\"",
"minor": "npm version minor -m \"Tag v%s\"",
"major": "npm version major -m \"Tag v%s\"",
"chart": "mkdir -p dist && rm -rf dist/stackedit && cp -r chart dist/stackedit && sed -i.bak -e s/STACKEDIT_VERSION/$npm_package_version/g dist/stackedit/*.yaml && rm dist/stackedit/*.yaml.bak"
"major": "npm version major -m \"Tag v%s\""
},
"dependencies": {
"@vue/test-utils": "^1.0.0-beta.16",
"abcjs": "^5.2.0",
"babel-runtime": "^6.26.0",
"bezier-easing": "^1.1.0",
"body-parser": "^1.18.2",
"clipboard": "^1.7.1",
"clunderscore": "^1.0.3",
"compression": "^1.7.0",
"diff-match-patch": "^1.0.0",
"file-saver": "^1.3.8",
"file-saver": "^1.3.3",
"handlebars": "^4.0.10",
"indexeddbshim": "^3.6.2",
"js-yaml": "^3.11.0",
"katex": "^0.16.2",
"markdown-it": "^8.4.1",
"indexeddbshim": "^3.0.4",
"js-yaml": "^3.9.1",
"katex": "^0.7.1",
"markdown-it": "^8.3.1",
"markdown-it-abbr": "^1.0.4",
"markdown-it-deflist": "^2.0.2",
"markdown-it-emoji": "^1.3.0",
"markdown-it-footnote": "^3.0.1",
"markdown-it-imsize": "^2.0.1",
"markdown-it-mark": "^2.0.0",
"markdown-it-pandoc-renderer": "1.1.3",
"markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0",
"mermaid": "^8.9.2",
"mousetrap": "^1.6.1",
"normalize-scss": "^7.0.1",
"normalize-scss": "^7.0.0",
"prismjs": "^1.6.0",
"request": "^2.85.0",
"serve-static": "^1.13.2",
"tmp": "^0.0.33",
"turndown": "^7.1.1",
"vue": "^2.5.16",
"vuex": "^3.0.1"
"raw-loader": "^0.5.1",
"request": "^2.82.0",
"serve-static": "^1.12.6",
"vue": "^2.3.3",
"vuex": "^2.3.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.4",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-import-resolver-webpack": "^0.9.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.11.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.4",
"eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.1.3",
"eslint-friendly-formatter": "^2.0.7",
"eslint-import-resolver-webpack": "^0.8.1",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-import": "^2.2.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.3",
"express": "^4.15.5",
"extract-text-webpack-plugin": "^2.0.0",
"favicons-webpack-plugin": "^0.0.9",
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.7.0",
"gulp": "^4.0.2",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"gulp": "^3.9.1",
"gulp-bump": "^2.7.0",
"gulp-concat": "^2.6.1",
"html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.18.0",
"identity-obj-proxy": "^3.0.0",
"gulp-util": "^3.0.8",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"ignore-loader": "^0.1.2",
"jest": "^23.0.0",
"jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^0.3.0",
"js-md5": "^0.7.3",
"node-sass": "^4.0.0",
"node-sass": "^4.5.3",
"npm-bump": "^0.0.23",
"offline-plugin": "^5.0.3",
"offline-plugin": "^4.8.4",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.2.0",
"raw-loader": "^0.5.1",
"replace-in-file": "^4.1.0",
"rimraf": "^2.6.0",
"sass-loader": "^7.0.1",
"semver": "^5.5.0",
"shelljs": "^0.8.1",
"string-replace-loader": "^2.1.1",
"stylelint": "^9.2.0",
"sass-loader": "^6.0.5",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"stylelint-config-standard": "^16.0.0",
"stylelint-processor-html": "^1.0.0",
"stylelint-webpack-plugin": "^0.10.4",
"url-loader": "^1.0.1",
"vue-jest": "^1.0.2",
"vue-loader": "^15.0.9",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"stylelint-webpack-plugin": "^0.7.0",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"webpack": "^2.6.1",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.2",
"webpack-pwa-manifest": "^3.7.1",
"worker-loader": "^1.1.1"
"webpack-merge": "^4.1.0",
"worker-loader": "^0.8.1"
},
"engines": {
"node": ">= 8.0.0",
"npm": ">= 5.0.0"
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
"not ie <= 8"
]
}

View File

@ -1,54 +0,0 @@
const pandocPath = process.env.PANDOC_PATH || 'pandoc';
const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;
const dropboxAppKey = process.env.DROPBOX_APP_KEY;
const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
const githubClientId = process.env.GITHUB_CLIENT_ID;
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
const giteeClientId = process.env.GITEE_CLIENT_ID;
const giteeClientSecret = process.env.GITEE_CLIENT_SECRET;
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleApiKey = process.env.GOOGLE_API_KEY;
const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;
const giteaClientId = process.env.GITEA_CLIENT_ID;
const giteaClientSecret = process.env.GITEA_CLIENT_SECRET;
const giteaUrl = process.env.GITEA_URL;
const gitlabClientId = process.env.GITLAB_CLIENT_ID;
const gitlabClientSecret = process.env.GITLAB_CLIENT_SECRET;
const gitlabUrl = process.env.GITLAB_URL;
exports.values = {
pandocPath,
wkhtmltopdfPath,
paypalReceiverEmail,
dropboxAppKey,
dropboxAppKeyFull,
githubClientId,
githubClientSecret,
giteeClientId,
giteeClientSecret,
googleClientId,
googleApiKey,
wordpressClientId,
giteaClientId,
giteaClientSecret,
giteaUrl,
gitlabClientId,
gitlabClientSecret,
gitlabUrl,
};
exports.publicValues = {
dropboxAppKey,
dropboxAppKeyFull,
githubClientId,
googleClientId,
googleApiKey,
wordpressClientId,
allowSponsorship: !!paypalReceiverEmail,
giteaClientId,
giteaUrl,
gitlabClientId,
gitlabUrl,
};

View File

@ -1,40 +0,0 @@
const request = require('request');
const conf = require('./conf');
function giteaToken(queryParam) {
return new Promise((resolve, reject) => {
request({
method: 'POST',
url: `${conf.values.giteaUrl}/login/oauth/access_token`,
headers: {
'content-type': 'application/json',
},
json: true,
body: {
...queryParam,
client_id: conf.values.giteaClientId,
client_secret: conf.values.giteaClientSecret,
},
}, (err, res, body) => {
if (err) {
reject(err);
}
const token = body.access_token;
if (token) {
resolve(body);
} else {
reject(res.statusCode + ',body:' + JSON.stringify(body));
}
});
});
}
exports.giteaToken = (req, res) => {
giteaToken(req.query)
.then(
tokenBody => res.send(tokenBody),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
};

View File

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

View File

@ -1,22 +1,21 @@
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
const request = require('request');
const conf = require('./conf');
var qs = require('qs');
var request = require('request');
function githubToken(clientId, code) {
return new Promise((resolve, reject) => {
return new Promise(function (resolve, reject) {
request({
method: 'POST',
url: 'https://github.com/login/oauth/access_token',
qs: {
client_id: clientId,
client_secret: conf.values.githubClientSecret,
code,
client_secret: process.env.GITHUB_SECRET,
code: code
},
}, (err, res, body) => {
}, function(err, res, body) {
if (err) {
reject(err);
}
const token = qs.parse(body).access_token;
var token = qs.parse(body).access_token;
if (token) {
resolve(token);
} else {
@ -26,12 +25,11 @@ function githubToken(clientId, code) {
});
}
exports.githubToken = (req, res) => {
exports.githubToken = function (req, res) {
githubToken(req.query.clientId, req.query.code)
.then(
token => res.send(token),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
.then(function (token) {
res.send(token);
}, function (err) {
res.status(400).send(err ? err.message || err.toString() : 'bad_code');
});
};

View File

@ -1,40 +0,0 @@
const request = require('request');
const conf = require('./conf');
function gitlabToken(queryParam) {
return new Promise((resolve, reject) => {
request({
method: 'POST',
url: `${conf.values.gitlabUrl}/oauth/token`,
headers: {
'content-type': 'application/json',
},
json: true,
qs: {
...queryParam,
client_id: conf.values.gitlabClientId,
client_secret: conf.values.gitlabClientSecret,
},
}, (err, res, body) => {
if (err) {
reject(err);
}
const token = body.access_token;
if (token) {
resolve(body);
} else {
reject(res.statusCode + ',body:' + JSON.stringify(body));
}
});
});
}
exports.gitlabToken = (req, res) => {
gitlabToken(req.query)
.then(
tokenBody => res.send(tokenBody),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
};

View File

@ -1,89 +1,69 @@
const compression = require('compression');
const serveStatic = require('serve-static');
const bodyParser = require('body-parser');
const path = require('path');
const github = require('./github');
const gitee = require('./gitee');
const gitea = require('./gitea');
const gitlab = require('./gitlab');
const pdf = require('./pdf');
const pandoc = require('./pandoc');
const conf = require('./conf');
var compression = require('compression');
var serveStatic = require('serve-static');
var path = require('path');
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
module.exports = (app) => {
module.exports = function (app, serveV4) {
// Use gzip compression
if (process.env.NODE_ENV === 'production') {
// Enable CORS for fonts
app.all('*', (req, res, next) => {
if (/\.(eot|ttf|woff2?|svg)$/.test(req.url)) {
app.all('*', function(req, res, next) {
// Force HTTPS on stackedit.io
if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect('https://stackedit.io' + req.url);
}
// Enable CORS for fonts
if (/\.(eot|ttf|woff|svg)$/.test(req.url)) {
res.header('Access-Control-Allow-Origin', '*');
}
next();
});
// Use gzip compression
app.use(compression());
}
app.get('/oauth2/githubToken', github.githubToken);
app.get('/oauth2/giteeToken', gitee.giteeToken);
app.get('/oauth2/giteaToken', gitea.giteaToken);
app.get('/oauth2/gitlabToken', gitlab.gitlabToken);
app.get('/conf', (req, res) => res.send(conf.publicValues));
app.post('/pdfExport', pdf.generate);
app.post('/pandocExport', pandoc.generate);
app.get('/giteeClientId', (req, res) => {
const giteeClientIds = conf.values.giteeClientId.split(',');
// 仅一个 则直接返回
if (giteeClientIds.length === 1) {
res.send(giteeClientIds[0]);
return;
}
// 是否随机一个clientId 默认第一个 如果random 为1 则使用随机 避免单个应用接口次数用满无法自动切换其他应用
const random = req.query.random;
if (!random) {
res.send(giteeClientIds[0]);
return;
}
// 随机一个 排除第一个 因为第一个应用接口次数用完了
const clientId = giteeClientIds[Math.floor(((giteeClientIds.length - 1) * Math.random())) + 1];
res.send(clientId);
});
// Serve landing.html
app.get('/', (req, res) => res.sendFile(resolvePath('static/landing/index.html')));
// Serve privacy_policy.html
app.get('/privacy_policy.html', (req, res) => res.sendFile(resolvePath('static/landing/privacy_policy.html')));
// Serve sitemap.xml
app.get('/sitemap.xml', (req, res) => res.sendFile(resolvePath('static/sitemap.xml')));
// Serve callback.html
app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
// Google Drive action receiver
app.get('/googleDriveAction', (req, res) =>
res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));
// Serve the static folder with 30 day max-age
app.use('/themes', serveStatic(resolvePath('static/themes'), {
maxAge: '5d',
}));
app.get('/oauth2/githubToken', require('./github').githubToken);
if (serveV4) {
app.post('/pdfExport', require('../stackedit_v4/app/pdf').export);
app.post('/sshPublish', require('../stackedit_v4/app/ssh').publish);
app.post('/picasaImportImg', require('../stackedit_v4/app/picasa').importImg);
app.get('/downloadImport', require('../stackedit_v4/app/download').importPublic);
}
// Serve style.css with 1 day max-age
app.get('/style.css', (req, res) => res.sendFile(resolvePath('dist/style.css'), {
maxAge: '1d',
}));
// 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 callback.html in /app
app.get('/oauth2/callback', function(req, res) {
res.sendFile(path.join(__dirname, '../static/oauth2/callback.html'));
});
// Serve static resources
if (process.env.NODE_ENV === 'production') {
if (serveV4) {
// Serve landing.html in /
app.get('/', function(req, res) {
res.sendFile(require.resolve('../stackedit_v4/views/landing.html'));
});
// Serve editor.html in /viewer
app.get('/editor', function(req, res) {
res.sendFile(require.resolve('../stackedit_v4/views/editor.html'));
});
// Serve viewer.html in /viewer
app.get('/viewer', function(req, res) {
res.sendFile(require.resolve('../stackedit_v4/views/viewer.html'));
});
}
// Serve index.html in /app
app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
app.get('/app', function(req, res) {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
// Serve the static folder with 1 year max-age
app.use('/static', serveStatic(resolvePath('dist/static'), {
maxAge: '1y',
}));
app.use(serveStatic(path.join(__dirname, '../dist')));
app.use(serveStatic(resolvePath('dist')));
if (serveV4) {
app.use(serveStatic(path.dirname(require.resolve('../stackedit_v4/public/cache.manifest'))));
// Error 404
app.use(function(req, res) {
res.status(404).sendFile(require.resolve('../stackedit_v4/views/error_404.html'));
});
}
}
};

View File

@ -1,141 +0,0 @@
/* global window */
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const conf = require('./conf');
const outputFormats = {
asciidoc: 'text/plain',
context: 'application/x-latex',
epub: 'application/epub+zip',
epub3: 'application/epub+zip',
latex: 'application/x-latex',
odt: 'application/vnd.oasis.opendocument.text',
pdf: 'application/pdf',
rst: 'text/plain',
rtf: 'application/rtf',
textile: 'text/plain',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
const highlightStyles = [
'pygments',
'kate',
'monochrome',
'espresso',
'zenburn',
'haddock',
'tango',
];
const readJson = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return {};
}
};
exports.generate = (req, res) => {
let pandocError = '';
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
? req.query.format
: 'pdf';
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('--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

@ -1,178 +1,145 @@
/* global window,MathJax */
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const conf = require('./conf');
var spawn = require('child_process').spawn;
var fs = require('fs');
var path = require('path');
var os = require('os');
var request = require('request');
/* eslint-disable no-var, prefer-arrow-callback, func-names */
function waitForJavaScript() {
if (window.MathJax) {
// Amazon EC2: fix TeX font detection
MathJax.Hub.Register.StartupHook('HTML-CSS Jax Startup', function () {
var htmlCss = MathJax.OutputJax['HTML-CSS'];
htmlCss.Font.checkWebFont = function (check, font, callback) {
if (check.time(callback)) {
return;
}
if (check.total === 0) {
htmlCss.Font.testFont(font);
setTimeout(check, 200);
} else {
callback(check.STATUS.OK);
}
};
});
MathJax.Hub.Queue(function () {
window.status = 'done';
});
} else {
setTimeout(function () {
window.status = 'done';
}, 2000);
}
if(window.MathJax) {
// Amazon EC2: fix TeX font detection
MathJax.Hub.Register.StartupHook("HTML-CSS Jax Startup",function () {
var HTMLCSS = MathJax.OutputJax["HTML-CSS"];
HTMLCSS.Font.checkWebFont = function (check,font,callback) {
if (check.time(callback)) {
return;
}
if (check.total === 0) {
HTMLCSS.Font.testFont(font);
setTimeout(check,200);
} else {
callback(check.STATUS.OK);
}
};
});
MathJax.Hub.Queue(function () {
window.status = 'done';
});
}
else {
setTimeout(function() {
window.status = 'done';
}, 2000);
}
}
/* eslint-disable no-var, prefer-arrow-callback, func-names */
const authorizedPageSizes = [
'A3',
'A4',
'Legal',
'Letter',
var authorizedPageSizes = [
'A3',
'A4',
'Legal',
'Letter'
];
const readJson = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return {};
}
};
exports.generate = (req, res) => {
let wkhtmltopdfError = '';
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();
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.');
}
});
exports.export = function(req, res, next) {
function onError(err) {
next(err);
}
function onUnknownError() {
res.statusCode = 400;
res.end('Unknown error');
}
function onUnauthorizedError() {
res.statusCode = 401;
res.end('Unauthorized');
}
function onTimeout() {
res.statusCode = 408;
res.end('Request timeout');
}
request({
uri: 'https://monetizejs.com/api/payments',
qs: {
access_token: req.query.token
},
json: true
}, function (err, paymentsRes, payments) {
var authorized = payments && payments.app == 'ESTHdCYOi18iLhhO' && (
(payments.chargeOption && payments.chargeOption.alias == 'once') ||
(payments.subscriptionOption && payments.subscriptionOption.alias == 'yearly'));
if(err || paymentsRes.statusCode != 200 || !authorized) {
return onUnauthorizedError();
}
var options, params = [];
try {
options = JSON.parse(req.query.options);
}
catch(e) {
options = {};
}
// Margins
var marginTop = parseInt(options.marginTop);
params.push('-T', isNaN(marginTop) ? 25 : marginTop);
var marginRight = parseInt(options.marginRight);
params.push('-R', isNaN(marginRight) ? 25 : marginRight);
var marginBottom = parseInt(options.marginBottom);
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom);
var marginLeft = parseInt(options.marginLeft);
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft);
// Header
options.headerCenter && params.push('--header-center', options.headerCenter);
options.headerLeft && params.push('--header-left', options.headerLeft);
options.headerRight && params.push('--header-right', options.headerRight);
options.headerFontName && params.push('--header-font-name', options.headerFontName);
options.headerFontSize && params.push('--header-font-size', options.headerFontSize);
// Footer
options.footerCenter && params.push('--footer-center', options.footerCenter);
options.footerLeft && params.push('--footer-left', options.footerLeft);
options.footerRight && params.push('--footer-right', options.footerRight);
options.footerFontName && params.push('--footer-font-name', options.footerFontName);
options.footerFontSize && params.push('--footer-font-size', options.footerFontSize);
// Page size
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize);
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
var filePath = path.join(os.tmpDir(), Date.now() + '.pdf');
var binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
params.push('--run-script', waitForJavaScript.toString() + 'waitForJavaScript()');
params.push('--window-status', 'done');
var wkhtmltopdf = spawn(binPath, params.concat('-', filePath), {
stdio: [
'pipe',
'ignore',
'ignore'
]
});
var timeoutId = setTimeout(function() {
timeoutId = undefined;
wkhtmltopdf.kill();
}, 30000);
wkhtmltopdf.on('error', onError);
wkhtmltopdf.stdin.on('error', onError);
wkhtmltopdf.on('close', function(code) {
if(!timeoutId) {
return onTimeout();
}
clearTimeout(timeoutId);
if(code) {
return onUnknownError();
}
var readStream = fs.createReadStream(filePath);
readStream.on('open', function() {
readStream.pipe(res);
});
readStream.on('close', function() {
fs.unlink(filePath, function() {
});
});
readStream.on('error', onUnknownError);
});
req.pipe(wkhtmltopdf.stdin);
});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><path d="M20.512 178.499c-3.359-.884-6.258-2.184-8.931-4.006-2.257-1.538-5.556-4.717-6.811-6.563-1.532-2.255-3.293-6.117-4.011-8.795-.732-2.732-.743-3.82-.757-69.395-.013-65.245.002-66.679.72-69.483C3.259 10.341 11.117 2.797 21.251.546c2.914-.647 133.08-.76 136.223-.118 8.509 1.738 15.198 6.846 19.068 14.564 3.078 6.135 2.803-.617 2.943 72.231.09 46.349.007 65.808-.288 68.232-1.386 11.345-9.211 20.143-20.471 23.019-2.88.735-3.882.746-69.275.726-63.227-.019-66.474-.052-68.939-.701z" fill="#f57d00"/><path d="M115.162 144.835c8.064-1.1 14.384-4.333 20.313-10.39 4.289-4.382 6.974-9.125 8.728-15.419.729-2.615.79-3.888.924-19.242.101-11.588.017-17.015-.285-18.385-.437-1.986-1.677-3.83-3.092-4.599-.435-.237-3.224-.538-6.198-.67-4.982-.221-5.54-.318-7.113-1.24-2.494-1.462-3.181-3.041-3.188-7.327-.013-8.189-3.421-15.792-10.155-22.654-4.797-4.889-10.149-8.198-16.257-10.052-1.462-.444-4.736-.595-15.702-.725-17.207-.203-21.026.15-26.884 2.483-10.8 4.302-18.56 13.368-21.39 24.99-.532 2.183-.635 5.682-.761 25.779-.157 25.177.016 28.874 1.59 33.864 1.299 4.122 2.611 6.648 5.313 10.234 5.146 6.83 12.86 11.763 20.572 13.156 3.67.663 48.948.829 53.585.197z" fill="#fff"/><path d="M67.575 75.717c-4.123-1.136-5.663-7.051-2.633-10.111 1.937-1.955 2.472-2.029 14.595-2.029 10.883 0 11.249.023 12.848.831 2.31 1.167 3.314 2.812 3.314 5.432 0 2.367-.943 4.025-3.046 5.357-1.129.716-1.804.76-12.467.823-6.584.039-11.83-.087-12.611-.303zM67.058 115.526c-1.769-.771-3.417-2.913-3.702-4.813-.272-1.809.638-4.296 2.032-5.558 1.757-1.59 2.528-1.643 24.134-1.66 22.227-.017 22.111-.027 24.219 1.941 2.976 2.78 2.349 7.728-1.239 9.76l-3.686.6-19.213.224c-16.883.198-21.666-.111-22.545-.494z" fill="#f57d00"/></svg>
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
<g>
<path d="M20.512,178.499c-3.359,-0.884 -6.258,-2.184 -8.931,-4.006c-2.257,-1.538 -5.556,-4.717 -6.811,-6.563c-1.532,-2.255 -3.293,-6.117 -4.011,-8.795c-0.732,-2.732 -0.743,-3.82 -0.757,-69.395c-0.013,-65.245 0.002,-66.679 0.72,-69.483c2.537,-9.916 10.395,-17.46 20.529,-19.711c2.914,-0.647 133.08,-0.76 136.223,-0.118c8.509,1.738 15.198,6.846 19.068,14.564c3.078,6.135 2.803,-0.617 2.943,72.231c0.09,46.349 0.007,65.808 -0.288,68.232c-1.386,11.345 -9.211,20.143 -20.471,23.019c-2.88,0.735 -3.882,0.746 -69.275,0.726c-63.227,-0.019 -66.474,-0.052 -68.939,-0.701l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
<path d="M115.162,144.835c8.064,-1.1 14.384,-4.333 20.313,-10.39c4.289,-4.382 6.974,-9.125 8.728,-15.419c0.729,-2.615 0.79,-3.888 0.924,-19.242c0.101,-11.588 0.017,-17.015 -0.285,-18.385c-0.437,-1.986 -1.677,-3.83 -3.092,-4.599c-0.435,-0.237 -3.224,-0.538 -6.198,-0.67c-4.982,-0.221 -5.54,-0.318 -7.113,-1.24c-2.494,-1.462 -3.181,-3.041 -3.188,-7.327c-0.013,-8.189 -3.421,-15.792 -10.155,-22.654c-4.797,-4.889 -10.149,-8.198 -16.257,-10.052c-1.462,-0.444 -4.736,-0.595 -15.702,-0.725c-17.207,-0.203 -21.026,0.15 -26.884,2.483c-10.8,4.302 -18.56,13.368 -21.39,24.99c-0.532,2.183 -0.635,5.682 -0.761,25.779c-0.157,25.177 0.016,28.874 1.59,33.864c1.299,4.122 2.611,6.648 5.313,10.234c5.146,6.83 12.86,11.763 20.572,13.156c3.67,0.663 48.948,0.829 53.585,0.197Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M67.575,75.717c-4.123,-1.136 -5.663,-7.051 -2.633,-10.111c1.937,-1.955 2.472,-2.029 14.595,-2.029c10.883,0 11.249,0.023 12.848,0.831c2.31,1.167 3.314,2.812 3.314,5.432c0,2.367 -0.943,4.025 -3.046,5.357c-1.129,0.716 -1.804,0.76 -12.467,0.823c-6.584,0.039 -11.83,-0.087 -12.611,-0.303l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
<path d="M67.058,115.526c-1.769,-0.771 -3.417,-2.913 -3.702,-4.813c-0.272,-1.809 0.638,-4.296 2.032,-5.558c1.757,-1.59 2.528,-1.643 24.134,-1.66c22.227,-0.017 22.111,-0.027 24.219,1.941c2.976,2.78 2.349,7.728 -1.239,9.76l-3.686,0.6l-19.213,0.224c-16.883,0.198 -21.666,-0.111 -22.545,-0.494l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,4 +0,0 @@
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<path d="M405.365,303.996c0,20.375 -11.207,30.563 -31.582,31.582l-248.584,0c-20.376,0 -31.583,-10.188 -31.583,-31.582c0,-20.376 11.207,-30.564 31.583,-31.583l249.602,0c20.376,1.019 30.564,11.207 30.564,31.583Zm-30.564,46.864l-249.602,0c-20.376,0 -31.583,10.188 -31.583,31.582c0,20.376 11.207,30.564 31.583,31.583l249.602,0c20.376,0 31.583,-10.188 31.583,-31.583c-1.019,-21.394 -11.207,-31.582 -31.583,-31.582Zm77.428,-172.175c-20.376,0 -31.582,10.188 -31.582,30.563l0,172.175c0,20.376 11.206,30.564 31.582,31.583c30.564,-1.019 46.864,-31.583 46.864,-93.729l0,-77.427c0,-41.771 -16.3,-62.146 -46.864,-63.165Zm-404.458,0c-30.564,1.019 -46.864,21.394 -46.864,63.165l0,77.427c0,62.146 16.3,92.71 46.864,93.729c20.376,0 31.582,-10.188 31.582,-31.583l0,-171.156c-1.019,-20.375 -11.206,-30.563 -31.582,-31.582Zm404.458,-15.282c0,-51.958 -27.507,-76.409 -77.428,-77.428l-249.602,0c-50.94,1.019 -77.428,26.489 -77.428,77.428c30.563,0 46.864,16.301 46.864,46.864c0,30.564 16.301,46.864 46.864,46.864l218.021,0c30.563,0 46.864,-16.3 46.864,-46.864c-1.019,-31.582 16.3,-45.845 45.845,-46.864Z" style="fill:#e42528;fill-rule:nonzero;"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="32" height="32"><path d="M395.9 484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z" fill="#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z" fill="#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z" fill="#609926"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +1,4 @@
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 58">
<g fill="none" fill-rule="evenodd">

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 1011 B

View File

@ -1,6 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 58">
<g fill="none" fill-rule="evenodd">
<path d="m1324.62 140c-16.355 0-29.616 13.219-29.616 29.527 0 13.04 8.485 24.11 20.256 28.01 1.482.27 2.02-.642 2.02-1.425 0-.7-.025-2.557-.04-5.02-8.238 1.784-9.976-3.958-9.976-3.958-1.347-3.411-3.289-4.317-3.289-4.317-2.689-1.832.204-1.796.204-1.796 2.973.21 4.536 3.043 4.536 3.043 2.642 4.511 6.931 3.208 8.62 2.454.269-1.909 1.033-3.21 1.88-3.948-6.576-.745-13.491-3.279-13.491-14.592 0-3.223 1.155-5.858 3.049-7.922-.305-.747-1.322-3.748.289-7.814 0 0 2.487-.794 8.145 3.03 2.362-.656 4.896-.982 7.415-.995 2.515.013 5.05.339 7.415.995 5.655-3.821 8.136-3.03 8.136-3.03 1.616 4.065.6 7.07.295 7.814 1.898 2.064 3.045 4.7 3.045 7.922 0 11.343-6.925 13.838-13.524 14.569 1.064.912 2.01 2.713 2.01 5.468 0 3.946-.036 7.13-.036 8.098 0 .79.533 1.709 2.036 1.421 11.758-3.913 20.238-14.971 20.238-28.01 0-16.309-13.262-29.527-29.62-29.527" transform="translate(-1295-140)" fill="#fff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

View File

@ -1,12 +0,0 @@
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1"
xmlns="http://www.w3.org/2000/svg"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<path d="M14.581,28.019l5.369,-16.526l-10.738,0l5.369,16.526l0,0Z" style="fill:#e24329;"/>
<path d="M14.581,28.019l-5.37,-16.526l-7.525,0l12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
<path d="M1.686,11.493l-1.632,5.022c-0.148,0.458 0.015,0.96 0.404,1.243l14.123,10.261l-12.895,-16.526l0,0Z" style="fill:#fca326;"/>
<path d="M1.686,11.493l7.526,0l-3.235,-9.953c-0.166,-0.512 -0.89,-0.512 -1.057,0l-3.234,9.953l0,0Z" style="fill:#e24329;"/>
<path d="M14.581,28.019l5.369,-16.526l7.526,0l-12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
<path d="M27.476,11.493l1.631,5.022c0.149,0.458 -0.014,0.96 -0.404,1.243l-14.122,10.261l12.895,-16.526l0,0Z" style="fill:#fca326;"/>
<path d="M27.476,11.493l-7.526,0l3.234,-9.953c0.167,-0.512 0.891,-0.512 1.058,0l3.234,9.953l0,0Z" style="fill:#e24329;"/>
</svg>

Before

Width:  |  Height:  |  Size: 1005 B

View File

@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
<defs>
<path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
</defs>
<clipPath id="b">
<use xlink:href="#a" overflow="visible"/>
</clipPath>
<path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
<path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
<path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
<path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
</svg>

Before

Width:  |  Height:  |  Size: 729 B

View File

@ -1,25 +0,0 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve"> <image id="image0" width="32" height="32" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB7FBMVEUAAAB3AP+IAP+IAP+H
AP+IAP+IAP+GAP+HAP+AAP+GAP+IAP+IAP+HAP+KAP+OAP+IAP+IAP+IAP9xAP+IAP+IAP+KAP+F
AP+HAP+IAP+IAP+DAP+AAP+HAP+HAP+AAP+IAP+IAP+JAP+HAP+IAP+HAP+IAP+HAP+HAP+IAP+J
AP+GAP+IAP+IAP+IAP+HAP+HAP+HAP+HAP+IAP+IAP+IAP+HAP+IAP+HAP+IAP+IAP+HAP+HAP+H
AP+DAP+HAP+DAP+HAP+IAP+IAP+JAv+mQv+8cP+9c/+sTv+MCf+iOP/s1//////05/+mQf+hNv/6
9v/37/+QE//kx//Yrf+pR/+nRP/YrP+1Yf+SF//+/v+TGP/Df/+gNP/Egf+lPv+5af+aKP/8+P+j
O//asf/27f+RFf+IAv/x4//37v/NlP/x4v+jOv+4Z//Rn/+LB/+IAf/Xqv+/eP+LBv/Wqf+7b/+3
Zv/euP+vVf/Nlv/8+v+bKf/Dgf/Ghv/Ol/+eMf/9/P+vU/+QEf/48f/Sof+UG//ctP/48P/v3v+M
CP/z5f/16/+aJ/+SFv/Zr//ozv/euf+cKv+WHv+tUP/r1P+mQP/Egv/Lkf+PEP/v3P/Jjf/duf/+
/f/fu/+kPf+2Y//Cff/Bev+xWP+TGf+sor+CAAAAQ3RSTlMAD1qczOv7nFkOJp72nSUJjfqLCdTS
IzLqL+kjCtXRCI+JJ/v5oJoR9/QNXVafmNDI8Oj++PzszJsP9SSO0iWKJ5/LDpe2AgAAAAFiS0dE
TPdvEPMAAAAHdElNRQfmBhwAJyh2NlUnAAAByUlEQVQ4y2NggANGJmYWVjY2VhZ2Dk4GTMDFzeMM
Bzy8fGjS/AKCzihAUEgYWZ5PxBkDiIoh5MVZnbEACUm4fiR5F1c3dw9PCFtKGiIvI4uQ9/L2AQJf
PwhPTh6sQAEh7x/gAwaBQRC+IkheSRmhINjHJyQ0LNzHJwLCVwFZooqQj4zyiYp0do728YmBiqgx
MKhrIBTEAk2Pc3aO9/FJgIpoajFoI3ksEaggKdk5xSc1DSakw6CLpCA9A6giM8snOwcupMegjxw2
uWA/5OUjRAwYDJEVFBSCFEQVIUSMGIxRgre4BKyiFC5gwmCKkC0rr3AuqASpqKqGiZkymMHla3x8
amG21MEEzRjMYcz6Bh+fRiDd1AxU4A0TZWFghjFbgOKtIEYbkNEOE7VgYIIxO4DinTAFXTBRDgZL
WFLsBor3AOlekBV9UEErYPLlhrL7geITJk6aPAXkz6lQQWZQcoYm12k+CDAdKi9oDUoQNhDOjJkw
6ahZLlAFQpAkB03SrbOjQNJz5s6DudAWmvTtYIm2Z/6ChYsWw0NOSozYZI8949hKI2ctGQFlVGlB
e5SsBwQOjkiZ14rZDkv+tmRiNjdjU2Z1skDO/gDtseT0Fzic2AAAACV0RVh0ZGF0ZTpjcmVhdGUA
MjAyMi0wNi0yOFQwMDozOTo0MCswMDowMPmC6NgAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDYt
MjhUMDA6Mzk6NDArMDA6MDCI31BkAAAAAElFTkSuQmCC" />
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill-rule="evenodd" stroke-linejoin="round" clip-rule="evenodd" stroke-miterlimit="1.414"><path d="M 24 6 c 0 -3.3 -2.7 -6 -6 -6 H 6 C 2.7 0 0 2.7 0 6 v 12 c 0 3.3 2.7 6 6 6 h 12 c 3.3 0 6 -2.7 6 -6 V 6 z" fill="none"/><clipPath id="prefix__a"><path d="M 24 6 c 0 -3.3 -2.7 -6 -6 -6 H 6 C 2.7 0 0 2.7 0 6 v 12 c 0 3.3 2.7 6 6 6 h 12 c 3.3 0 6 -2.7 6 -6 V 6 z"/></clipPath><g clip-path="url(#prefix__a)"><path d="M 24 0 H 0 l 12 12 L 24 0 z" fill="gold"/><path d="M 0 0 v 24 l 12 -12 L 0 0 z" fill="#a5c700"/><path d="M 0 24 h 24 L 12 12 L 0 24 z" fill="#ff8a00"/><path d="M 24 24 V 0 L 12 12 l 12 12 z" fill="#66aefd"/><path d="M 22.5 -1.5 L 12 9 l 3 3 L 25.5 1.5 l -3 -3 z" fill="url(#prefix___Linear2)"/><path d="M 25.5 22.5 L 15 12 l -3 3 l 10.5 10.5 l 3 -3 z" fill="url(#prefix___Linear3)"/><path d="M 1.5 25.5 L 12 15 l -3 -3 l -10.5 10.5 l 3 3 z" fill="url(#prefix___Linear4)"/><path d="M -1.5 1.5 L 9 12 l 3 -3 L 1.5 -1.5 l -3 3 z" fill="url(#prefix___Linear5)"/></g><path d="M 21.8 5.9 c 0 -2.2 -1.8 -4 -4 -4 H 6.3 c -2.2 0 -4 1.8 -4 4 v 11.5 c 0 2.2 1.8 4 4 4 h 11.5 c 2.2 0 4 -1.8 4 -4 V 5.9 z" fill="#ffffff"/><path d="M 4.6 6 H 6 V 4.2 h 1.4 V 6 h 1.7 V 4.2 h 1.4 V 6 h 1.4 v 1.7 h -1.4 v 1.9 h 1.4 v 1.7 h -1.4 v 1.8 H 9.1 v -1.8 H 7.4 v 1.8 H 6 v -1.8 H 4.6 V 9.6 H 6 V 7.7 H 4.6 V 6 z m 2.8 1.7 v 1.9 h 1.7 V 7.7 H 7.4 z M 10 14 v 6 h 4 v -2 h -2 v -2 h 2 v -2 H 10 z m 5 0 v 6 h 2 v -3 l 1 3 h 2 v -6 h -2 v 3 l -1 -3 h -2 z M 7 18 l 0 2 l 2 0 l 0 -2 l -2 0 Z" fill="#737373"/><defs><linearGradient id="prefix___Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.99995 -3 3 -2.99995 23.997 3.003)"><stop offset="0" stop-color="#66aefd"/><stop offset="1" stop-color="gold"/></linearGradient><linearGradient id="prefix___Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3 -2.99995 2.99995 3 20.999 24.003)"><stop offset="0" stop-color="#ff8a00"/><stop offset="1" stop-color="#66aefd"/></linearGradient><linearGradient id="prefix___Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.99995 3 -3 2.99995 -.003 21.001)"><stop offset="0" stop-color="#a5c700"/><stop offset="1" stop-color="#ff8a00"/></linearGradient><linearGradient id="prefix___Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3 2.99995 -2.99995 -3 2.997 .003)"><stop offset="0" stop-color="gold"/><stop offset="1" stop-color="#a5c700"/></linearGradient></defs></svg>
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126 126">
<path d="M103.5,0c3.762,0.003 7.395,0.971 10.725,2.716c0.966,0.506 1.808,1.221 2.712,1.831l-53.937,40.453l-53.937,-40.453c4.402,-3.289 8.02,-4.273 13.437,-4.547l81,0Z" style="fill:#ffe600;fill-rule:nonzero;"/>
<path d="M9.063,4.547l53.937,40.453l-58.04,72.55c-3.44,-4.417 -4.681,-8.528 -4.96,-14.05l0,-81c0.064,-5.221 1.801,-10.265 5.138,-14.312c0.914,-1.109 2.033,-2.033 3.05,-3.05l0.875,-0.591l0,0Z" style="fill:#bbd500;fill-rule:nonzero;"/>
<path d="M63,45l58.04,72.549l-0.178,0.263c-4.901,5.465 -10.068,7.82 -17.362,8.188l-81,0c-5.221,-0.065 -10.265,-1.801 -14.312,-5.138c-1.109,-0.915 -2.033,-2.034 -3.05,-3.05l-0.177,-0.262l58.039,-72.55l0,0Z" style="fill:#ff8a00;fill-rule:nonzero;"/>
<path d="M116.937,4.547c3.844,2.631 6.684,6.83 8.051,11.262c0.441,1.427 0.673,2.914 0.896,4.391c0.114,0.759 0.077,1.533 0.116,2.3l0,81c-0.023,3.748 -0.939,7.415 -2.716,10.725c-0.632,1.178 -1.496,2.216 -2.245,3.325l-58.039,-72.55l53.937,-40.453l0,0Z" style="fill:#75b7fd;fill-rule:nonzero;"/>
<path d="M32.063,12l61.874,0c7.767,0 14.063,6.296 14.063,14.063l0,61.874c0,7.767 -6.296,14.063 -14.063,14.063l-61.875,0c-7.766,0 -14.062,-6.296 -14.062,-14.063l0,-61.875c0,-7.766 6.296,-14.062 14.062,-14.062l0.001,0Z" style="fill:#fff;fill-rule:nonzero;"/>
<g>
<g>
<rect x="40.711" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
<rect x="56.263" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
</g>
<g>
<path d="M71.278,44.466l0,-8.841l-38.556,0l0,8.841l38.556,0Z" style="fill:#737373;fill-rule:nonzero;"/>
<rect x="32.722" y="55.195" width="38.556" height="8.842" style="fill:#737373;fill-rule:nonzero;"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,109 +1,53 @@
<template>
<div class="app" :class="classes" @keydown.esc="close">
<splash-screen v-if="!ready"></splash-screen>
<layout v-else></layout>
<modal></modal>
<splash-screen v-if="!ready"></splash-screen>
<div v-else class="app">
<layout></layout>
<modal v-if="showModal"></modal>
<notification></notification>
<context-menu></context-menu>
</div>
</template>
<script>
import '../styles';
import '../styles/markdownHighlighting.scss';
import '../styles/app.scss';
import Vue from 'vue';
import { mapState } from 'vuex';
import Layout from './Layout';
import Modal from './Modal';
import Notification from './Notification';
import ContextMenu from './ContextMenu';
import SplashScreen from './SplashScreen';
import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc';
import tempFileSvc from '../services/tempFileSvc';
import store from '../store';
import './common/vueGlobals';
import utils from '../services/utils';
import providerRegistry from '../services/providers/common/providerRegistry';
const themeClasses = {
light: ['app--light'],
dark: ['app--dark'],
};
// Global directives
Vue.directive('focus', {
inserted(el) {
el.focus();
},
});
export default {
components: {
Layout,
Modal,
Notification,
ContextMenu,
SplashScreen,
},
data: () => ({
ready: false,
}),
computed: {
classes() {
const result = themeClasses[store.getters['data/computedSettings'].colorTheme];
return Array.isArray(result) ? result : themeClasses.light;
...mapState([
'ready',
]),
showModal() {
return !!this.$store.getters['modal/config'];
},
},
methods: {
close() {
tempFileSvc.close();
},
//
viewFileByPath(path) {
// md
if (!path) {
return;
}
const currDirNode = store.getters['explorer/selectedNodeFolder'];
if (path.slice(-3) === '.md') {
const rootNode = store.getters['explorer/rootNode'];
const node = utils.findNodeByPath(rootNode, currDirNode, path);
if (!node) {
return;
}
store.commit('explorer/setSelectedId', node.item.id);
// Prevent from freezing the UI while loading the file
setTimeout(() => {
store.commit('file/setCurrentId', node.item.id);
}, 10);
} else {
const workspace = store.getters['workspace/currentWorkspace'];
const provider = providerRegistry.providersById[workspace.providerId];
if (provider == null) {
return;
}
const absolutePath = utils.getAbsoluteFilePath(currDirNode, path);
const url = provider.getFilePathUrl(absolutePath);
if (url) {
window.open(url, '_blank');
}
}
},
},
async created() {
window.viewFileByPath = this.viewFileByPath;
try {
await syncSvc.init();
await networkSvc.init();
// store
const editTheme = localStorage.getItem('theme/currEditTheme');
store.dispatch('theme/setEditTheme', editTheme || 'default');
// store
const previewTheme = localStorage.getItem('theme/currPreviewTheme');
store.dispatch('theme/setPreviewTheme', previewTheme || 'default');
this.ready = true;
tempFileSvc.setReady();
} catch (err) {
if (err && err.message === 'RELOAD') {
window.location.reload();
} else if (err && err.message !== 'RELOAD') {
console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err);
}
}
},
};
</script>
<style lang="scss">
@import 'common/app';
.app__spash-screen {
margin: 0 auto;
max-width: 600px;
height: 100%;
background: no-repeat center url('../assets/logo.svg');
background-size: contain;
}
</style>

View File

@ -1,42 +1,37 @@
<template>
<div class="button-bar">
<div class="button-bar__inner button-bar__inner--top">
<button class="button-bar__button button-bar__button--navigation-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'切换导航栏'">
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()">
<icon-navigation-bar></icon-navigation-bar>
</button>
<button class="button-bar__button button-bar__button--side-preview-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'切换侧边预览'">
</div>
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()">
<icon-side-preview></icon-side-preview>
</button>
<button class="button-bar__button button-bar__button--editor-toggler button" @click="toggleEditor(false)" v-title="'阅读模式'">
</div>
<div class="button-bar__button" @click="toggleEditor(false)">
<icon-eye></icon-eye>
</button>
</div>
</div>
<div class="button-bar__inner button-bar__inner--bottom">
<button class="button-bar__button button-bar__button--focus-mode-toggler button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'切换对焦模式'">
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()">
<icon-target></icon-target>
</button>
<button class="button-bar__button button-bar__button--scroll-sync-toggler button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'切换滚动同步'">
</div>
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()">
<icon-scroll-sync></icon-scroll-sync>
</button>
<button class="button-bar__button button-bar__button--status-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'切换状态栏'">
</div>
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()">
<icon-status-bar></icon-status-bar>
</button>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'light',
]),
...mapGetters('data', [
'layoutSettings',
]),
},
computed: mapGetters('data', [
'localSettings',
]),
methods: mapActions('data', [
'toggleNavigationBar',
'toggleEditor',
@ -48,9 +43,8 @@ export default {
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
<style lang="scss">
.button-bar {
position: absolute;
width: 100%;
@ -66,44 +60,19 @@ export default {
}
.button-bar__button {
cursor: pointer;
color: rgba(0, 0, 0, 0.2);
display: block;
width: 26px;
height: 26px;
padding: 2px;
margin: 3px 0;
.app--dark & {
color: rgba(255, 255, 255, 0.15);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.2);
.app--dark & {
color: rgba(255, 255, 255, 0.15);
background-color: $navbar-hover-background;
}
color: rgba(0, 0, 0, 0.4);
}
}
.button-bar__button--on {
color: rgba(0, 0, 0, 0.4);
.app--dark & {
color: rgba(255, 255, 255, 0.4);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.4);
.app--dark & {
color: rgba(255, 255, 255, 0.4);
}
}
}
</style>

View File

@ -4,24 +4,23 @@
<script>
import Prism from 'prismjs';
import cledit from '../services/editor/cledit';
import cledit from '../libs/cledit';
export default {
props: ['value', 'lang', 'disabled', 'scrollClass'],
props: ['value', 'lang', 'disabled'],
mounted() {
const preElt = this.$el;
let scrollElt = preElt;
const scrollCls = this.scrollClass || 'modal';
while (scrollElt && !scrollElt.classList.contains(scrollCls)) {
while (scrollElt && !scrollElt.classList.contains('modal')) {
scrollElt = scrollElt.parentNode;
}
if (scrollElt) {
const clEditor = cledit(preElt, scrollElt);
clEditor.on('contentChanged', value => this.$emit('changed', value));
clEditor.init({
content: this.value,
sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
});
clEditor.on('contentChanged', value => this.$emit('changed', value));
clEditor.toggleEditable(!this.disabled);
}
},
@ -29,13 +28,14 @@ export default {
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.code-editor {
margin: 0;
font-family: $font-family-monospace;
font-size: $font-size-monospace;
font-variant-ligatures: no-common-ligatures;
white-space: pre-wrap;
word-break: break-word;
word-wrap: normal;
height: auto;
@ -44,10 +44,6 @@ export default {
overflow: auto;
padding: 0.2em 0.4em;
.app--dark & {
caret-color: $editor-color-dark-low;
}
* {
line-height: $line-height-base;
font-size: inherit !important;

View File

@ -1,79 +0,0 @@
<template>
<div class="context-menu" v-if="items.length" @click="close()" @contextmenu.prevent="close()">
<div class="context-menu__inner flex flex--column" :style="{ left: coordinates.left + 'px', top: coordinates.top + 'px' }" @click.stop>
<div v-for="(item, idx) in items" :key="idx">
<div class="context-menu__separator" v-if="item.type === 'separator'"></div>
<div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
<a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import store from '../store';
export default {
computed: {
...mapState('contextMenu', [
'coordinates',
'items',
'resolve',
]),
},
methods: {
close(item = null) {
this.resolve(item);
store.dispatch('contextMenu/close');
},
},
};
</script>
<style lang="scss">
.context-menu {
position: absolute;
width: 100%;
height: 100%;
font-size: 14px;
line-height: 18px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
user-select: none;
}
$padding: 5px;
.context-menu__inner {
position: absolute;
background-color: #ebebeb;
border-radius: $padding;
padding: $padding 0;
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.16), 0 3px 10px 1px rgba(0, 0, 0, 0.12);
}
.context-menu__item {
display: block;
color: #333;
text-decoration: none;
padding: 0 25px;
}
a.context-menu__item {
&:active,
&:focus,
&:hover {
background-color: #338dfc;
color: #fff;
}
}
.context-menu__item--disabled {
color: #aaa;
}
.context-menu__separator {
border-top: 2px solid #dcdcdd;
margin: $padding 0;
}
</style>

View File

@ -1,31 +1,15 @@
<template>
<div class="editor" ondrop="return false;">
<div class="editor">
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
<comment-list v-if="styles.editorGutterWidth"></comment-list>
<editor-new-discussion-button v-if="!isCurrentTemp"></editor-new-discussion-button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import CommentList from './gutters/CommentList';
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
import store from '../store';
import editorSvc from '../services/editorSvc';
import imageSvc from '../services/imageSvc';
import utils from '../services/utils';
export default {
components: {
CommentList,
EditorNewDiscussionButton,
},
computed: {
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('layout', [
'styles',
]),
@ -33,104 +17,11 @@ export default {
'computedSettings',
]),
},
methods: {
async processUpload(items) {
let file = null;
if (!items || items.length === 0) {
return;
}
for (let i = 0; i < items.length; i += 1) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
if (!file) {
return;
}
const imgId = utils.uid();
store.dispatch('img/setCurrImgId', imgId);
editorSvc.pagedownEditor.uiManager.doClick('imageUploading');
try {
const { url, error } = await imageSvc.updateImg(file);
//
if (error) {
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `[图片上传失败...(image-${imgId})]`);
store.dispatch('notification/error', error);
return;
}
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `![输入图片说明](${url})`);
} catch (err) {
console.error(err); // eslint-disable-line no-console
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `[图片上传失败...(image-${imgId})]`);
store.dispatch('notification/error', err);
}
},
},
mounted() {
//
const currImgStorageStr = localStorage.getItem('img/checkedStorage');
if (currImgStorageStr) {
store.commit('img/changeCheckedStorage', JSON.parse(currImgStorageStr));
}
//
const workspaceImgPath = localStorage.getItem('img/workspaceImgPath');
if (workspaceImgPath) {
store.commit('img/setWorkspaceImgPath', JSON.parse(workspaceImgPath));
}
const editorElt = this.$el.querySelector('.editor__inner');
const onDiscussionEvt = cb => (evt) => {
let elt = evt.target;
while (elt && elt !== editorElt) {
if (elt.discussionId) {
cb(elt.discussionId);
return;
}
elt = elt.parentNode;
}
};
const classToggler = toggle => (discussionId) => {
editorElt.getElementsByClassName(`discussion-editor-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.toggle('discussion-editor-highlighting--hover', toggle));
document.getElementsByClassName(`comment--discussion-${discussionId}`)
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
};
editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
store.commit('discussion/setCurrentDiscussionId', discussionId);
}));
editorElt.addEventListener('drop', (event) => {
const transItems = event.dataTransfer.items;
this.processUpload(transItems);
});
editorElt.addEventListener('paste', (event) => {
const pasteItems = (event.clipboardData || window.clipboardData).items;
this.processUpload(pasteItems);
});
this.$watch(
() => store.state.discussion.currentDiscussionId,
(discussionId, oldDiscussionId) => {
if (oldDiscussionId) {
editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`)
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
}
if (discussionId) {
editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
}
},
);
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.editor {
position: absolute;
@ -146,6 +37,7 @@ export default {
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
caret-color: #000;
* {
line-height: $line-height-base;
@ -155,6 +47,11 @@ export default {
font-family: inherit;
}
.discussion-highlight,
.find-replace-highlight {
background-color: transparentize(#ffe400, 0.5);
}
.hide {
display: none;
}

View File

@ -1,197 +0,0 @@
<template>
<div class="editor-in-page-buttons">
<ul>
<li :title="`查找 ${mod}+F`">
<a @click="showFind"><icon-search></icon-search></a>
</li>
<li :title="`替换 ${mod}+Alt+F`">
<a @click="showFindReplace"><icon-find-replace></icon-find-replace></a>
</li>
<li title="切换编辑主题">
<dropdown-menu :selected="selectedTheme" :options="allThemes" :closeOnItemClick="false" @change="changeTheme">
<icon-select-theme></icon-select-theme>
</dropdown-menu>
</li>
<li class="after">
<icon-ellipsis></icon-ellipsis>
</li>
</ul>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import store from '../store';
import editorSvc from '../services/editorSvc';
import DropdownMenu from './common/DropdownMenu';
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
export default {
components: {
DropdownMenu,
},
data: () => ({
mod,
allThemes: [{
name: '默认主题',
value: 'default',
}, {
name: '天蓝黑',
value: 'azure',
}, {
name: '冰山黑',
value: 'iceberg_contrast',
}, {
name: '黎明白',
value: 'dawn',
}, {
name: '孔雀黑',
value: 'peacock',
}, {
name: '薄荷黑',
value: 'mintchoc',
}, {
name: '薄荷绿',
value: 'spearmint',
}, {
name: '暗蓝黑',
value: 'slate',
}, {
name: '文墨黑',
value: 'carbonight',
}, {
name: '日光白',
value: 'solarized_light',
}, {
name: '咖啡黑',
value: 'espresso_libre',
}, {
name: '薰衣草黑',
value: 'lavender',
}, {
name: '耀斑黑',
value: 'solarflare',
}, {
name: 'Clouds白',
value: 'clouds',
}, {
name: 'Clouds黑',
value: 'clouds_midnight',
}, {
name: 'GitHub白',
value: 'github',
}, {
name: '自定义',
value: 'custom',
}],
}),
computed: {
...mapGetters('theme', [
'currEditTheme',
'customEditThemeStyle',
]),
selectedTheme() {
return {
value: this.currEditTheme || 'default',
};
},
},
methods: {
...mapActions('data', [
'toggleSideBar',
]),
showFind() {
store.dispatch('findReplace/open', {
type: 'find',
findText: editorSvc.clEditor.selectionMgr.hasFocus() &&
editorSvc.clEditor.selectionMgr.getSelectedText(),
});
},
showFindReplace() {
store.dispatch('findReplace/open', {
type: 'replace',
findText: editorSvc.clEditor.selectionMgr.hasFocus() &&
editorSvc.clEditor.selectionMgr.getSelectedText(),
});
},
async changeTheme(item) {
await store.dispatch('theme/setEditTheme', item.value);
//
if (item.value === 'custom' && !this.customEditThemeStyle) {
this.toggleSideBar(true);
store.dispatch('data/setSideBarPanel', 'editTheme');
}
},
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
.editor-in-page-buttons {
position: absolute;
top: 0;
left: -108px;
height: 34px;
padding: 5px;
background-color: rgba(84, 96, 114, 0.4);
border-radius: $border-radius-base;
transition: 0.5s;
display: flex;
.dropdown-menu {
display: none;
.dropdown-menu-items {
right: unset;
left: 0;
}
}
&:active,
&:focus,
&:hover {
left: 0;
transition: 0.5s;
background-color: #546072;
.dropdown-menu {
display: block;
}
}
ul {
padding: 0;
margin-left: 10px;
line-height: 20px;
li {
width: 16px;
display: inline-block;
vertical-align: middle;
list-style: none;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}
}
.icon {
color: #dea731;
opacity: 0.7;
&:active,
&:focus,
&:hover {
opacity: 1;
}
}
.after {
margin-left: 0;
margin-right: -6px;
}
}
</style>

View File

@ -1,174 +1,102 @@
<template>
<div class="explorer flex flex--column">
<div class="side-title flex flex--row flex--space-between">
<div class="flex flex--row" v-if="!showSearch">
<button class="side-title__button side-title__button--new-file button" @click="newItem()" v-title="'创建文件'">
<div class="flex flex--row">
<button class="side-title__button button" @click="newItem()">
<icon-file-plus></icon-file-plus>
</button>
<button class="side-title__button side-title__button--new-folder button" @click="newItem(true)" v-title="'创建文件夹'">
<button class="side-title__button button" @click="newItem(true)">
<icon-folder-plus></icon-folder-plus>
</button>
<button class="side-title__button side-title__button--delete button" @click="deleteItem()" v-title="'删除'">
<icon-delete></icon-delete>
</button>
<button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'重命名'">
<button class="side-title__button button" @click="editItem()">
<icon-pen></icon-pen>
</button>
<button class="side-title__button side-title__button--search button" @click="toSearch()" v-title="'搜索文件'">
<icon-file-search></icon-file-search>
<button class="side-title__button button" @click="deleteItem()">
<icon-delete></icon-delete>
</button>
</div>
<div class="flex flex--row" v-else>
<button class="side-title__button button" @click="back()" v-title="'返回资源管理器'">
<icon-dots-horizontal></icon-dots-horizontal>
</button>
<div class="side-title__title">
搜索文件
</div>
</div>
<button class="side-title__button side-title__button--close button" @click="toggleExplorer(false)" v-title="'关闭资源管理器'">
<button class="side-title__button button" @click="toggleExplorer(false)">
<icon-close></icon-close>
</button>
</div>
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" v-show="!showSearch" tabindex="0" @keydown.delete="deleteItem()">
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0">
<explorer-node :node="rootNode" :depth="0"></explorer-node>
</div>
<div class="explorer__search" tabindex="0" v-if="!light && showSearch">
<input type="text" v-model="searchText" class="text-input" placeholder="请输入关键字回车" @keyup.enter="search" />
<div class="explorer__search-list">
<div class="search-tips" v-if="searching">正在查询中...</div>
<a class="menu-entry button flex flex--row flex--align-center" :class="{'search-node--selected': currentFileId === fileItem.id}"
v-for="fileItem in searchItems" :key="fileItem.id" @click="clickSearch(fileItem)" href="javascript:void(0)">
{{ fileItem.name }}
</a>
<div class="search-tips">最多返回匹配的50个文档</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc';
import store from '../store';
import MenuEntry from './menus/common/MenuEntry';
import localDbSvc from '../services/localDbSvc';
import badgeSvc from '../services/badgeSvc';
export default {
components: {
ExplorerNode,
MenuEntry,
},
data: () => ({
currentFileId: '',
showSearch: false,
searching: false,
searchText: '',
searchItems: [],
}),
computed: {
...mapState([
'light',
]),
...mapState('explorer', [
'newChildNode',
]),
...mapGetters('explorer', [
'rootNode',
'selectedNode',
]),
workspaceId: () => store.getters['workspace/currentWorkspace'].id,
},
methods: {
...mapActions('data', [
'toggleExplorer',
]),
newItem: isFolder => explorerSvc.newItem(isFolder),
deleteItem: () => explorerSvc.deleteItem(),
editItem() {
const node = this.selectedNode;
if (!node.isTrash && !node.isTemp) {
store.commit('explorer/setEditingId', node.item.id);
}
},
back() {
this.showSearch = false;
},
toSearch() {
this.showSearch = true;
},
search() {
this.searchItems = [];
if (!this.searchText) {
return;
}
this.searching = true;
const allFileById = {};
const filterIds = [];
localDbSvc.getWorkspaceItems(this.workspaceId, (item) => {
if (item.type !== 'file' && item.type !== 'content') {
return;
}
if (item.type === 'file') {
allFileById[item.id] = item;
}
if (filterIds.length >= 50) {
return;
}
const fileId = item.id.split('/')[0];
//
if (filterIds.indexOf(fileId) > -1) {
return;
}
if (item.name && item.name.indexOf(this.searchText) > -1) {
filterIds.push(fileId);
}
if (item.text && item.text.indexOf(this.searchText) > -1) {
filterIds.push(fileId);
}
}, () => {
filterIds.forEach((it) => {
const file = allFileById[it];
if (file) {
this.searchItems.push(file);
}
});
this.searching = false;
badgeSvc.addBadge('searchFile');
newItem(isFolder) {
const parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
this.$store.dispatch('explorer/openNode', parentId);
this.$store.commit('explorer/setNewItem', {
type: isFolder ? 'folder' : 'file',
parentId,
});
},
clickSearch(item) {
const node = store.getters['explorer/nodeMap'][item.id];
if (!node) {
return;
editItem() {
const selectedNode = this.$store.getters['explorer/selectedNode'];
this.$store.commit('explorer/setEditingId', selectedNode.item.id);
},
deleteItem() {
const selectedNode = this.$store.getters['explorer/selectedNode'];
if (!selectedNode.isNil) {
this.$store.dispatch(selectedNode.isFolder
? 'modal/folderDeletion'
: 'modal/fileDeletion',
selectedNode.item)
.then(() => {
if (selectedNode === this.$store.getters['explorer/selectedNode']) {
if (selectedNode.isFolder) {
const recursiveDelete = (folderNode) => {
folderNode.folders.forEach(recursiveDelete);
folderNode.files.forEach((fileNode) => {
this.$store.commit('file/deleteItem', fileNode.item.id);
});
this.$store.commit('folder/deleteItem', folderNode.item.id);
};
recursiveDelete(selectedNode);
} else {
this.$store.commit('file/deleteItem', selectedNode.item.id);
}
}
});
}
store.commit('explorer/setSelectedId', item.id);
// Prevent from freezing the UI while loading the file
setTimeout(() => {
store.commit('file/setCurrentId', item.id);
}, 10);
},
},
created() {
this.$watch(
() => store.getters['file/current'].id,
this.$store.watch(
() => this.$store.getters['file/current'].id,
(currentFileId) => {
this.currentFileId = currentFileId;
store.commit('explorer/setSelectedId', currentFileId);
store.dispatch('explorer/openNode', currentFileId);
this.$store.commit('explorer/setSelectedId', currentFileId);
this.$store.dispatch('explorer/openNode', currentFileId);
}, {
immediate: true,
},
);
});
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
.explorer,
.explorer__tree {
height: 100%;
@ -183,43 +111,4 @@ export default {
cursor: auto;
}
}
.explorer__search {
overflow: auto;
.explorer__search-list {
margin-top: 10px;
}
.menu-entry {
font-size: 14px;
padding: 5px;
}
.menu-entry__icon {
width: 0;
margin-left: 0;
border-bottom: 1px solid $hr-color;
}
.search-tips {
font-size: 10px;
background-color: rgba(255, 173, 51, 0.14902);
padding: 5px;
text-align: center;
}
.search-node--selected {
background-color: rgba(0, 0, 0, 0.2);
.app--dark & {
background-color: rgba(0, 0, 0, 0.4);
}
&:focus {
background-color: #39f;
color: #fff;
}
}
}
</style>

View File

@ -1,33 +1,28 @@
<template>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node)" @dragleave.stop="isDragTarget && setDragTarget()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
<div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingNodeName">
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
</div>
<div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTarget()">
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
{{node.item.name}}
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
</div>
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
<div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc.stop="submitNewChild(true)" v-model.trim="newChildName">
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
</div>
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
</div>
<button ref="copyId" v-clipboard="copyPath()" @click="info('路径已复制到剪切板!')" style="display: none;"></button>
</div>
</template>
<script>
import { mapMutations, mapActions } from 'vuex';
import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc';
import store from '../store';
import badgeSvc from '../services/badgeSvc';
import utils from '../services/utils';
export default {
name: 'explorer-node', // Required for recursivity
name: 'explorer-node',
props: ['node', 'depth'],
data: () => ({
editingValue: '',
@ -40,35 +35,35 @@ export default {
return `${(this.depth + 1) * 15}px`;
},
isSelected() {
return store.getters['explorer/selectedNode'] === this.node;
return this.$store.getters['explorer/selectedNode'] === this.node;
},
isEditing() {
return store.getters['explorer/editingNode'] === this.node;
return this.$store.getters['explorer/editingNode'] === this.node;
},
isDragTarget() {
return store.getters['explorer/dragTargetNode'] === this.node;
return this.$store.getters['explorer/dragTargetNode'] === this.node;
},
isDragTargetFolder() {
return store.getters['explorer/dragTargetNodeFolder'] === this.node;
return this.$store.getters['explorer/dragTargetNodeFolder'] === this.node;
},
isOpen() {
return store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
},
newChild() {
return store.getters['explorer/newChildNodeParent'] === this.node
&& store.state.explorer.newChildNode;
return this.$store.getters['explorer/newChildNodeParent'] === this.node
&& this.$store.state.explorer.newChildNode;
},
newChildName: {
get() {
return store.state.explorer.newChildNode.item.name;
return this.$store.state.explorer.newChildNode.item.name;
},
set(value) {
store.commit('explorer/setNewItemName', value);
this.$store.commit('explorer/setNewItemName', value && value.slice(0, 250));
},
},
editingNodeName: {
get() {
return store.getters['explorer/editingNode'].item.name;
return this.$store.getters['explorer/editingNode'].item.name;
},
set(value) {
this.editingValue = value.trim();
@ -77,132 +72,86 @@ export default {
},
methods: {
...mapMutations('explorer', [
'setEditingId',
'setDragTargetId',
]),
...mapActions('explorer', [
'setDragTarget',
]),
...mapActions('notification', [
'info',
]),
select(id = this.node.item.id, doOpen = true) {
const node = store.getters['explorer/nodeMap'][id];
if (!node) {
return false;
select(id) {
const node = this.$store.getters['explorer/nodeMap'][id];
if (node) {
this.$store.commit('explorer/setSelectedId', id);
if (node.isFolder) {
this.$store.commit('explorer/toggleOpenNode', id);
} else {
this.$store.commit('file/setCurrentId', id);
}
}
store.commit('explorer/setSelectedId', id);
if (doOpen) {
// Prevent from freezing the UI while loading the file
setTimeout(() => {
if (node.isFolder) {
store.commit('explorer/toggleOpenNode', id);
} else if (store.state.file.currentId !== id) {
store.commit('file/setCurrentId', id);
badgeSvc.addBadge('switchFile');
}
}, 10);
}
return true;
},
async submitNewChild(cancel) {
const { newChildNode } = store.state.explorer;
submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
try {
if (newChildNode.isFolder) {
const item = await workspaceSvc.storeItem(newChildNode.item);
this.select(item.id);
badgeSvc.addBadge('createFolder');
} else {
const item = await workspaceSvc.createFile(newChildNode.item);
this.select(item.id);
badgeSvc.addBadge('createFile');
}
} catch (e) {
// Cancel
}
}
store.commit('explorer/setNewItem', null);
},
async submitEdit(cancel) {
const { item, isFolder } = store.getters['explorer/editingNode'];
const value = this.editingValue;
this.setEditingId(null);
if (!cancel && item.id && value && item.name !== value) {
try {
await workspaceSvc.storeItem({
...item,
name: value,
const id = utils.uid();
if (newChildNode.isFolder) {
this.$store.commit('folder/setItem', {
...newChildNode.item,
id,
});
} else {
// Add empty line at the end if needed
const ensureFinalNewLine = text => `${text}\n`.replace(/\n\n$/, '\n');
const text = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileContent);
const properties = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileProperties);
this.$store.commit('content/setItem', {
id: `${id}/content`,
text,
properties,
});
this.$store.commit('file/setItem', {
...newChildNode.item,
id,
});
badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
} catch (e) {
// Cancel
}
this.select(id);
}
this.$store.commit('explorer/setNewItem', null);
},
submitEdit(cancel) {
const editingNode = this.$store.getters['explorer/editingNode'];
const id = editingNode.item.id;
const value = this.editingValue;
if (!cancel && id && value) {
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
id,
name: value.slice(0, 250),
});
}
this.$store.commit('explorer/setEditingId', null);
},
setDragSourceId(evt) {
if (this.node.noDrag) {
const id = this.node.item.id;
if (id === 'fake') {
evt.preventDefault();
return;
}
store.commit('explorer/setDragSourceId', this.node.item.id);
// Fix for Firefox
// See https://stackoverflow.com/a/3977637/1333165
evt.dataTransfer.setData('Text', '');
},
copyPath() {
let path = utils.getAbsoluteDir(this.node).replaceAll(' ', '%20');
path = path.indexOf('/') === 0 ? path : `/${path}`;
return this.node.isFolder ? path : `${path}.md`;
this.$store.commit('explorer/setDragSourceId', id);
},
onDrop() {
const sourceNode = store.getters['explorer/dragSourceNode'];
const targetNode = store.getters['explorer/dragTargetNodeFolder'];
this.setDragTarget();
const sourceNode = this.$store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
this.setDragTargetId();
if (!sourceNode.isNil
&& !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id
) {
workspaceSvc.storeItem({
...sourceNode.item,
const patch = {
id: sourceNode.item.id,
parentId: targetNode.item.id,
});
badgeSvc.addBadge(sourceNode.isFolder ? 'moveFolder' : 'moveFile');
}
},
async onContextMenu(evt) {
if (this.select(undefined, false)) {
evt.preventDefault();
evt.stopPropagation();
const item = await store.dispatch('contextMenu/open', {
coordinates: {
left: evt.clientX,
top: evt.clientY,
},
items: [{
name: '新建文件',
disabled: !this.node.isFolder || this.node.isTrash,
perform: () => explorerSvc.newItem(false),
}, {
name: '新建文件夹',
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => explorerSvc.newItem(true),
}, {
type: 'separator',
}, {
name: '重命名',
disabled: this.node.isTrash || this.node.isTemp,
perform: () => this.setEditingId(this.node.item.id),
}, {
name: '删除',
perform: () => explorerSvc.deleteItem(),
}, {
name: '复制路径',
disabled: this.node.isTrash || this.node.isTemp,
perform: () => this.$refs.copyId.click(),
}],
});
if (item) {
item.perform();
};
if (sourceNode.isFolder) {
this.$store.commit('folder/patchItem', patch);
} else {
this.$store.commit('file/patchItem', patch);
}
}
},
@ -218,7 +167,6 @@ $item-font-size: 14px;
}
.explorer-node__item {
position: relative;
cursor: pointer;
font-size: $item-font-size;
overflow: hidden;
@ -229,10 +177,6 @@ $item-font-size: 14px;
.explorer-node--selected > & {
background-color: rgba(0, 0, 0, 0.2);
.app--dark & {
background-color: rgba(0, 0, 0, 0.4);
}
.explorer__tree:focus & {
background-color: #39f;
color: #fff;
@ -251,29 +195,17 @@ $item-font-size: 14px;
}
}
.explorer-node--trash,
.explorer-node--temp {
color: rgba(0, 0, 0, 0.5);
.app--dark & {
color: rgba(255, 255, 255, 0.5);
}
}
.explorer-node--folder > .explorer-node__item,
.explorer-node--folder > .explorer-node__item-editor,
.explorer-node__item--folder,
.explorer-node__item-editor--folder,
.explorer-node__new-child--folder {
&::before {
content: '';
content: '▸';
position: absolute;
margin-left: -13px;
}
}
.explorer-node--folder.explorer-node--open > .explorer-node__item,
.explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
&::before {
content: '▾';
.explorer-node--open > & {
content: '▾';
}
}
}

View File

@ -1,395 +0,0 @@
<template>
<div class="find-replace" @keydown.esc.stop="onEscape">
<button class="find-replace__close-button button not-tabbable" @click="close()" v-title="'关闭'">
<icon-close></icon-close>
</button>
<div class="find-replace__row">
<input type="text" class="find-replace__text-input find-replace__text-input--find text-input" @keydown.enter="find('forward')" v-model="findText">
<div class="find-replace__find-stats">
{{findPosition}} of {{findCount}}
</div>
<div class="flex flex--row flex--space-between">
<div class="flex flex--row">
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findCaseSensitive}" @click="findCaseSensitive = !findCaseSensitive" title="Case sensitive">Aa</button>
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findUseRegexp}" @click="findUseRegexp = !findUseRegexp" title="Regular expression">.<sup></sup></button>
</div>
<div class="flex flex--row">
<button class="find-replace__button button" @click="find('backward')">上一个</button>
<button class="find-replace__button button" @click="find('forward')">下一个</button>
</div>
</div>
</div>
<div v-if="type === 'replace'">
<div class="find-replace__row">
<input type="text" class="find-replace__text-input find-replace__text-input--replace text-input" @keydown.enter="replace" v-model="replaceText">
</div>
<div class="find-replace__row flex flex--row flex--end">
<button class="find-replace__button button" @click="replace">替换</button>
<button class="find-replace__button button" @click="replaceAll">全部替换</button>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import editorSvc from '../services/editorSvc';
import cledit from '../services/editor/cledit';
import store from '../store';
import EditorClassApplier from './common/EditorClassApplier';
const accessor = (fieldName, setterName) => ({
get() {
return store.state.findReplace[fieldName];
},
set(value) {
store.commit(`findReplace/${setterName}`, value);
},
});
const computedLayoutSetting = key => ({
get() {
return store.getters['data/layoutSettings'][key];
},
set(value) {
store.dispatch('data/patchLayoutSettings', {
[key]: value,
});
},
});
class DynamicClassApplier {
constructor(cssClass, offset, silent) {
this.startMarker = new cledit.Marker(offset.start);
this.endMarker = new cledit.Marker(offset.end);
editorSvc.clEditor.addMarker(this.startMarker);
editorSvc.clEditor.addMarker(this.endMarker);
if (!silent) {
this.classApplier = new EditorClassApplier(
[`find-replace-${this.startMarker.id}`, cssClass],
() => ({
start: this.startMarker.offset,
end: this.endMarker.offset,
}),
);
}
}
clean = () => {
editorSvc.clEditor.removeMarker(this.startMarker);
editorSvc.clEditor.removeMarker(this.endMarker);
if (this.classApplier) {
this.classApplier.stop();
}
}
}
export default {
data: () => ({
findCount: 0,
findPosition: 0,
}),
computed: {
...mapState('findReplace', [
'type',
'lastOpen',
]),
findText: accessor('findText', 'setFindText'),
replaceText: accessor('replaceText', 'setReplaceText'),
findCaseSensitive: computedLayoutSetting('findCaseSensitive'),
findUseRegexp: computedLayoutSetting('findUseRegexp'),
},
methods: {
highlightOccurrences() {
const oldClassAppliers = {};
Object.entries(this.classAppliers).forEach(([, classApplier]) => {
const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
oldClassAppliers[newKey] = classApplier;
});
const offsetList = [];
this.classAppliers = {};
if (this.state !== 'destroyed' && this.findText) {
try {
this.searchRegex = this.findText;
if (!this.findUseRegexp) {
this.searchRegex = this.searchRegex.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
}
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
editorSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
const match = params[0];
const offset = params[params.length - 2];
offsetList.push({
start: offset,
end: offset + match.length,
});
});
offsetList.forEach((offset, i) => {
const key = `${offset.start}:${offset.end}`;
this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
'find-replace-highlighting',
offset,
i > 200,
);
});
} catch (e) {
// Ignore
}
if (this.state !== 'created') {
this.find('selection');
this.state = 'created';
}
}
Object.entries(oldClassAppliers).forEach(([key, classApplier]) => {
if (!this.classAppliers[key]) {
classApplier.clean();
if (classApplier === this.selectedClassApplier) {
this.selectedClassApplier.child.clean();
this.selectedClassApplier = null;
}
}
});
this.findCount = offsetList.length;
},
unselectClassApplier() {
if (this.selectedClassApplier) {
this.selectedClassApplier.child.clean();
this.selectedClassApplier.child = null;
this.selectedClassApplier = null;
}
this.findPosition = 0;
},
find(mode = 'forward') {
const { selectedClassApplier } = this;
this.unselectClassApplier();
const { selectionMgr } = editorSvc.clEditor;
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const keys = Object.keys(this.classAppliers);
const finder = checker => (key) => {
if (checker(this.classAppliers[key]) && selectedClassApplier !== this.classAppliers[key]) {
this.selectedClassApplier = this.classAppliers[key];
return true;
}
return false;
};
if (mode === 'backward') {
this.selectedClassApplier = this.classAppliers[keys[keys.length - 1]];
keys.reverse().some(finder(classApplier => classApplier.startMarker.offset <= startOffset));
} else if (mode === 'selection') {
keys.some(finder(classApplier => classApplier.startMarker.offset === startOffset &&
classApplier.endMarker.offset === endOffset));
} else if (mode === 'forward') {
this.selectedClassApplier = this.classAppliers[keys[0]];
keys.some(finder(classApplier => classApplier.endMarker.offset >= endOffset));
}
if (this.selectedClassApplier) {
selectionMgr.setSelectionStartEnd(
this.selectedClassApplier.startMarker.offset,
this.selectedClassApplier.endMarker.offset,
);
this.selectedClassApplier.child = new DynamicClassApplier('find-replace-selection', {
start: this.selectedClassApplier.startMarker.offset,
end: this.selectedClassApplier.endMarker.offset,
});
selectionMgr.updateCursorCoordinates(this.$el.parentNode.clientHeight);
// Deduce the findPosition
Object.keys(this.classAppliers).forEach((key, i) => {
if (this.selectedClassApplier !== this.classAppliers[key]) {
return false;
}
this.findPosition = i + 1;
return true;
});
}
},
replace() {
if (this.searchRegex) {
if (!this.selectedClassApplier) {
this.find();
return;
}
editorSvc.clEditor.replaceAll(
this.replaceRegex,
this.replaceText,
this.selectedClassApplier.startMarker.offset,
);
this.$nextTick(() => this.find());
}
},
replaceAll() {
if (this.searchRegex) {
editorSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
}
},
close() {
store.commit('findReplace/setType');
},
onEscape() {
editorSvc.clEditor.focus();
},
},
mounted() {
this.classAppliers = {};
// Highlight occurences
this.debouncedHighlightOccurrences = cledit.Utils.debounce(
() => this.highlightOccurrences(),
25,
);
// Refresh highlighting when find text changes or changing options
this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
// Refresh highlighting when content changes
editorSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);
// Last open changes trigger focus on text input and find occurence in selection
this.$watch(() => this.lastOpen, () => {
const elt = this.$el.querySelector(`.find-replace__text-input--${this.type}`);
elt.focus();
elt.setSelectionRange(0, this[`${this.type}Text`].length);
// Highlight and find in selection
this.state = null;
this.debouncedHighlightOccurrences();
}, {
immediate: true,
});
// Close on escape
this.onKeyup = (evt) => {
if (evt.which === 27) {
// Esc key
store.commit('findReplace/setType');
}
};
window.addEventListener('keyup', this.onKeyup);
// Unselect class applier when focus is out of the panel
this.onFocusIn = () => this.$el.contains(document.activeElement) ||
setTimeout(() => this.unselectClassApplier(), 15);
window.addEventListener('focusin', this.onFocusIn);
},
destroyed() {
// Unregister listeners
editorSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
window.removeEventListener('keyup', this.onKeyup);
window.removeEventListener('focusin', this.onFocusIn);
this.state = 'destroyed';
this.debouncedHighlightOccurrences();
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
.find-replace {
padding: 0 35px 0 25px;
}
.find-replace__row {
margin: 10px 0;
}
.find-replace__button {
font-size: 15px;
padding: 0 8px;
line-height: 28px;
height: 28px;
}
.find-replace__button--find-option {
padding: 0;
width: 28px;
font-weight: 600;
letter-spacing: -0.025em;
color: rgba(0, 0, 0, 0.25);
text-transform: none;
.app--dark & {
color: rgba(255, 255, 255, 0.25);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.25);
.app--dark & {
color: rgba(255, 255, 255, 0.25);
}
}
}
.find-replace__button--on {
color: rgba(0, 0, 0, 0.67);
.app--dark & {
color: rgba(255, 255, 255, 0.67);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.67);
.app--dark & {
color: rgba(255, 255, 255, 0.67);
}
}
}
.find-replace__text-input {
border: 1px solid transparent;
padding: 2px 5px;
height: 32px;
&:focus {
border-color: $link-color;
}
}
.find-replace__close-button {
position: absolute;
top: 5px;
right: 5px;
width: 25px;
height: 25px;
padding: 2px;
color: rgba(0, 0, 0, 0.5);
.app--dark & {
color: rgba(255, 255, 255, 0.5);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.75);
.app--dark & {
color: rgba(255, 255, 255, 0.75);
}
}
}
.find-replace__find-stats {
text-align: right;
font-size: 0.75em;
opacity: 0.6;
}
.find-replace-highlighting {
background-color: $highlighting-color;
color: $editor-color-light !important;
.app--dark & {
background-color: $dark-highlighting-color;
}
}
.find-replace-selection {
background-color: $selection-highlighting-color;
}
</style>

View File

@ -1,57 +1,37 @@
<template>
<div class="layout" :class="{'layout--revision': revisionContent}">
<div class="layout">
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{width: styles.layoutOverflow ? '100%' : constants.explorerWidth + 'px'}">
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
<explorer></explorer>
</div>
<div class="layout__panel flex flex--column" tour-step-anchor="welcome,end" :style="{width: styles.innerWidth + 'px'}">
<div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{height: constants.navigationBarHeight + 'px'}">
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
<div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{ height: constants.navigationBarHeight + 'px' }">
<navigation-bar></navigation-bar>
</div>
<div class="layout__panel flex flex--row" :style="{height: styles.innerHeight + 'px'}">
<div class="layout__panel layout__panel--editor" :class="editTheme" v-show="styles.showEditor" :style="{width: (styles.editorWidth + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
<div class="gutter__background" v-if="styles.editorGutterWidth" :style="{width: styles.editorGutterWidth + 'px'}"></div>
</div>
<div class="layout__panel flex flex--row" :style="{ height: styles.innerHeight + 'px' }">
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
<editor></editor>
<editor-in-page-buttons v-if="editorShowInPageButtons"></editor-in-page-buttons>
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
<sticky-comment v-if="styles.editorGutterWidth && stickyComment === 'top'"></sticky-comment>
<current-discussion v-if="styles.editorGutterWidth"></current-discussion>
</div>
</div>
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{width: constants.buttonBarWidth + 'px'}">
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
<button-bar></button-bar>
</div>
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{width: (styles.previewWidth + styles.previewGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
<div class="gutter__background" v-if="styles.previewGutterWidth" :style="{width: styles.previewGutterWidth + 'px'}"></div>
</div>
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{ width: styles.previewWidth + 'px', 'font-size': styles.fontSize + 'px' }">
<preview></preview>
<preview-in-page-buttons></preview-in-page-buttons>
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
<sticky-comment v-if="styles.previewGutterWidth && stickyComment === 'top'"></sticky-comment>
<current-discussion v-if="styles.previewGutterWidth"></current-discussion>
</div>
</div>
<div class="layout__panel layout__panel--find-replace" v-if="showFindReplace">
<find-replace></find-replace>
</div>
</div>
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{height: constants.statusBarHeight + 'px'}">
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{ height: constants.statusBarHeight + 'px' }">
<status-bar></status-bar>
</div>
</div>
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{width: styles.layoutOverflow ? '100%' : constants.sideBarWidth + 'px'}">
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{ width: constants.sideBarWidth + 'px' }">
<side-bar></side-bar>
</div>
</div>
<tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapGetters, mapMutations } from 'vuex';
import NavigationBar from './NavigationBar';
import ButtonBar from './ButtonBar';
import StatusBar from './StatusBar';
@ -59,15 +39,7 @@ import Explorer from './Explorer';
import SideBar from './SideBar';
import Editor from './Editor';
import Preview from './Preview';
import Tour from './Tour';
import EditorInPageButtons from './EditorInPageButtons';
import PreviewInPageButtons from './PreviewInPageButtons';
import StickyComment from './gutters/StickyComment';
import CurrentDiscussion from './gutters/CurrentDiscussion';
import FindReplace from './FindReplace';
import editorSvc from '../services/editorSvc';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';
export default {
components: {
@ -78,56 +50,24 @@ export default {
SideBar,
Editor,
Preview,
Tour,
EditorInPageButtons,
PreviewInPageButtons,
StickyComment,
CurrentDiscussion,
FindReplace,
},
computed: {
...mapState([
'light',
]),
...mapState('content', [
'revisionContent',
]),
...mapState('discussion', [
'stickyComment',
]),
...mapGetters('layout', [
'constants',
'styles',
]),
...mapGetters('data', [
'layoutSettings',
]),
...mapGetters('theme', [
'currEditTheme',
]),
editTheme() {
return `edit-theme--${this.currEditTheme || 'default'}`;
},
showFindReplace() {
return !!store.state.findReplace.type;
},
editorShowInPageButtons() {
return store.getters['data/computedSettings'].editor.showInPageButtons;
},
},
methods: {
...mapActions('layout', [
...mapMutations('layout', [
'updateBodySize',
]),
saveSelection: () => editorSvc.saveSelection(true),
},
created() {
markdownConversionSvc.init(); // Needs to be inited before mount
this.updateBodySize();
window.addEventListener('resize', this.updateBodySize);
window.addEventListener('keyup', this.saveSelection);
window.addEventListener('mouseup', this.saveSelection);
window.addEventListener('focusin', this.saveSelection);
window.addEventListener('contextmenu', this.saveSelection);
},
mounted() {
@ -135,28 +75,18 @@ export default {
const previewElt = this.$el.querySelector('.preview__inner-2');
const tocElt = this.$el.querySelector('.toc__inner');
editorSvc.init(editorElt, previewElt, tocElt);
// Focus on the editor every time reader mode is disabled
const focus = () => {
if (this.styles.showEditor) {
editorSvc.clEditor.focus();
}
};
setTimeout(focus, 100);
this.$watch(() => this.styles.showEditor, focus);
},
destroyed() {
window.removeEventListener('resize', this.updateStyle);
window.removeEventListener('keyup', this.saveSelection);
window.removeEventListener('mouseup', this.saveSelection);
window.removeEventListener('focusin', this.saveSelection);
window.removeEventListener('contextmenu', this.saveSelection);
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.layout {
position: absolute;
@ -169,11 +99,10 @@ export default {
width: 100%;
height: 100%;
flex: none;
overflow: hidden;
}
.layout__panel--navigation-bar {
background-color: $navbar-bg;
background-color: #2c2c2c;
}
.layout__panel--status-bar {
@ -181,65 +110,16 @@ export default {
}
.layout__panel--editor {
background-color: $editor-background-light;
.app--dark & {
background-color: $editor-background-dark;
}
.gutter__background,
.comment-list__current-discussion,
.sticky-comment,
.current-discussion {
background-color: mix(#000, $editor-background-light, 6.7%);
.app--dark & {
background-color: mix(#fff, $editor-background-dark, 6.7%);
}
}
}
$preview-background-light: #f3f3f3;
$preview-background-dark: #444;
.layout__panel--preview,
.layout__panel--button-bar {
background-color: $preview-background-light;
.app--dark & {
background-color: $preview-background-dark;
}
background-color: #fff;
}
.layout__panel--button-bar,
.layout__panel--preview {
.gutter__background,
.comment-list__current-discussion,
.sticky-comment,
.current-discussion {
background-color: mix(#000, $preview-background-light, 6.7%);
}
background-color: #f3f3f3;
}
.layout__panel--explorer,
.layout__panel--side-bar {
background-color: #ddd;
.app--dark & {
background-color: #383c4a;
}
}
.layout__panel--find-replace {
background-color: #e6e6e6;
position: absolute;
left: 0;
bottom: 0;
width: 300px;
height: auto;
border-top-right-radius: $border-radius-base;
.app--dark & {
background-color: #4d5160;
}
background-color: #dadada;
}
</style>

View File

@ -1,294 +1,172 @@
<template>
<div class="modal" v-if="config" @keydown.esc.stop="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<!-- <div class="modal__sponsor-banner" v-if="!isSponsor">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/mafgwo/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div> -->
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
<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 class="modal" @keyup.esc="onEscape">
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
<about-modal v-else-if="config.type === 'about'"></about-modal>
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
<link-modal v-else-if="config.type === 'link'"></link-modal>
<image-modal v-else-if="config.type === 'image'"></image-modal>
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
<google-drive-sync-modal v-else-if="config.type === 'googleDriveSync'"></google-drive-sync-modal>
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
<dropbox-sync-modal v-else-if="config.type === 'dropboxSync'"></dropbox-sync-modal>
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
<github-sync-modal v-else-if="config.type === 'githubSync'"></github-sync-modal>
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
<wordpress-publish-modal v-else-if="config.type === 'wordpressPublish'"></wordpress-publish-modal>
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
<div v-else class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__content" v-html="config.content"></div>
<div class="modal__button-bar">
<button v-if="config.rejectText" class="button" @click="config.reject()">{{config.rejectText}}</button>
<button v-if="config.resolveText" class="button" @click="config.resolve()">{{config.resolveText}}</button>
</div>
</div>
</modal-inner>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import giteeHelper from '../services/providers/helpers/giteeHelper';
import store from '../store';
import ModalInner from './modals/common/ModalInner';
import editorEngineSvc from '../services/editorEngineSvc';
import FilePropertiesModal from './modals/FilePropertiesModal';
import SettingsModal from './modals/SettingsModal';
import TemplatesModal from './modals/TemplatesModal';
import AboutModal from './modals/AboutModal';
import HtmlExportModal from './modals/HtmlExportModal';
import PdfExportModal from './modals/PdfExportModal';
import PandocExportModal from './modals/PandocExportModal';
import LinkModal from './modals/LinkModal';
import ImageModal from './modals/ImageModal';
import GooglePhotoModal from './modals/GooglePhotoModal';
import SyncManagementModal from './modals/SyncManagementModal';
import PublishManagementModal from './modals/PublishManagementModal';
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
import AccountManagementModal from './modals/AccountManagementModal';
import BadgeManagementModal from './modals/BadgeManagementModal';
import SponsorModal from './modals/SponsorModal';
import CommitMessageModal from './modals/CommitMessageModal';
import WorkspaceImgPathModal from './modals/WorkspaceImgPathModal';
import ChatGptModal from './modals/ChatGptModal';
// Providers
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
import GoogleDriveAccountModal from './modals/providers/GoogleDriveAccountModal';
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
import DropboxPublishModal from './modals/providers/DropboxPublishModal';
import GithubAccountModal from './modals/providers/GithubAccountModal';
import GithubOpenModal from './modals/providers/GithubOpenModal';
import GithubSaveModal from './modals/providers/GithubSaveModal';
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GithubImgStorageModal from './modals/providers/GithubImgStorageModal';
import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal';
import GiteeAccountModal from './modals/providers/GiteeAccountModal';
import GiteeOpenModal from './modals/providers/GiteeOpenModal';
import GiteeSaveModal from './modals/providers/GiteeSaveModal';
import GiteeWorkspaceModal from './modals/providers/GiteeWorkspaceModal';
import GiteePublishModal from './modals/providers/GiteePublishModal';
import GiteeGistSyncModal from './modals/providers/GiteeGistSyncModal';
import GiteeGistPublishModal from './modals/providers/GiteeGistPublishModal';
import GitlabAccountModal from './modals/providers/GitlabAccountModal';
import GitlabOpenModal from './modals/providers/GitlabOpenModal';
import GitlabPublishModal from './modals/providers/GitlabPublishModal';
import GitlabSaveModal from './modals/providers/GitlabSaveModal';
import GitlabWorkspaceModal from './modals/providers/GitlabWorkspaceModal';
import GiteaAccountModal from './modals/providers/GiteaAccountModal';
import GiteaOpenModal from './modals/providers/GiteaOpenModal';
import GiteaPublishModal from './modals/providers/GiteaPublishModal';
import GiteaSaveModal from './modals/providers/GiteaSaveModal';
import GiteaWorkspaceModal from './modals/providers/GiteaWorkspaceModal';
import GiteaImgStorageModal from './modals/providers/GiteaImgStorageModal';
import WordpressPublishModal from './modals/providers/WordpressPublishModal';
import BloggerPublishModal from './modals/providers/BloggerPublishModal';
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';
import SmmsAccountModal from './modals/providers/SmmsAccountModal';
import CustomAccountModal from './modals/providers/CustomAccountModal';
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield, input[type=checkbox]')
// Filter enabled and visible element
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
import DropboxAccountModal from './modals/DropboxAccountModal';
import DropboxSyncModal from './modals/DropboxSyncModal';
import DropboxPublishModal from './modals/DropboxPublishModal';
import GithubAccountModal from './modals/GithubAccountModal';
import GithubSyncModal from './modals/GithubSyncModal';
import GithubPublishModal from './modals/GithubPublishModal';
import GistSyncModal from './modals/GistSyncModal';
import GistPublishModal from './modals/GistPublishModal';
import WordpressPublishModal from './modals/WordpressPublishModal';
import BloggerPublishModal from './modals/BloggerPublishModal';
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
import ZendeskAccountModal from './modals/ZendeskAccountModal';
import ZendeskPublishModal from './modals/ZendeskPublishModal';
export default {
components: {
ModalInner,
FilePropertiesModal,
SettingsModal,
TemplatesModal,
AboutModal,
HtmlExportModal,
PdfExportModal,
PandocExportModal,
LinkModal,
ImageModal,
GooglePhotoModal,
SyncManagementModal,
PublishManagementModal,
WorkspaceManagementModal,
AccountManagementModal,
BadgeManagementModal,
SponsorModal,
CommitMessageModal,
WorkspaceImgPathModal,
ChatGptModal,
// Providers
GooglePhotoModal,
GoogleDriveAccountModal,
GoogleDriveSaveModal,
GoogleDriveWorkspaceModal,
GoogleDriveSyncModal,
GoogleDrivePublishModal,
DropboxAccountModal,
DropboxSaveModal,
DropboxSyncModal,
DropboxPublishModal,
GithubAccountModal,
GithubOpenModal,
GithubSaveModal,
GithubWorkspaceModal,
GithubSyncModal,
GithubPublishModal,
GithubImgStorageModal,
GistSyncModal,
GistPublishModal,
GiteeAccountModal,
GiteeOpenModal,
GiteeSaveModal,
GiteeWorkspaceModal,
GiteePublishModal,
GiteeGistSyncModal,
GiteeGistPublishModal,
GitlabAccountModal,
GitlabOpenModal,
GitlabPublishModal,
GitlabSaveModal,
GitlabWorkspaceModal,
GiteaAccountModal,
GiteaOpenModal,
GiteaPublishModal,
GiteaSaveModal,
GiteaWorkspaceModal,
GiteaImgStorageModal,
WordpressPublishModal,
BloggerPublishModal,
BloggerPagePublishModal,
ZendeskAccountModal,
ZendeskPublishModal,
CouchdbWorkspaceModal,
CouchdbCredentialsModal,
SmmsAccountModal,
CustomAccountModal,
},
computed: {
...mapGetters([
'isSponsor',
]),
...mapGetters('modal', [
'config',
]),
currentModalComponent() {
if (this.config.type) {
let componentName = this.config.type[0].toUpperCase();
componentName += this.config.type.slice(1);
componentName += 'Modal';
if (this.$options.components[componentName]) {
return componentName;
}
}
return null;
},
simpleModal() {
return simpleModals[this.config.type] || {};
},
},
computed: mapGetters('modal', [
'config',
]),
methods: {
async sponsor() {
try {
if (!store.getters['workspace/sponsorToken']) {
// User has to sign in
await store.dispatch('modal/open', 'signInForSponsorship');
await giteeHelper.signin();
await syncSvc.afterSignIn();
syncSvc.requestSync();
}
if (!store.getters.isSponsor) {
await store.dispatch('modal/open', 'sponsor');
}
} catch (e) { /* cancel */ }
},
onEscape() {
this.config.reject();
editorSvc.clEditor.focus();
},
onTab(evt) {
const tabbables = getTabbables(this.$el);
const firstTabbable = tabbables[0];
const lastTabbable = tabbables[tabbables.length - 1];
if (evt.shiftKey && firstTabbable === evt.target) {
evt.preventDefault();
lastTabbable.focus();
} else if (!evt.shiftKey && lastTabbable === evt.target) {
evt.preventDefault();
firstTabbable.focus();
}
editorEngineSvc.clEditor.focus();
},
onFocusInOut(evt) {
const { parentNode } = evt.target;
if (parentNode && parentNode.parentNode) {
const isFocusIn = evt.type === 'focusin';
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
// Focus effect
if (parentNode.classList.contains('form-entry__field')
&& parentNode.parentNode.classList.contains('form-entry')) {
parentNode.parentNode.classList.toggle(
'form-entry--focused',
evt.type === 'focusin',
);
if (evt.target.parentNode.classList.contains('form-entry__field') &&
evt.target.parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
}
}
if (isFocusIn && this.config) {
const modalInner = this.$el.querySelector('.modal__inner-2');
let target = evt.target;
while (target) {
if (target === modalInner) {
return;
}
target = target.parentNode;
}
this.config.reject();
}
},
},
mounted() {
this.$watch(
() => this.config,
(isOpen) => {
if (isOpen) {
const tabbables = getTabbables(this.$el);
if (tabbables[0]) {
tabbables[0].focus();
}
}
},
{ immediate: true },
);
window.addEventListener('focusin', this.onFocusInOut);
window.addEventListener('focusout', this.onFocusInOut);
const eltToFocus = this.$el.querySelector('input.text-input')
|| this.$el.querySelector('.textfield')
|| this.$el.querySelector('.button');
if (eltToFocus) {
eltToFocus.focus();
}
},
destroyed() {
window.removeEventListener('focusin', this.onFocusInOut);
window.removeEventListener('focusout', this.onFocusInOut);
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.modal {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(160, 160, 160, 0.5);
background-color: rgba(180, 180, 180, 0.75);
overflow: auto;
p {
line-height: 1.5;
}
}
.modal__sponsor-banner {
position: fixed;
z-index: 1;
width: 100%;
color: darken($error-color, 10%);
background-color: transparentize(lighten($error-color, 33%), 0.075);
font-size: 0.9em;
line-height: 1.33;
text-align: center;
padding: 0.25em 1em;
}
.modal__inner-1 {
margin: 0 auto;
width: 100%;
min-width: 320px;
max-width: 480px;
max-width: 500px;
}
.modal__inner-2 {
margin: 40px 10px 100px;
background-color: #f8f8f8;
padding: 50px 50px 40px;
margin: 50px 10px 100px;
background-color: #fff;
padding: 40px 50px 30px;
border-radius: $border-radius-base;
position: relative;
overflow: hidden;
.app--dark & {
background-color: #383c4a;
}
&::before {
content: '';
position: absolute;
@ -296,7 +174,7 @@ export default {
left: 0;
height: $border-radius-base;
width: 100%;
background-image: linear-gradient(to left, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
background-image: linear-gradient(to left, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
}
&::after {
@ -306,20 +184,19 @@ export default {
left: 0;
height: $border-radius-base;
width: 100%;
background-image: linear-gradient(to right, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
background-image: linear-gradient(to right, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
}
}
.modal__content > :first-child,
.modal__content > .modal__image:first-child + * {
.modal__content :first-child {
margin-top: 0;
}
.modal__image {
float: left;
width: 60px;
height: 60px;
margin: 1.5em 1.2em 0.5em 0;
width: 64px;
height: 64px;
margin: 1.5em 1.5em 0.5em 0;
& + *::after {
content: '';
@ -328,46 +205,24 @@ export default {
}
}
.modal__title {
font-weight: bold;
font-size: 1.5rem;
line-height: 1.4;
margin-top: 2.5rem;
}
.modal__sub-title {
opacity: 0.6;
font-size: 0.75rem;
margin-bottom: 1.5rem;
}
.modal__error {
color: #de2c00;
}
.modal__info {
background-color: $info-bg;
.modal__tip {
background-color: transparentize(#ffd600, 0.85);
border-radius: $border-radius-base;
margin: 1.2em 0;
padding: 0.75em 1.25em;
font-size: 0.95em;
line-height: 1.6;
pre {
line-height: 1.5;
}
}
.modal__info--multiline {
padding-top: 0.1em;
padding-bottom: 0.1em;
}
.modal__button-bar {
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 1.75rem;
text-align: right;
.button {
margin-left: 5px;
}
}
.form-entry {
@ -377,35 +232,21 @@ export default {
.form-entry__label {
display: block;
font-size: 0.9rem;
color: #808080;
color: #a0a0a0;
.form-entry--focused & {
color: darken($link-color, 10%);
}
.form-entry--error & {
color: darken($error-color, 10%);
}
}
.form-entry__label-info {
font-size: 0.75rem;
}
.form-entry__field {
border: 1px solid #b0b0b0;
border: 1px solid #d8d8d8;
border-radius: $border-radius-base;
position: relative;
overflow: hidden;
.form-entry--focused & {
border-color: $link-color;
box-shadow: 0 0 0 2.5px transparentize($link-color, 0.67);
}
.form-entry--error & {
border-color: $error-color;
box-shadow: 0 0 0 2.5px transparentize($error-color, 0.67);
}
}
@ -441,49 +282,8 @@ export default {
.form-entry__info {
font-size: 0.75em;
opacity: 0.67;
opacity: 0.5;
line-height: 1.4;
margin: 0.25em 0;
}
.tabs {
border-bottom: 1px solid $hr-color;
margin: 1em 0 2em;
&::after {
content: '';
display: block;
clear: both;
}
}
.tabs__tab {
width: 50%;
float: left;
text-align: center;
line-height: 1.4;
font-weight: 400;
font-size: 1.1em;
}
.tabs__tab > a {
width: 100%;
text-decoration: none;
padding: 0.67em 0.33em;
cursor: pointer;
border-bottom: 2px solid transparent;
border-top-left-radius: $border-radius-base;
border-top-right-radius: $border-radius-base;
color: $link-color;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.05);
}
}
.tabs__tab--active > a {
border-bottom: 2px solid $link-color;
color: inherit;
}
</style>

View File

@ -1,84 +1,85 @@
<template>
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent, 'navigation-bar--light': light}">
<!-- Explorer -->
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button navigation-bar__button--close button" v-if="light" @click="close()" v-title="'关闭StackEdit'"><icon-check-circle></icon-check-circle></button>
<button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'切换资源管理器'"><icon-folder></icon-folder></button>
<button class="navigation-bar__button button" @click="toggleExplorer()">
<icon-folder></icon-folder>
</button>
</div>
<!-- Side bar -->
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
<button class="navigation-bar__button navigation-bar__button--theme button" v-title="'切换主题'" tour-step-anchor="theme" @click="switchTheme"><icon-switch-theme></icon-switch-theme></button>
<a class="navigation-bar__button navigation-bar__button--stackedit button" v-if="light" href="app" target="_blank" v-title="'打开StackEdit'"><icon-provider provider-id="stackedit"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--stackedit button" v-else tour-step-anchor="menu" @click="toggleSideBar()" v-title="'切换侧边栏'"><icon-provider provider-id="stackedit"></icon-provider></button>
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
<icon-provider provider-id="stackedit"></icon-provider>
</button>
</div>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
<!-- Spinner -->
<div class="navigation-bar__spinner">
<div v-if="!offline && showSpinner" class="spinner"></div>
<icon-sync-off v-if="offline"></icon-sync-off>
</div>
<!-- Title -->
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle(false)" @keydown.esc.stop="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<!-- Sync/Publish -->
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'同步位置'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'立即同步'"><icon-sync></icon-sync></button>
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank" v-title="'发布位置'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish" v-title="'立即发布'"><icon-upload></icon-upload></button>
</div>
<!-- Revision -->
<div class="flex flex--row" v-if="revisionContent">
<button class="navigation-bar__button navigation-bar__button--revision navigation-bar__button--restore button" @click="restoreRevision">恢复</button>
<button class="navigation-bar__button navigation-bar__button--revision button" @click="setRevisionContent()" v-title="'关闭修订'"><icon-close></icon-close></button>
</div>
</div>
<div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons">
<button class="navigation-bar__button button" @click="undo" v-title="'回退'" :disabled="!canUndo"><icon-undo></icon-undo></button>
<button class="navigation-bar__button button" @click="redo" v-title="'重做'" :disabled="!canRedo"><icon-redo></icon-redo></button>
<div v-for="button in pagedownButtons" :key="button.method">
<button class="navigation-bar__button button" v-if="button.method" @click="pagedownClick(button.method)" v-title="button.titleWithShortcut">
<component :is="button.iconClass"></component>
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync">
<icon-sync></icon-sync>
</button>
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish">
<icon-upload></icon-upload>
</button>
<div class="navigation-bar__spacer" v-else></div>
</div>
</div>
</nav>
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
<icon-format-bold></icon-format-bold>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('italic')">
<icon-format-italic></icon-format-italic>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')">
<icon-format-strikethrough></icon-format-strikethrough>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('heading')">
<icon-format-size></icon-format-size>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('ulist')">
<icon-format-list-bulleted></icon-format-list-bulleted>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('olist')">
<icon-format-list-numbers></icon-format-list-numbers>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('table')">
<icon-table></icon-table>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('quote')">
<icon-format-quote-close></icon-format-quote-close>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('code')">
<icon-code-tags></icon-code-tags>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('link')">
<icon-link-variant></icon-link-variant>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('image')">
<icon-file-image></icon-file-image>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('hr')">
<icon-format-horizontal-rule></icon-format-horizontal-rule>
</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import publishSvc from '../services/publishSvc';
import animationSvc from '../services/animationSvc';
import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons';
import store from '../store';
import workspaceSvc from '../services/workspaceSvc';
import badgeSvc from '../services/badgeSvc';
// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
const getShortcut = (method) => {
let result = '';
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
if (`${shortcut.method || shortcut}` === method) {
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
}
return result;
});
return result && ` ${result}`;
};
export default {
data: () => ({
@ -89,7 +90,6 @@ export default {
}),
computed: {
...mapState([
'light',
'offline',
]),
...mapState('queue', [
@ -97,13 +97,6 @@ export default {
'isPublishRequested',
'currentLocation',
]),
...mapState('layout', [
'canUndo',
'canRedo',
]),
...mapState('content', [
'revisionContent',
]),
...mapGetters('layout', [
'styles',
]),
@ -113,20 +106,12 @@ export default {
...mapGetters('publishLocation', {
publishLocations: 'current',
}),
pagedownButtons() {
const buttonShowObj = store.getters['data/computedSettings'].editor.headButtons;
return pagedownButtons.filter(it => buttonShowObj[it.method]).map(button => ({
...button,
titleWithShortcut: `${button.title}${getShortcut(button.method)}`,
iconClass: `icon-${button.icon}`,
}));
},
isSyncPossible() {
return store.getters['workspace/syncToken'] ||
store.getters['syncLocation/current'].length;
return this.$store.getters['data/loginToken'] ||
this.$store.getters['syncLocation/current'].length;
},
showSpinner() {
return !store.state.queue.isEmpty;
return !this.$store.state.queue.isEmpty;
},
titleWidth() {
if (!this.mounted) {
@ -134,7 +119,9 @@ export default {
}
this.titleFakeElt.textContent = this.title;
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
return Math.min(width, this.styles.titleMaxWidth);
return width < this.styles.titleMaxWidth
? width
: this.styles.titleMaxWidth;
},
titleScrolling() {
const result = this.titleHover && !this.titleFocus;
@ -154,34 +141,15 @@ export default {
}
return result;
},
editCancelTrigger() {
const current = store.getters['file/current'];
return utils.serializeObject([
current.id,
current.name,
]);
},
},
methods: {
...mapMutations('content', [
'setRevisionContent',
]),
...mapActions('content', [
'restoreRevision',
]),
...mapActions('data', [
'toggleExplorer',
'toggleSideBar',
]),
undo() {
return editorSvc.clEditor.undoMgr.undo();
},
redo() {
return editorSvc.clEditor.undoMgr.redo();
},
requestSync() {
if (this.isSyncPossible && !this.isSyncRequested) {
syncSvc.requestSync(true);
syncSvc.requestSync();
}
},
requestPublish() {
@ -189,35 +157,23 @@ export default {
publishSvc.requestPublish();
}
},
switchTheme() {
store.dispatch('data/switchThemeSetting');
},
pagedownClick(name) {
if (store.getters['content/isCurrentEditable']) {
const text = editorSvc.clEditor.getContent();
if (this.$store.getters['content/current'].id &&
this.$store.getters['layout/styles'].showEditor
) {
editorSvc.pagedownEditor.uiManager.doClick(name);
if (text !== editorSvc.clEditor.getContent()) {
badgeSvc.addBadge('formatButtons');
}
}
},
async editTitle(toggle) {
editTitle(toggle) {
this.titleFocus = toggle;
if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else {
const title = this.title.trim();
this.title = store.getters['file/current'].name;
if (title && this.title !== title) {
try {
await workspaceSvc.storeItem({
...store.getters['file/current'],
name: title,
});
badgeSvc.addBadge('editCurrentFileName');
} catch (e) {
// Cancel
}
if (title) {
this.$store.dispatch('file/patchCurrent', { name: title.slice(0, 250) });
} else {
this.title = this.$store.getters['file/current'].name;
}
}
},
@ -227,19 +183,13 @@ export default {
}
this.titleInputElt.blur();
},
close() {
tempFileSvc.close();
},
},
created() {
this.$watch(
() => this.editCancelTrigger,
() => {
this.title = '';
this.editTitle(false);
},
{ immediate: true },
);
this.$store.watch(
() => this.$store.getters['file/current'].name,
(name) => {
this.title = name;
}, { immediate: true });
},
mounted() {
this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
@ -250,7 +200,7 @@ export default {
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.navigation-bar {
position: absolute;
@ -268,26 +218,22 @@ export default {
float: left;
&.navigation-bar__inner--button {
margin-right: 12px;
margin-right: 15px;
}
}
.navigation-bar__inner--right {
float: right;
/* prevent from seeing wrapped pagedownButtons */
margin-bottom: 20px;
}
.navigation-bar__inner--button {
margin: 0 4px;
}
.navigation-bar__inner--edit-pagedownButtons {
.navigation-bar__inner--edit-buttons {
margin-left: 15px;
.navigation-bar__button,
.navigation-bar__spacer {
.navigation-bar__button {
float: left;
}
}
@ -296,36 +242,18 @@ export default {
flex: none;
}
.navigation-bar__button,
.navigation-bar__spacer {
height: 36px;
padding: 0 4px;
/* prevent from seeing wrapped pagedownButtons */
margin-bottom: 20px;
}
.navigation-bar__button {
width: 34px;
padding: 0 7px;
transition: opacity 0.25s;
width: 36px;
height: 36px;
padding: 0 8px;
/* prevent from seeing wrapped buttons */
margin-bottom: 20px;
.navigation-bar__inner--button & {
padding: 0 4px;
width: 38px;
&.navigation-bar__button--theme {
width: 34px;
padding: 0 7px;
opacity: 0.85;
&:active,
&:focus,
&:hover {
opacity: 1;
}
}
&.navigation-bar__button--stackedit {
opacity: 0.85;
@ -338,30 +266,8 @@ export default {
}
}
.navigation-bar__button--revision {
width: 38px;
&:first-child {
margin-left: 10px;
}
&:last-child {
margin-right: 10px;
}
}
.navigation-bar__button--restore {
width: auto;
}
.navigation-bar__title {
margin: 0 4px;
font-size: 21px;
.layout--revision & {
position: absolute;
left: -9999px;
}
}
.navigation-bar__title,
@ -369,6 +275,7 @@ export default {
display: inline-block;
color: $navbar-color;
background-color: transparent;
font-size: 22px;
}
.navigation-bar__button--sync,
@ -435,7 +342,9 @@ export default {
}
.navigation-bar__title--input,
.navigation-bar__inner--edit-pagedownButtons {
.navigation-bar__inner--edit-buttons,
.navigation-bar__inner--button,
.navigation-bar__spinner {
display: none;
.navigation-bar--editor & {
@ -446,50 +355,32 @@ export default {
.navigation-bar__button {
display: none;
.navigation-bar__inner--button &,
.navigation-bar--editor & {
display: inline-block;
}
}
.navigation-bar__button--revision {
display: inline-block;
}
.navigation-bar__button--close {
color: lighten($link-color, 15%);
&:active,
&:focus,
&:hover {
color: lighten($link-color, 25%);
}
}
.navigation-bar__title--input {
cursor: pointer;
&.navigation-bar__title--focus {
cursor: text;
}
.navigation-bar--light & {
display: none;
}
}
$r: 10px;
$r: 9px;
$d: $r * 2;
$b: $d/10;
$t: 3000ms;
.navigation-bar__spinner {
width: 24px;
margin: 7px 0 0 8px;
width: 22px;
margin: 8px 0 0 8px;
color: #b2b2b2;
.icon {
width: 24px;
height: 24px;
width: 22px;
height: 22px;
color: transparentize($error-color, 0.5);
}
}
@ -499,7 +390,7 @@ $t: 3000ms;
height: $d;
display: block;
position: relative;
border: $b solid transparentize($navbar-color, 0.5);
border: $b solid currentColor;
border-radius: 50%;
margin: 2px;
@ -509,20 +400,20 @@ $t: 3000ms;
position: absolute;
display: block;
width: $b;
background-color: $navbar-color;
background-color: currentColor;
border-radius: $b * 0.5;
transform-origin: 50% 0;
}
&::before {
height: $r * 0.4;
height: $r * 0.35;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t linear infinite;
}
&::after {
height: $r * 0.6;
height: $r * 0.5;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t/4 linear infinite;

View File

@ -2,19 +2,12 @@
<div class="notification">
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
<div class="notification__icon flex flex--column flex--center">
<icon-alert v-if="item.type === 'error'"></icon-alert>
<icon-check-circle v-else-if="item.type === 'badge'"></icon-check-circle>
<icon-information v-else></icon-information>
<icon-information v-if="item.type === 'info'"></icon-information>
<icon-alert v-else-if="item.type === 'error'"></icon-alert>
</div>
<div class="notification__content">
{{item.content}}
</div>
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.reject">
</button>
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.resolve">
</button>
</div>
</div>
</template>
@ -30,7 +23,7 @@ export default {
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.notification {
position: absolute;
@ -56,17 +49,4 @@ export default {
margin-right: 12px;
flex: none;
}
.notification__button {
color: $navbar-color;
padding: 8px;
flex: none;
&:active,
&:focus,
&:hover {
color: $navbar-hover-color;
background-color: $navbar-hover-background;
}
}
</style>

View File

@ -1,17 +1,13 @@
<template>
<div class="preview">
<div class="preview__inner-1" @click="onClick" @scroll="onScroll">
<div class="preview__inner-2" :class="previewTheme" :style="{padding: styles.previewPadding}">
</div>
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
<comment-list v-if="styles.previewGutterWidth"></comment-list>
<preview-new-discussion-button v-if="!isCurrentTemp"></preview-new-discussion-button>
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
</div>
</div>
<div v-if="!styles.showEditor" class="preview__corner">
<button class="preview__button button" @click="toggleEditor(true)" v-title="'编辑文件'">
<div v-if="!styles.showEditor" class="preview__button-bar">
<div class="preview__button" @click="toggleEditor(true)">
<icon-pen></icon-pen>
</button>
</div>
</div>
</div>
</template>
@ -19,34 +15,16 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import CommentList from './gutters/CommentList';
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
import store from '../store';
const appUri = `${window.location.protocol}//${window.location.host}`;
export default {
components: {
CommentList,
PreviewNewDiscussionButton,
},
data: () => ({
previewTop: true,
}),
computed: {
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('theme', [
'currPreviewTheme',
]),
...mapGetters('layout', [
'styles',
]),
previewTheme() {
return `preview-theme--${this.currPreviewTheme || 'default'}`;
},
},
computed: mapGetters('layout', [
'styles',
]),
methods: {
...mapActions('data', [
'toggleEditor',
@ -68,51 +46,11 @@ export default {
this.previewTop = evt.target.scrollTop < 10;
},
},
mounted() {
const previewElt = this.$el.querySelector('.preview__inner-2');
const onDiscussionEvt = cb => (evt) => {
let elt = evt.target;
while (elt && elt !== previewElt) {
if (elt.discussionId) {
cb(elt.discussionId);
return;
}
elt = elt.parentNode;
}
};
const classToggler = toggle => (discussionId) => {
previewElt.getElementsByClassName(`discussion-preview-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.toggle('discussion-preview-highlighting--hover', toggle));
document.getElementsByClassName(`comment--discussion-${discussionId}`)
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
};
previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
store.commit('discussion/setCurrentDiscussionId', discussionId);
}));
this.$watch(
() => store.state.discussion.currentDiscussionId,
(discussionId, oldDiscussionId) => {
if (oldDiscussionId) {
previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`)
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
}
if (discussionId) {
previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
}
},
);
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.preview,
.preview__inner-1 {
@ -133,49 +71,23 @@ export default {
margin-top: 0;
}
$corner-size: 110px;
.preview__corner {
.preview__button-bar {
position: absolute;
top: 0;
right: 0;
&::before {
content: '';
position: absolute;
right: 0;
border-top: $corner-size solid rgba(0, 0, 0, 0.075);
border-left: $corner-size solid transparent;
pointer-events: none;
.app--dark & {
border-top-color: rgba(255, 255, 255, 0.075);
}
}
top: 10px;
right: 26px;
}
.preview__button {
position: absolute;
top: 15px;
right: 15px;
cursor: pointer;
color: rgba(0, 0, 0, 0.25);
width: 40px;
height: 40px;
padding: 5px;
color: rgba(0, 0, 0, 0.25);
border-radius: $border-radius-base;
.app--dark & {
color: rgba(255, 255, 255, 0.25);
}
&:active,
&:focus,
&:hover {
color: rgba(0, 0, 0, 0.33);
background-color: transparent;
.app--dark & {
color: rgba(255, 255, 255, 0.33);
}
background-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.75);
}
}
</style>

View File

@ -1,212 +0,0 @@
<template>
<div class="preview-in-page-buttons">
<ul>
<li class="before">
<icon-ellipsis></icon-ellipsis>
</li>
<li title="分享">
<a href="javascript:void(0)" @click="share"><icon-share></icon-share></a>
</li>
<li title="切换预览主题">
<dropdown-menu :selected="selectedTheme" :options="allThemes" :closeOnItemClick="false" @change="changeTheme">
<icon-select-theme></icon-select-theme>
</dropdown-menu>
</li>
<li title="Markdown语法帮助">
<a href="javascript:void(0)" @click="showHelp"><icon-help-circle></icon-help-circle></a>
</li>
</ul>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
// import juice from 'juice';
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: {
DropdownMenu,
},
data: () => ({
allThemes: [{
name: '默认主题',
value: 'default',
}, {
name: '凝夜紫',
value: 'ningyezi',
}, {
name: '草原绿',
value: 'caoyuangreen',
}, {
name: '雁栖湖',
value: 'yanqihu',
}, {
name: '灵动蓝',
value: 'activeblue',
}, {
name: '极客黑',
value: 'jikebrack',
}, {
name: '极简黑',
value: 'simplebrack',
}, {
name: '全栈蓝',
value: 'allblue',
}, {
name: '自定义',
value: 'custom',
}],
baseCss: '',
sharing: false,
}),
computed: {
...mapGetters('theme', [
'currPreviewTheme',
'customPreviewThemeStyle',
]),
...mapGetters('publishLocation', {
publishLocations: 'current',
}),
selectedTheme() {
return {
value: this.currPreviewTheme || 'default',
};
},
},
methods: {
...mapActions('data', [
'toggleSideBar',
]),
async changeTheme(item) {
await store.dispatch('theme/setPreviewTheme', item.value);
//
if (item.value === 'custom' && !this.customPreviewThemeStyle) {
this.toggleSideBar(true);
store.dispatch('data/setSideBarPanel', 'previewTheme');
}
},
showHelp() {
this.toggleSideBar(true);
store.dispatch('data/setSideBarPanel', 'help');
},
async share() {
if (this.sharing) {
store.dispatch('notification/info', '分享链接创建中...请稍后再试');
return;
}
try {
const currentFile = store.getters['file/current'];
await store.dispatch('modal/open', { type: 'shareHtmlPre', name: currentFile.name });
this.sharing = true;
const mainToken = store.getters['workspace/mainWorkspaceToken'];
if (!mainToken) {
store.dispatch('notification/info', '登录主文档空间之后才可使用分享功能!');
return;
}
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) {
tempGistId = filterLocations[0].gistId;
}
const location = (isGithub ? gistProvider : giteeGistProvider).makeLocation(
mainToken,
`分享-${currentFile.name}`,
true,
null,
);
location.templateId = 'styledHtmlWithTheme';
location.fileId = currentFile.id;
location.gistId = tempGistId;
const { gistId } = await publishSvc.publishLocationAndStore(location);
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) {
store.dispatch('notification/error', err);
}
} finally {
this.sharing = false;
}
},
},
};
</script>
<style lang="scss">
@import '../styles/variables.scss';
.preview-in-page-buttons {
position: absolute;
bottom: 10px;
right: -98px;
height: 34px;
padding: 5px;
background-color: rgba(84, 96, 114, 0.4);
border-radius: $border-radius-base;
transition: 0.5s;
display: flex;
.dropdown-menu {
display: none;
}
&:active,
&:focus,
&:hover {
right: 0;
transition: 0.5s;
background-color: #546072;
.dropdown-menu {
display: block;
}
}
.dropdown-menu-items {
bottom: 100%;
top: unset;
}
ul {
padding: 0;
margin-left: 10px;
line-height: 20px;
li {
line-height: 16px;
width: 16px;
display: inline-block;
vertical-align: middle;
list-style: none;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
.icon {
color: #fff;
opacity: 0.7;
&:active,
&:focus,
&:hover {
opacity: 1;
}
}
}
.before {
margin-left: -16px;
margin-right: 0;
}
}
}
</style>

View File

@ -1,30 +1,25 @@
<template>
<div class="side-bar flex flex--column">
<div class="side-title flex flex--row">
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'主菜单'">
<icon-dots-horizontal></icon-dots-horizontal>
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')">
<icon-arrow-left></icon-arrow-left>
</button>
<div class="side-title__title">
{{panelName}}
</div>
<button class="side-title__button button" @click="toggleSideBar(false)" v-title="'关闭侧边栏'">
<button class="side-title__button button" @click="toggleSideBar(false)">
<icon-close></icon-close>
</button>
</div>
<div class="side-bar__inner">
<main-menu v-if="panel === 'menu'"></main-menu>
<workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
<history-menu v-else-if="panel === 'history'"></history-menu>
<export-menu v-else-if="panel === 'export'"></export-menu>
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
<workspace-backup-menu v-else-if="panel === 'workspaceBackups'"></workspace-backup-menu>
<more-menu v-else-if="panel === 'more'"></more-menu>
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
</div>
<edit-theme-menu v-else-if="panel === 'editTheme'"></edit-theme-menu>
<preview-theme-menu v-else-if="panel === 'previewTheme'"></preview-theme-menu>
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
<toc>
</toc>
@ -36,56 +31,41 @@
<script>
import { mapActions } from 'vuex';
import Toc from './Toc';
import MenuEntry from './menus/MenuEntry';
import MainMenu from './menus/MainMenu';
import WorkspacesMenu from './menus/WorkspacesMenu';
import SyncMenu from './menus/SyncMenu';
import PublishMenu from './menus/PublishMenu';
import HistoryMenu from './menus/HistoryMenu';
import ImportExportMenu from './menus/ImportExportMenu';
import WorkspaceBackupMenu from './menus/WorkspaceBackupMenu';
import EditThemeMenu from './menus/EditThemeMenu';
import PreviewThemeMenu from './menus/PreviewThemeMenu';
import ExportMenu from './menus/ExportMenu';
import MoreMenu from './menus/MoreMenu';
import markdownSample from '../data/markdownSample.md';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';
const panelNames = {
menu: '菜单',
workspaces: '文档空间',
help: 'Markdown 帮助',
toc: '目录',
sync: '同步',
publish: '发布',
history: '文件历史',
importExport: '导入/导出',
workspaceBackups: '文档空间备份',
editTheme: '编辑区主题',
previewTheme: '预览区主题',
menu: 'Menu',
help: 'Markdown cheat sheet',
toc: 'Table of contents',
sync: 'Synchronize',
publish: 'Publish',
export: 'Export to disk',
more: 'More',
};
export default {
components: {
Toc,
MenuEntry,
MainMenu,
WorkspacesMenu,
SyncMenu,
PublishMenu,
HistoryMenu,
ImportExportMenu,
WorkspaceBackupMenu,
EditThemeMenu,
PreviewThemeMenu,
ExportMenu,
MoreMenu,
},
data: () => ({
markdownSample: markdownConversionSvc.highlight(markdownSample),
}),
computed: {
panel() {
if (store.state.light) {
return null; // No menu in light mode
}
const result = store.getters['data/layoutSettings'].sideBarPanel;
return panelNames[result] ? result : 'menu';
return this.$store.getters['data/localSettings'].sideBarPanel;
},
panelName() {
return panelNames[this.panel];
@ -103,7 +83,7 @@ export default {
</script>
<style lang="scss">
@import '../styles/variables.scss';
@import 'common/variables.scss';
.side-bar {
overflow: hidden;
@ -112,7 +92,6 @@ export default {
hr {
margin: 10px 40px;
display: none;
border-top: 1px solid $hr-color;
}
* + hr {
@ -122,11 +101,6 @@ export default {
hr + hr {
display: none;
}
.textfield {
font-size: 14px;
height: 26px;
}
}
.side-bar__inner {
@ -139,12 +113,6 @@ export default {
width: 100%;
height: 100%;
overflow: auto;
&::after {
content: '';
display: block;
height: 40px;
}
}
.side-bar__panel--hidden {
@ -152,11 +120,11 @@ export default {
}
.side-bar__panel--menu {
padding: 10px;
padding: 10px 10px 50px;
}
.side-bar__panel--help {
padding: 0 10px 0 20px;
padding: 0 10px 40px 20px;
pre {
font-size: 0.9em;
@ -175,17 +143,15 @@ export default {
}
}
.side-bar__info {
.side-bar__warning {
padding: 10px;
margin: -10px -10px 10px;
background-color: $info-bg;
font-size: 0.95em;
margin: 0 -10px;
color: darken($error-color, 10);
background-color: transparentize($error-color, 0.925);
p {
margin: 10px 15px;
font-size: 0.9rem;
opacity: 0.67;
line-height: 1.3;
margin: 10px;
line-height: 1.4;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More