Compare commits
164 Commits
Author | SHA1 | Date | |
---|---|---|---|
7a82ba2250 | |||
445cfe69fc | |||
40e77fef15 | |||
348b5668fd | |||
60c751f340 | |||
024706f20f | |||
408618eceb | |||
9a7570b02f | |||
182c863eef | |||
|
d422df5e42 | ||
|
91215e6db9 | ||
|
1286c42d4c | ||
|
3467a6ee09 | ||
|
b4f4c71f85 | ||
|
39167fb193 | ||
|
97b8d3c288 | ||
|
b4c9407b06 | ||
|
96ea8cd0db | ||
|
550bb2fd91 | ||
|
2092045b7f | ||
|
12e4befa96 | ||
|
80e0e3bc99 | ||
|
4747f91749 | ||
|
e04fd5a911 | ||
|
9cd27e274e | ||
|
81cad7ee84 | ||
|
81612deab7 | ||
|
282b546edc | ||
|
1727be1eaf | ||
|
57931b9db2 | ||
|
d175557ab9 | ||
|
8e12eaebd2 | ||
|
90d887519d | ||
|
c1232b59db | ||
|
a40af9c545 | ||
|
f3d827fef1 | ||
|
92f2c4dee6 | ||
|
74f25af839 | ||
|
20d7a9d2db | ||
|
64d493d692 | ||
|
eda517cd61 | ||
|
24635c54ed | ||
|
87c37401ed | ||
|
599d71b597 | ||
|
0e02822add | ||
|
1daa5afe39 | ||
|
2e9e4b73f6 | ||
|
4243a41e31 | ||
|
ae828cfb56 | ||
|
58c9144612 | ||
|
1b8124f2a2 | ||
|
8713688b57 | ||
|
e65c433f13 | ||
|
b1691e0d4f | ||
|
4d8ff0ea0c | ||
|
d757b48d99 | ||
|
d927099b28 | ||
|
9ebde2eb75 | ||
|
bda261a767 | ||
|
9419865d76 | ||
|
be9323c408 | ||
|
a756acf27c | ||
|
e731016e04 | ||
|
6cca063f8c | ||
|
13b9528840 | ||
|
31bec53520 | ||
|
4e9acad585 | ||
|
5eb2b2e67a | ||
|
2b45a94879 | ||
|
5a30338e83 | ||
|
808891e47c | ||
|
d3193e1739 | ||
|
df91db5882 | ||
|
26e8979245 | ||
|
dd78ec7b3a | ||
|
401c2787af | ||
|
a4ab4b2da1 | ||
|
e7450df251 | ||
|
058fcaa147 | ||
|
ed79c8cd49 | ||
|
867315a19d | ||
|
480875a5ec | ||
|
440c5e93b8 | ||
|
405e082651 | ||
|
7335455185 | ||
|
7da611b398 | ||
|
554547af5a | ||
|
380980d66f | ||
|
68f281c6e7 | ||
|
545f8da3cb | ||
|
ee9bd1ab5a | ||
|
e7fa160383 | ||
|
f71cef4d9f | ||
|
347358f6bc | ||
|
8aff518e34 | ||
|
21a3e59b5d | ||
|
95d27a4a0a | ||
|
b1ad58a121 | ||
|
d51c19d6fd | ||
|
398784efc4 | ||
|
f020cb887b | ||
|
6fa7992685 | ||
|
a6493a41da | ||
|
f5b4627083 | ||
|
fc74346b4b | ||
|
a957432749 | ||
|
d3abfe2fe6 | ||
|
f073f00019 | ||
|
06827fe3eb | ||
|
e69474db36 | ||
|
ed1a07ccac | ||
|
3b66956763 | ||
|
4158737843 | ||
|
fa1f207ca1 | ||
|
5bd3a259b7 | ||
|
8fa5084a25 | ||
|
cf9089b087 | ||
|
04ee93237d | ||
|
c6d5ddfe3d | ||
|
a85fe36105 | ||
|
be3cb3c65f | ||
|
1d57f0365f | ||
|
3d61b71350 | ||
|
5f116222fd | ||
|
1da8fbd02a | ||
|
36a4832eff | ||
|
80a49a3da5 | ||
|
b033b7372a | ||
|
c99e46cb6a | ||
|
beeb026abe | ||
|
13a584e400 | ||
|
51367f1ae5 | ||
|
08e81f39b0 | ||
|
9239541df8 | ||
|
19d3a16034 | ||
|
73ead46873 | ||
|
73b23e1659 | ||
|
246f5b394e | ||
|
dc8b4677be | ||
|
7cd0cee836 | ||
|
bfb7a6447e | ||
|
59a867b8b9 | ||
|
3c53903239 | ||
|
1352ea0a9a | ||
|
09416c75a4 | ||
|
492958702a | ||
|
ce91307a9c | ||
|
72f9fea42e | ||
|
e1eb7ec350 | ||
|
9f800ecece | ||
|
8ec5bf9ec6 | ||
|
1580f1c658 | ||
|
cb1354516f | ||
|
cc6c8ff5ab | ||
|
8a045e3ca9 | ||
|
31ee3d15e7 | ||
|
070206cb5a | ||
|
698ddb9abd | ||
|
7a5a4e65c2 | ||
|
a15309be14 | ||
|
273b4e3500 | ||
|
0bee41647b | ||
|
f489a611b0 | ||
|
45bee6bbf6 |
@ -2,3 +2,8 @@ node_modules
|
||||
.git
|
||||
dist
|
||||
.history
|
||||
images
|
||||
docs
|
||||
Dockerfile
|
||||
README.md
|
||||
build.sh
|
||||
|
@ -1,10 +1,10 @@
|
||||
FROM mafgwo/wkhtmltopdf-nodejs:11.15.0
|
||||
|
||||
RUN mkdir -p /opt/stackedit
|
||||
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
|
||||
|
260
README.md
@ -1,64 +1,183 @@
|
||||
# StackEdit中文版
|
||||
<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 />
|
||||
|
||||
从 [StackEdit 官方](https://github.com/benweet/stackedit) fork出来,然后加上了 **Gitee** 的支持,并且已经重新打了镜像,以下官方的部署方式,除了Docker镜像地址不同,其他均一致。
|
||||
## 说明
|
||||
|
||||
### Fork出来修改的原因
|
||||
StackEdit的作者可能因为什么原因,已经很久不维护了,Github授权登录很早之前就登录不了了,并且还没法支持国内常用的Gitee,所以想到Fork出来改,大概花了周末一整天终于改好了。
|
||||
本项目为本人clone修改自用,如果你也喜欢,请至原作者处获取及交流。
|
||||
|
||||
新的Docker镜像在中央仓库为:[mafgwo/stackedit](https://hub.docker.com/r/mafgwo/stackedit)
|
||||
## 截图
|
||||
|
||||
并增加了以下2个环境变量:
|
||||
- `GITEE_CLIENT_ID` Gitee 的 Client ID
|
||||
- `GITEE_CLIENT_SECRET` Gitee 的 Client Secret
|
||||
**亮暗主题切换、编辑主题切换**
|
||||
![](./images/theme.gif)
|
||||
|
||||
### TODO: 关于后续的一些想法
|
||||
- 支持**Gitea**、**Gogs**两个轻量级且适于自建的Git仓库(毕竟Gitlab对机器配置要求较高)。想支持这两个主要也是考虑到其实很多公司已经禁用了Github或Gitee仓库,在公司都没法连上自己的Git仓库。
|
||||
- 汉化,毕竟大家最熟悉的还是母语,并且该编辑器功能页面也不多,汉化工作量并不会很大。
|
||||
- 替换主文档空间为Gitee(原版本主文档空间是Google Drive,国内只有fan墙才可以用)
|
||||
- 引入mdnice,右边预览增加mdnice预览选项,主要含选主题(含mdnice常用20多个主题)、支持自定义主题、复制到公众号、复制到知乎、复制到稀土掘金等基本功能,便于喜欢写公众号、博客的同学可以更好更快的排版。
|
||||
- ... 另外,朋友们有好的想法也可以在Issue或者加我微信 qicoding 提给我。
|
||||
**支持的文档空间**
|
||||
![](./images/workspace.png)
|
||||
|
||||
#### TODO 进度
|
||||
**拖拽粘贴上传图片**
|
||||
![](./images/uploadimg.gif)
|
||||
|
||||
**已支持Gitea (2022-05-25),自建Gitea要支持跨域,不知道如何支持跨域提issue我会回答**
|
||||
对应Docker版本:5.15.3, tag: v5.15.3
|
||||
**支持文档搜索**
|
||||
![](./images/search.gif)
|
||||
|
||||
**Gogs目前无法支持,因为API目前不支持直接把Markdown推上去,如果后续Gogs的API支持了再添加**
|
||||
**ChatGPT集成协助写作**
|
||||
![](./images/chatgpt.gif)
|
||||
|
||||
**已汉化主要功能部分(2022-06-01)**
|
||||
对应Docker版本:5.15.5, tag: v5.15.5
|
||||
## 相比国外开源版本的区别:
|
||||
|
||||
**已完成修改主文档空间为Gitee(2022-06-04)**
|
||||
对应Docker版本:5.15.6, tag: v5.15.6
|
||||
- 修复了Github授权登录问题
|
||||
- 支持了Gitee仓库(2022-05-25)
|
||||
- 支持了Gitea仓库(2022-05-25)
|
||||
- 汉化(2022-06-01)
|
||||
- 主文档空间从GoogleDrive切换为Gitee(2022-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和Secret(2022-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)
|
||||
|
||||
## 国外开源版本弊端:
|
||||
|
||||
### 目前已部署地址
|
||||
https://stackedit.cn/
|
||||
- 作者已经不维护了或很少维护了
|
||||
- 不支持国内常用Gitee
|
||||
- 强依赖GoogleDrive,而Google Drive在国内不能正常访问
|
||||
|
||||
该地址可以作为试用或长期使用,本人承诺绝对没采集任何人的Token等敏感信息,不需要担心私有仓库泄漏。
|
||||
## 部署说明
|
||||
|
||||
> 建议docker-compose方式部署,其他部署方式如遇到问题欢迎提issue。
|
||||
|
||||
# 以下是官方原有README内容
|
||||
docker官方仓库下载太慢可以使用阿里云的镜像仓库,镜像仓库地址:registry.cn-hangzhou.aliyuncs.com/mafgwo/stackedit:【版本号】
|
||||
|
||||
# StackEdit
|
||||
`docker-compose.yml`如下:
|
||||
|
||||
[![Build Status](https://img.shields.io/travis/benweet/stackedit.svg?style=flat)](https://travis-ci.org/benweet/stackedit) [![NPM version](https://img.shields.io/npm/v/stackedit.svg?style=flat)](https://www.npmjs.org/package/stackedit)
|
||||
```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
|
||||
```
|
||||
|
||||
> Full-featured, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites.
|
||||
|
||||
https://stackedit.io/
|
||||
|
||||
### Ecosystem
|
||||
|
||||
- [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
|
||||
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
|
||||
- NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
|
||||
- [Community](https://community.stackedit.io/)
|
||||
|
||||
### Build
|
||||
docker-compose方式的启动或停止命令
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
# 在 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
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
@ -70,62 +189,3 @@ npm run build
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
|
||||
### Deploy with Helm
|
||||
|
||||
StackEdit Helm chart allows easy StackEdit deployment to any Kubernetes cluster.
|
||||
You can use it to configure deployment with your existing ingress controller and cert-manager.
|
||||
|
||||
```bash
|
||||
# Add the StackEdit Helm repository
|
||||
helm repo add stackedit https://benweet.github.io/stackedit-charts/
|
||||
|
||||
# Update your local Helm chart repository cache
|
||||
helm repo update
|
||||
|
||||
# Deploy StackEdit chart to your cluster
|
||||
helm install --name stackedit stackedit/stackedit \
|
||||
--set dropboxAppKey=$DROPBOX_API_KEY \
|
||||
--set dropboxAppKeyFull=$DROPBOX_FULL_ACCESS_API_KEY \
|
||||
--set googleClientId=$GOOGLE_CLIENT_ID \
|
||||
--set googleApiKey=$GOOGLE_API_KEY \
|
||||
--set githubClientId=$GITHUB_CLIENT_ID \
|
||||
--set githubClientSecret=$GITHUB_CLIENT_SECRET \
|
||||
--set wordpressClientId=\"$WORDPRESS_CLIENT_ID\" \
|
||||
--set wordpressSecret=$WORDPRESS_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Later, to upgrade StackEdit to the latest version:
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm upgrade stackedit stackedit/stackedit
|
||||
```
|
||||
|
||||
If you want to uninstall StackEdit:
|
||||
|
||||
```bash
|
||||
helm delete --purge stackedit
|
||||
```
|
||||
|
||||
If you want to use your existing ingress controller and cert-manager issuer:
|
||||
|
||||
```bash
|
||||
# See https://docs.cert-manager.io/en/latest/tutorials/acme/quick-start/index.html
|
||||
helm install --name stackedit stackedit/stackedit \
|
||||
--set dropboxAppKey=$DROPBOX_API_KEY \
|
||||
--set dropboxAppKeyFull=$DROPBOX_FULL_ACCESS_API_KEY \
|
||||
--set googleClientId=$GOOGLE_CLIENT_ID \
|
||||
--set googleApiKey=$GOOGLE_API_KEY \
|
||||
--set githubClientId=$GITHUB_CLIENT_ID \
|
||||
--set githubClientSecret=$GITHUB_CLIENT_SECRET \
|
||||
--set wordpressClientId=\"$WORDPRESS_CLIENT_ID\" \
|
||||
--set wordpressSecret=$WORDPRESS_CLIENT_SECRET \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.annotations."kubernetes\.io/ingress\.class"=nginx \
|
||||
--set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod \
|
||||
--set ingress.hosts[0].host=stackedit.example.com \
|
||||
--set ingress.hosts[0].paths[0]=/ \
|
||||
--set ingress.tls[0].secretName=stackedit-tls \
|
||||
--set ingress.tls[0].hosts[0]=stackedit.example.com
|
||||
```
|
||||
|
37
build.sh
Normal file
@ -0,0 +1,37 @@
|
||||
#!/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 "操作完成"
|
@ -15,6 +15,11 @@ wordpressSecret: ""
|
||||
paypalReceiverEmail: ""
|
||||
awsAccessKeyId: ""
|
||||
awsSecretAccessKey: ""
|
||||
giteaClientId: ""
|
||||
giteaClientSecret: ""
|
||||
giteaUrl: ""
|
||||
gitlabClientId: ""
|
||||
gitlabUrl: ""
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 7.6 KiB |
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "StackEdit中文版",
|
||||
"description": "支持Gitee仓库的浏览器内 Markdown 编辑器",
|
||||
"version": "1.0.13",
|
||||
"description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器",
|
||||
"version": "5.15.17",
|
||||
"manifest_version": 2,
|
||||
"container" : "GOOGLE_DRIVE",
|
||||
"container": "GITEE",
|
||||
"api_console_project_id": "241271498917",
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
@ -15,10 +15,10 @@
|
||||
},
|
||||
"app": {
|
||||
"urls": [
|
||||
"https://stackedit.cn/"
|
||||
"https://md.jonylee.top/"
|
||||
],
|
||||
"launch": {
|
||||
"web_url": "https://stackedit.cn/app"
|
||||
"web_url": "https://md.jonylee.top/app"
|
||||
}
|
||||
},
|
||||
"offline_enabled": true,
|
||||
|
@ -9,4 +9,10 @@ module.exports = merge(prodEnv, {
|
||||
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"',
|
||||
})
|
10
docs/大文档导出PDF方式.md
Normal file
@ -0,0 +1,10 @@
|
||||
# 大文档导出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` 查看帮助文档。
|
||||
|
||||
|
||||
|
20
docs/部署之GitHub应用创建.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 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)
|
||||
|
35
docs/部署之Gitea应用创建.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 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;
|
||||
}
|
||||
```
|
19
docs/部署之Gitee应用创建.md
Normal file
@ -0,0 +1,19 @@
|
||||
# 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。
|
BIN
images/chatgpt.gif
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
images/dark.png
Normal file
After Width: | Height: | Size: 793 KiB |
BIN
images/fileSearch.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
images/gitea/gitea01.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
images/gitea/gitea02.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
images/gitea/gitea03.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
images/gitea/gitea04.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
images/gitee/gitee01.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
images/gitee/gitee02.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
images/gitee/gitee03.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
images/gitee/gitee04.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
images/github/github01.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
images/github/github02.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
images/github/github03.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
images/github/github04.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
images/github/github05.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
images/imageBed.png
Normal file
After Width: | Height: | Size: 339 KiB |
BIN
images/light.png
Normal file
After Width: | Height: | Size: 726 KiB |
BIN
images/qq.jpeg
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
images/search.gif
Normal file
After Width: | Height: | Size: 360 KiB |
BIN
images/theme.gif
Normal file
After Width: | Height: | Size: 937 KiB |
BIN
images/uploadimg.gif
Normal file
After Width: | Height: | Size: 761 KiB |
BIN
images/workspace.png
Normal file
After Width: | Height: | Size: 195 KiB |
17
index.html
@ -1,28 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>StackEdit中文版</title>
|
||||
<link rel="canonical" href="https://stackedit.cn/app">
|
||||
<meta name="description" content="免费,开源,功能全面的Markdown编辑器。">
|
||||
<title>Markdown编辑器-JonyLee的设计导航</title>
|
||||
<link rel="canonical" href="https://md.jonylee.top">
|
||||
<meta name="description" content="StackEdit中文版,免费,开源,功能全面的Markdown编辑器。">
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<!-- baidu统计 -->
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?20a1e7a201b42702c49074c87a1f1035";
|
||||
hm.src = "https://hm.baidu.com/hm.js?dad4b4383b13eedea1ab45ee323df1c3";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
<!-- baidu统计结束 -->
|
||||
</body>
|
||||
|
||||
</html>
|
443
package-lock.json
generated
13
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "stackedit",
|
||||
"version": "5.15.6",
|
||||
"description": "Free, open-source, full-featured Markdown editor",
|
||||
"author": "Benoit Schweblin",
|
||||
"version": "5.15.21",
|
||||
"description": "免费, 开源, 功能齐全的 Markdown 编辑器",
|
||||
"author": "Benoit Schweblin, 豆萁",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mafgwo/stackedit/issues"
|
||||
@ -27,7 +27,6 @@
|
||||
"dependencies": {
|
||||
"@vue/test-utils": "^1.0.0-beta.16",
|
||||
"abcjs": "^5.2.0",
|
||||
"aws-sdk": "^2.317.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bezier-easing": "^1.1.0",
|
||||
"body-parser": "^1.18.2",
|
||||
@ -35,11 +34,10 @@
|
||||
"compression": "^1.7.0",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"google-id-token-verifier": "^0.2.3",
|
||||
"handlebars": "^4.0.10",
|
||||
"indexeddbshim": "^3.6.2",
|
||||
"js-yaml": "^3.11.0",
|
||||
"katex": "^0.13.0",
|
||||
"katex": "^0.16.2",
|
||||
"markdown-it": "^8.4.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-deflist": "^2.0.2",
|
||||
@ -57,7 +55,7 @@
|
||||
"request": "^2.85.0",
|
||||
"serve-static": "^1.13.2",
|
||||
"tmp": "^0.0.33",
|
||||
"turndown": "^4.0.2",
|
||||
"turndown": "^7.1.1",
|
||||
"vue": "^2.5.16",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
@ -100,6 +98,7 @@
|
||||
"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",
|
||||
"npm-bump": "^0.0.23",
|
||||
"offline-plugin": "^5.0.3",
|
||||
|
@ -1,7 +1,5 @@
|
||||
const pandocPath = process.env.PANDOC_PATH || 'pandoc';
|
||||
const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||
const userBucketName = process.env.USER_BUCKET_NAME || 'stackedit-users';
|
||||
const paypalUri = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
|
||||
const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;
|
||||
|
||||
const dropboxAppKey = process.env.DROPBOX_APP_KEY;
|
||||
@ -13,12 +11,16 @@ 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,
|
||||
userBucketName,
|
||||
paypalUri,
|
||||
paypalReceiverEmail,
|
||||
dropboxAppKey,
|
||||
dropboxAppKeyFull,
|
||||
@ -29,15 +31,24 @@ exports.values = {
|
||||
googleClientId,
|
||||
googleApiKey,
|
||||
wordpressClientId,
|
||||
giteaClientId,
|
||||
giteaClientSecret,
|
||||
giteaUrl,
|
||||
gitlabClientId,
|
||||
gitlabClientSecret,
|
||||
gitlabUrl,
|
||||
};
|
||||
|
||||
exports.publicValues = {
|
||||
dropboxAppKey,
|
||||
dropboxAppKeyFull,
|
||||
githubClientId,
|
||||
giteeClientId,
|
||||
googleClientId,
|
||||
googleApiKey,
|
||||
wordpressClientId,
|
||||
allowSponsorship: !!paypalReceiverEmail,
|
||||
giteaClientId,
|
||||
giteaUrl,
|
||||
gitlabClientId,
|
||||
gitlabUrl,
|
||||
};
|
||||
|
40
server/gitea.js
Normal file
@ -0,0 +1,40 @@
|
||||
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'),
|
||||
);
|
||||
};
|
@ -3,13 +3,15 @@ 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: conf.values.giteeClientSecret,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: oauth2RedirectUri,
|
||||
|
40
server/gitlab.js
Normal file
@ -0,0 +1,40 @@
|
||||
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'),
|
||||
);
|
||||
};
|
@ -2,9 +2,10 @@ const compression = require('compression');
|
||||
const serveStatic = require('serve-static');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
const user = require('./user');
|
||||
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');
|
||||
@ -27,14 +28,28 @@ module.exports = (app) => {
|
||||
|
||||
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.get('/userInfo', user.userInfo);
|
||||
app.post('/pdfExport', pdf.generate);
|
||||
app.post('/pandocExport', pandoc.generate);
|
||||
app.post('/paypalIpn', bodyParser.urlencoded({
|
||||
extended: false,
|
||||
}), user.paypalIpn);
|
||||
|
||||
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
|
||||
@ -46,16 +61,23 @@ module.exports = (app) => {
|
||||
// Google Drive action receiver
|
||||
app.get('/googleDriveAction', (req, res) =>
|
||||
res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));
|
||||
|
||||
// Serve static resources
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Serve index.html in /app
|
||||
app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
|
||||
// Serve the static folder with 30 day max-age
|
||||
app.use('/themes', serveStatic(resolvePath('static/themes'), {
|
||||
maxAge: '5d',
|
||||
}));
|
||||
|
||||
// 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 static resources
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Serve index.html in /app
|
||||
app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
|
||||
|
||||
// Serve the static folder with 1 year max-age
|
||||
app.use('/static', serveStatic(resolvePath('dist/static'), {
|
||||
|
116
server/user.js
@ -1,116 +0,0 @@
|
||||
const request = require('request');
|
||||
const AWS = require('aws-sdk');
|
||||
const verifier = require('google-id-token-verifier');
|
||||
const conf = require('./conf');
|
||||
|
||||
const s3Client = new AWS.S3();
|
||||
|
||||
const cb = (resolve, reject) => (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getUser = id => new Promise((resolve, reject) => {
|
||||
s3Client.getObject({
|
||||
Bucket: conf.values.userBucketName,
|
||||
Key: id,
|
||||
}, cb(resolve, reject));
|
||||
})
|
||||
.then(
|
||||
res => JSON.parse(`${res.Body}`),
|
||||
(err) => {
|
||||
if (err.code !== 'NoSuchKey') {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
exports.putUser = (id, user) => new Promise((resolve, reject) => {
|
||||
s3Client.putObject({
|
||||
Bucket: conf.values.userBucketName,
|
||||
Key: id,
|
||||
Body: JSON.stringify(user),
|
||||
}, cb(resolve, reject));
|
||||
});
|
||||
|
||||
exports.removeUser = id => new Promise((resolve, reject) => {
|
||||
s3Client.deleteObject({
|
||||
Bucket: conf.values.userBucketName,
|
||||
Key: id,
|
||||
}, cb(resolve, reject));
|
||||
});
|
||||
|
||||
exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
|
||||
.verify(idToken, conf.values.googleClientId, cb(resolve, reject)))
|
||||
.then(tokenInfo => exports.getUser(tokenInfo.sub));
|
||||
|
||||
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
|
||||
.then(
|
||||
user => res.send(Object.assign({
|
||||
sponsorUntil: 0,
|
||||
}, user)),
|
||||
err => res
|
||||
.status(400)
|
||||
.send(err ? err.message || err.toString() : 'invalid_token'),
|
||||
);
|
||||
|
||||
exports.paypalIpn = (req, res, next) => Promise.resolve()
|
||||
.then(() => {
|
||||
const userId = req.body.custom;
|
||||
const paypalEmail = req.body.payer_email;
|
||||
const gross = parseFloat(req.body.mc_gross);
|
||||
let sponsorUntil;
|
||||
if (gross === 5) {
|
||||
sponsorUntil = Date.now() + (3 * 31 * 24 * 60 * 60 * 1000); // 3 months
|
||||
} else if (gross === 15) {
|
||||
sponsorUntil = Date.now() + (366 * 24 * 60 * 60 * 1000); // 1 year
|
||||
} else if (gross === 25) {
|
||||
sponsorUntil = Date.now() + (2 * 366 * 24 * 60 * 60 * 1000); // 2 years
|
||||
} else if (gross === 50) {
|
||||
sponsorUntil = Date.now() + (5 * 366 * 24 * 60 * 60 * 1000); // 5 years
|
||||
}
|
||||
if (
|
||||
req.body.receiver_email !== conf.values.paypalReceiverEmail ||
|
||||
req.body.payment_status !== 'Completed' ||
|
||||
req.body.mc_currency !== 'USD' ||
|
||||
(req.body.txn_type !== 'web_accept' && req.body.txn_type !== 'subscr_payment') ||
|
||||
!userId || !sponsorUntil
|
||||
) {
|
||||
// Ignoring PayPal IPN
|
||||
return res.end();
|
||||
}
|
||||
// Processing PayPal IPN
|
||||
req.body.cmd = '_notify-validate';
|
||||
return new Promise((resolve, reject) => request.post({
|
||||
uri: conf.values.paypalUri,
|
||||
form: req.body,
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (body !== 'VERIFIED') {
|
||||
reject(new Error('PayPal IPN unverified'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}))
|
||||
.then(() => exports.putUser(userId, {
|
||||
paypalEmail,
|
||||
sponsorUntil,
|
||||
}))
|
||||
.then(() => res.end());
|
||||
})
|
||||
.catch(next);
|
||||
|
||||
exports.checkSponsor = (idToken) => {
|
||||
if (!conf.publicValues.allowSponsorship) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (!idToken) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return exports.getUserFromToken(idToken)
|
||||
.then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
|
||||
};
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
@ -1,10 +1 @@
|
||||
|
||||
<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>
|
||||
<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>
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
3
src/assets/iconCustom.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<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>
|
After Width: | Height: | Size: 1.3 KiB |
@ -1,5 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32">
|
||||
<path style="fill:#fff" d="m395.9 484.2-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"/>
|
||||
<path style="fill:#609926" 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"/>
|
||||
<path style="fill:#609926" 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"/>
|
||||
</svg>
|
||||
<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.2 KiB After Width: | Height: | Size: 2.1 KiB |
@ -1,4 +1,3 @@
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 58">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
|
Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 1010 B |
6
src/assets/iconGithubDark.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<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>
|
After Width: | Height: | Size: 1007 B |
25
src/assets/iconSmms.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.0 KiB |
@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M23.997,6.002c0,-3.311 -2.688,-5.999 -5.999,-5.999l-11.999,0c-3.311,0 -5.999,2.688 -5.999,5.999l0,11.999c0,3.311 2.688,5.999 5.999,5.999l11.999,0c3.311,0 5.999,-2.688 5.999,-5.999l0,-11.999Z" style="fill:none;"/><clipPath id="_clip1"><path d="M23.997,6.002c0,-3.311 -2.688,-5.999 -5.999,-5.999l-11.999,0c-3.311,0 -5.999,2.688 -5.999,5.999l0,11.999c0,3.311 2.688,5.999 5.999,5.999l11.999,0c3.311,0 5.999,-2.688 5.999,-5.999l0,-11.999Z"/></clipPath><g clip-path="url(#_clip1)"><path d="M23.997,0.003l-24,0l12,12l12,-12Z" style="fill:#ffd700;"/><path d="M-0.003,0.003l0,24l12,-12l-12,-12Z" style="fill:#a5c700;"/><path d="M-0.003,24.003l24,0l-12,-12l-12,12Z" style="fill:#ff8a00;"/><path d="M23.997,24.003l0,-24l-12,12l12,12Z" style="fill:#66aefd;"/><path d="M22.497,-1.497l-10.5,10.497l3,3.003l10.5,-10.5l-3,-3Z" style="fill:url(#_Linear2);"/><path d="M25.499,22.503l-10.498,-10.5l-3.002,3l10.5,10.5l3,-3Z" style="fill:url(#_Linear3);"/><path d="M1.497,25.501l10.5,-10.497l-3,-3.003l-10.5,10.5l3,3Z" style="fill:url(#_Linear4);"/><path d="M-1.503,1.503l10.498,10.5l3.002,-3l-10.5,-10.5l-3,3Z" style="fill:url(#_Linear5);"/></g><path d="M21.75,5.852c0,-2.195 -1.782,-3.977 -3.977,-3.977l-11.546,0c-2.195,0 -3.977,1.782 -3.977,3.977l0,11.546c0,2.195 1.782,3.977 3.977,3.977l11.546,0c2.195,0 3.977,-1.782 3.977,-3.977l0,-11.546Z" style="fill:#fff;"/><path d="M4.633,6.013l1.37,0l0,-1.828l1.399,0l0,1.828l1.696,0l0,-1.828l1.399,0l0,1.828l1.37,0l0,1.691l-1.37,0l0,1.902l1.37,0l0,1.69l-1.37,0l0,1.829l-1.399,0l0,-1.829l-1.696,0l0,1.829l-1.399,0l0,-1.829l-1.37,0l0,-1.69l1.37,0l0,-1.902l-1.37,0l0,-1.691Zm2.769,1.691l0,1.902l1.696,0l0,-1.902l-1.696,0Z" style="fill:#737373;"/><defs><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.99995,-3,3,-2.99995,23.9974,3.00265)"><stop offset="0" style="stop-color:#66aefd;stop-opacity:1"/><stop offset="1" style="stop-color:#ffd700;stop-opacity:1"/></linearGradient><linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3,-2.99995,2.99995,3,20.9987,24.0027)"><stop offset="0" style="stop-color:#ff8a00;stop-opacity:1"/><stop offset="1" style="stop-color:#66aefd;stop-opacity:1"/></linearGradient><linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.99995,3,-3,2.99995,-0.00255928,21.0013)"><stop offset="0" style="stop-color:#a5c700;stop-opacity:1"/><stop offset="1" style="stop-color:#ff8a00;stop-opacity:1"/></linearGradient><linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3,2.99995,-2.99995,-3,2.99744,0.00265252)"><stop offset="0" style="stop-color:#ffd700;stop-opacity:1"/><stop offset="1" style="stop-color:#a5c700;stop-opacity:1"/></linearGradient></defs></svg>
|
||||
<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>
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 5.4 KiB |
@ -22,6 +22,8 @@ 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'],
|
||||
@ -49,11 +51,49 @@ export default {
|
||||
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) {
|
||||
|
@ -7,11 +7,12 @@ import Prism from 'prismjs';
|
||||
import cledit from '../services/editor/cledit';
|
||||
|
||||
export default {
|
||||
props: ['value', 'lang', 'disabled'],
|
||||
props: ['value', 'lang', 'disabled', 'scrollClass'],
|
||||
mounted() {
|
||||
const preElt = this.$el;
|
||||
let scrollElt = preElt;
|
||||
while (scrollElt && !scrollElt.classList.contains('modal')) {
|
||||
const scrollCls = this.scrollClass || 'modal';
|
||||
while (scrollElt && !scrollElt.classList.contains(scrollCls)) {
|
||||
scrollElt = scrollElt.parentNode;
|
||||
}
|
||||
if (scrollElt) {
|
||||
@ -43,6 +44,10 @@ 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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<div class="editor" ondrop="return false;">
|
||||
<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>
|
||||
@ -13,6 +13,9 @@ 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: {
|
||||
@ -30,7 +33,51 @@ 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;
|
||||
@ -56,6 +103,15 @@ export default {
|
||||
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) => {
|
||||
|
197
src/components/EditorInPageButtons.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<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>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="explorer flex flex--column">
|
||||
<div class="side-title flex flex--row flex--space-between">
|
||||
<div class="flex flex--row">
|
||||
<div class="flex flex--row" v-if="!showSearch">
|
||||
<button class="side-title__button side-title__button--new-file button" @click="newItem()" v-title="'创建文件'">
|
||||
<icon-file-plus></icon-file-plus>
|
||||
</button>
|
||||
@ -14,14 +14,36 @@
|
||||
<button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'重命名'">
|
||||
<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>
|
||||
</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="'关闭资源管理器'">
|
||||
<icon-close></icon-close>
|
||||
</button>
|
||||
</div>
|
||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0" @keydown.delete="deleteItem()">
|
||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" v-show="!showSearch" tabindex="0" @keydown.delete="deleteItem()">
|
||||
<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>
|
||||
|
||||
@ -30,11 +52,22 @@ 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',
|
||||
@ -46,6 +79,7 @@ export default {
|
||||
'rootNode',
|
||||
'selectedNode',
|
||||
]),
|
||||
workspaceId: () => store.getters['workspace/currentWorkspace'].id,
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
@ -59,11 +93,69 @@ export default {
|
||||
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');
|
||||
});
|
||||
},
|
||||
clickSearch(item) {
|
||||
const node = store.getters['explorer/nodeMap'][item.id];
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
(currentFileId) => {
|
||||
this.currentFileId = currentFileId;
|
||||
store.commit('explorer/setSelectedId', currentFileId);
|
||||
store.dispatch('explorer/openNode', currentFileId);
|
||||
}, {
|
||||
@ -75,6 +167,8 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.explorer,
|
||||
.explorer__tree {
|
||||
height: 100%;
|
||||
@ -89,4 +183,43 @@ 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>
|
||||
|
@ -14,6 +14,7 @@
|
||||
</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>
|
||||
|
||||
@ -23,6 +24,7 @@ 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
|
||||
@ -80,6 +82,9 @@ export default {
|
||||
...mapActions('explorer', [
|
||||
'setDragTarget',
|
||||
]),
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
select(id = this.node.item.id, doOpen = true) {
|
||||
const node = store.getters['explorer/nodeMap'][id];
|
||||
if (!node) {
|
||||
@ -144,6 +149,11 @@ export default {
|
||||
// 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`;
|
||||
},
|
||||
onDrop() {
|
||||
const sourceNode = store.getters['explorer/dragSourceNode'];
|
||||
const targetNode = store.getters['explorer/dragTargetNodeFolder'];
|
||||
@ -169,22 +179,26 @@ export default {
|
||||
top: evt.clientY,
|
||||
},
|
||||
items: [{
|
||||
name: 'New file',
|
||||
name: '新建文件',
|
||||
disabled: !this.node.isFolder || this.node.isTrash,
|
||||
perform: () => explorerSvc.newItem(false),
|
||||
}, {
|
||||
name: 'New folder',
|
||||
name: '新建文件夹',
|
||||
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
|
||||
perform: () => explorerSvc.newItem(true),
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
name: 'Rename',
|
||||
name: '重命名',
|
||||
disabled: this.node.isTrash || this.node.isTemp,
|
||||
perform: () => this.setEditingId(this.node.item.id),
|
||||
}, {
|
||||
name: 'Delete',
|
||||
name: '删除',
|
||||
perform: () => explorerSvc.deleteItem(),
|
||||
}, {
|
||||
name: '复制路径',
|
||||
disabled: this.node.isTrash || this.node.isTemp,
|
||||
perform: () => this.$refs.copyId.click(),
|
||||
}],
|
||||
});
|
||||
if (item) {
|
||||
@ -215,6 +229,10 @@ $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;
|
||||
@ -236,6 +254,10 @@ $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,
|
||||
|
@ -14,8 +14,8 @@
|
||||
<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')">Previous</button>
|
||||
<button class="find-replace__button button" @click="find('forward')">Next</button>
|
||||
<button class="find-replace__button button" @click="find('backward')">上一个</button>
|
||||
<button class="find-replace__button button" @click="find('forward')">下一个</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,8 +24,8 @@
|
||||
<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">Replace</button>
|
||||
<button class="find-replace__button button" @click="replaceAll">All</button>
|
||||
<button class="find-replace__button button" @click="replace">替换</button>
|
||||
<button class="find-replace__button button" @click="replaceAll">全部替换</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -307,20 +307,36 @@ export default {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,10 +359,18 @@ export default {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,6 +383,10 @@ export default {
|
||||
.find-replace-highlighting {
|
||||
background-color: $highlighting-color;
|
||||
color: $editor-color-light !important;
|
||||
|
||||
.app--dark & {
|
||||
background-color: $dark-highlighting-color;
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace-selection {
|
||||
|
@ -9,11 +9,12 @@
|
||||
<navigation-bar></navigation-bar>
|
||||
</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 + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + '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>
|
||||
<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>
|
||||
@ -27,6 +28,7 @@
|
||||
<div class="gutter__background" v-if="styles.previewGutterWidth" :style="{width: styles.previewGutterWidth + 'px'}"></div>
|
||||
</div>
|
||||
<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>
|
||||
@ -58,6 +60,8 @@ 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';
|
||||
@ -75,6 +79,8 @@ export default {
|
||||
Editor,
|
||||
Preview,
|
||||
Tour,
|
||||
EditorInPageButtons,
|
||||
PreviewInPageButtons,
|
||||
StickyComment,
|
||||
CurrentDiscussion,
|
||||
FindReplace,
|
||||
@ -96,9 +102,18 @@ export default {
|
||||
...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', [
|
||||
@ -185,7 +200,7 @@ export default {
|
||||
}
|
||||
|
||||
$preview-background-light: #f3f3f3;
|
||||
$preview-background-dark: #252525;
|
||||
$preview-background-dark: #444;
|
||||
|
||||
.layout__panel--preview,
|
||||
.layout__panel--button-bar {
|
||||
@ -208,6 +223,10 @@ $preview-background-dark: #252525;
|
||||
.layout__panel--explorer,
|
||||
.layout__panel--side-bar {
|
||||
background-color: #ddd;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #383c4a;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__panel--find-replace {
|
||||
@ -218,5 +237,9 @@ $preview-background-dark: #252525;
|
||||
width: 300px;
|
||||
height: auto;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #4d5160;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
|
||||
<button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
|
||||
<button v-for="(item, idx) in (simpleModal.resolveArray || [])" class="button button--resolve" @click="config.resolve(item.value)">{{item.text}}</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</div>
|
||||
@ -39,6 +40,9 @@ 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';
|
||||
@ -54,6 +58,7 @@ 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';
|
||||
@ -61,6 +66,8 @@ 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';
|
||||
@ -71,6 +78,7 @@ 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';
|
||||
@ -78,6 +86,8 @@ 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
|
||||
@ -101,6 +111,9 @@ export default {
|
||||
AccountManagementModal,
|
||||
BadgeManagementModal,
|
||||
SponsorModal,
|
||||
CommitMessageModal,
|
||||
WorkspaceImgPathModal,
|
||||
ChatGptModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
GoogleDriveAccountModal,
|
||||
@ -115,6 +128,7 @@ export default {
|
||||
GithubSaveModal,
|
||||
GithubWorkspaceModal,
|
||||
GithubPublishModal,
|
||||
GithubImgStorageModal,
|
||||
GistSyncModal,
|
||||
GistPublishModal,
|
||||
GiteeAccountModal,
|
||||
@ -122,6 +136,8 @@ export default {
|
||||
GiteeSaveModal,
|
||||
GiteeWorkspaceModal,
|
||||
GiteePublishModal,
|
||||
GiteeGistSyncModal,
|
||||
GiteeGistPublishModal,
|
||||
GitlabAccountModal,
|
||||
GitlabOpenModal,
|
||||
GitlabPublishModal,
|
||||
@ -132,6 +148,7 @@ export default {
|
||||
GiteaPublishModal,
|
||||
GiteaSaveModal,
|
||||
GiteaWorkspaceModal,
|
||||
GiteaImgStorageModal,
|
||||
WordpressPublishModal,
|
||||
BloggerPublishModal,
|
||||
BloggerPagePublishModal,
|
||||
@ -139,6 +156,8 @@ export default {
|
||||
ZendeskPublishModal,
|
||||
CouchdbWorkspaceModal,
|
||||
CouchdbCredentialsModal,
|
||||
SmmsAccountModal,
|
||||
CustomAccountModal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
@ -169,6 +188,7 @@ export default {
|
||||
// User has to sign in
|
||||
await store.dispatch('modal/open', 'signInForSponsorship');
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
}
|
||||
if (!store.getters.isSponsor) {
|
||||
@ -265,6 +285,10 @@ export default {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #383c4a;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
@ -7,6 +7,7 @@
|
||||
</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>
|
||||
</div>
|
||||
@ -113,7 +114,8 @@ export default {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
pagedownButtons() {
|
||||
return pagedownButtons.map(button => ({
|
||||
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}`,
|
||||
@ -187,6 +189,9 @@ export default {
|
||||
publishSvc.requestPublish();
|
||||
}
|
||||
},
|
||||
switchTheme() {
|
||||
store.dispatch('data/switchThemeSetting');
|
||||
},
|
||||
pagedownClick(name) {
|
||||
if (store.getters['content/isCurrentEditable']) {
|
||||
const text = editorSvc.clEditor.getContent();
|
||||
@ -309,6 +314,18 @@ export default {
|
||||
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;
|
||||
|
||||
|
@ -10,10 +10,10 @@
|
||||
{{item.content}}
|
||||
</div>
|
||||
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.reject">
|
||||
No
|
||||
否
|
||||
</button>
|
||||
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.resolve">
|
||||
Yes
|
||||
是
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="preview">
|
||||
<div class="preview__inner-1" @click="onClick" @scroll="onScroll">
|
||||
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
|
||||
<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>
|
||||
@ -37,9 +37,15 @@ export default {
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('theme', [
|
||||
'currPreviewTheme',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
previewTheme() {
|
||||
return `preview-theme--${this.currPreviewTheme || 'default'}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
|
212
src/components/PreviewInPageButtons.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<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>
|
@ -23,6 +23,8 @@
|
||||
<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>
|
||||
@ -41,6 +43,8 @@ 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 markdownSample from '../data/markdownSample.md';
|
||||
import markdownConversionSvc from '../services/markdownConversionSvc';
|
||||
import store from '../store';
|
||||
@ -55,6 +59,8 @@ const panelNames = {
|
||||
history: '文件历史',
|
||||
importExport: '导入/导出',
|
||||
workspaceBackups: '文档空间备份',
|
||||
editTheme: '编辑区主题',
|
||||
previewTheme: '预览区主题',
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -67,6 +73,8 @@ export default {
|
||||
HistoryMenu,
|
||||
ImportExportMenu,
|
||||
WorkspaceBackupMenu,
|
||||
EditThemeMenu,
|
||||
PreviewThemeMenu,
|
||||
},
|
||||
data: () => ({
|
||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||
|
@ -99,6 +99,10 @@ export default {
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
}
|
||||
|
||||
* {
|
||||
font-weight: inherit;
|
||||
pointer-events: none;
|
||||
@ -150,5 +154,9 @@ export default {
|
||||
height: 35px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
pointer-events: none;
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'editor'">
|
||||
<h2>您的Markdown编辑器</h2>
|
||||
<p>StackEdit实时将Markdown转换为HTML。</p>
|
||||
<p>StackEdit中文版实时将Markdown转换为HTML。</p>
|
||||
<p>点击 <icon-side-preview></icon-side-preview> 切换侧边预览</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">跳过</button>
|
||||
@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'explorer'">
|
||||
<h2>文件资源管理器</h2>
|
||||
<p>StackEdit可以管理文档空间中的多个文件和文件夹。</p>
|
||||
<p>StackEdit中文版可以管理文档空间中的多个文件和文件夹。</p>
|
||||
<p>点击 <icon-folder></icon-folder> 打开文件资源管理器。</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">跳过</button>
|
||||
@ -29,17 +29,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'menu'">
|
||||
<h2>更多!</h2>
|
||||
<p>StackEdit还可以同步和发布文件,管理协作文档空间...</p>
|
||||
<h2>切换侧边栏!</h2>
|
||||
<p>StackEdit中文版还可以同步和发布文件,管理协作文档空间...</p>
|
||||
<p>点击 <icon-provider provider-id="stackedit"></icon-provider> 浏览菜单。</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">跳过</button>
|
||||
<button class="button button--resolve" @click="next">下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'theme'">
|
||||
<h2>切换主题!</h2>
|
||||
<p>StackEdit中文版可以切换亮/暗主题。</p>
|
||||
<p>点击 <icon-switch-theme></icon-switch-theme> 切换主题。</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button" @click="finish">跳过</button>
|
||||
<button class="button button--resolve" @click="next">下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-step__inner" v-else-if="step === 'end'">
|
||||
<h2>Enjoy!</h2>
|
||||
<p>如果您喜欢StackEdit,请在<a href="#">Chrome网络商店</a>上给5颗星打分。</p>
|
||||
<p>如果您喜欢StackEdit中文版,请在<a href="https://gitee.com/mafgwo/stackedit">Gitee仓库</a>上点一下Star,谢谢!</p>
|
||||
<div class="tour-step__button-bar">
|
||||
<button class="button button--resolve" @click="finish">确认</button>
|
||||
</div>
|
||||
@ -57,6 +66,7 @@ const steps = [
|
||||
'editor',
|
||||
'explorer',
|
||||
'menu',
|
||||
'theme',
|
||||
'end',
|
||||
];
|
||||
|
||||
@ -90,7 +100,8 @@ export default {
|
||||
break;
|
||||
}
|
||||
case 'editor':
|
||||
case 'menu': {
|
||||
case 'menu':
|
||||
case 'theme': {
|
||||
style.left = `${anchorRect.left}px`;
|
||||
break;
|
||||
}
|
||||
@ -139,6 +150,7 @@ export default {
|
||||
}
|
||||
|
||||
$tour-step-background: transparentize(mix(#f3f3f3, $selection-highlighting-color, 75%), 0.025);
|
||||
$tour-step-darkbackground: transparentize(mix(#4d4d4d, $selection-highlighting-color, 75%), 0.025);
|
||||
$tour-step-width: 240px;
|
||||
|
||||
.tour-step__inner {
|
||||
@ -151,6 +163,10 @@ $tour-step-width: 240px;
|
||||
text-align: center;
|
||||
border-radius: $border-radius-base;
|
||||
|
||||
.app--dark & {
|
||||
background-color: $tour-step-darkbackground;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
|
||||
@ -183,11 +199,16 @@ $tour-step-width: 240px;
|
||||
right: 0;
|
||||
border-top: 10px solid $tour-step-background;
|
||||
border-left: 10px solid transparent;
|
||||
|
||||
.app--dark & {
|
||||
border-top: 10px solid $tour-step-darkbackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tour-step--editor &,
|
||||
.tour-step--menu & {
|
||||
.tour-step--menu &,
|
||||
.tour-step--theme & {
|
||||
right: 15px;
|
||||
border-top-right-radius: 0;
|
||||
|
||||
@ -196,6 +217,10 @@ $tour-step-width: 240px;
|
||||
right: -10px;
|
||||
border-top: 10px solid $tour-step-background;
|
||||
border-right: 10px solid transparent;
|
||||
|
||||
.app--dark & {
|
||||
border-top: 10px solid $tour-step-darkbackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,6 +233,10 @@ $tour-step-width: 240px;
|
||||
left: -10px;
|
||||
border-top: 10px solid $tour-step-background;
|
||||
border-left: 10px solid transparent;
|
||||
|
||||
.app--dark & {
|
||||
border-top: 10px solid $tour-step-darkbackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
137
src/components/common/DropdownMenu.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<span class="dropdown-menu">
|
||||
<span ref="slotInfo" @click="toggleMenu()" class="dropdown-toggle">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<ul class="dropdown-menu-items" :style="dropdownStyle" v-if="showMenu">
|
||||
<li v-for="(option, idx) in options" :key="idx">
|
||||
<a href="javascript:void(0)" :class="{selected: option.value === selectedOption.value}" @click="updateOption(option)">
|
||||
{{ option.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '../../store';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
selectedOption: {
|
||||
value: '',
|
||||
name: '',
|
||||
},
|
||||
showMenu: false,
|
||||
}),
|
||||
props: {
|
||||
options: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
selected: {},
|
||||
closeOnOutsideClick: {
|
||||
type: [Boolean],
|
||||
default: true,
|
||||
},
|
||||
closeOnItemClick: {
|
||||
type: [Boolean],
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedOption = this.selected;
|
||||
if (this.closeOnOutsideClick) {
|
||||
document.addEventListener('click', this.clickHandler);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.clickHandler);
|
||||
},
|
||||
computed: {
|
||||
dropdownStyle() {
|
||||
const height = store.state.layout.bodyHeight;
|
||||
return `max-height: ${height * 0.7}px;`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateOption(option) {
|
||||
this.selectedOption = option;
|
||||
if (this.closeOnItemClick) {
|
||||
this.showMenu = false;
|
||||
}
|
||||
this.$emit('change', option);
|
||||
},
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
},
|
||||
clickHandler(event) {
|
||||
const { target } = event;
|
||||
const { $el } = this;
|
||||
if (!$el.contains(target)) {
|
||||
this.showMenu = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected(val) {
|
||||
this.selectedOption = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-menu {
|
||||
.dropdown-menu-items {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
max-height: 450px;
|
||||
overflow-y: scroll;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
font-size: 15px;
|
||||
background-color: #666;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgb(0, 0, 0 / 18%);
|
||||
box-shadow: 0 6px 12px rgb(0, 0, 0 / 18%);
|
||||
background-clip: padding-box;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
display: list-item;
|
||||
margin: 0;
|
||||
text-align: -webkit-match-parent;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.45;
|
||||
white-space: nowrap;
|
||||
color: #eee;
|
||||
padding: 5px 20px;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: rgb(82, 82, 82);
|
||||
}
|
||||
}
|
||||
|
||||
a.selected {
|
||||
background: #74b936 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,7 +16,7 @@
|
||||
<div class="comment__text-inner" v-html="text"></div>
|
||||
</div>
|
||||
<div class="comment__buttons flex flex--row flex--end" v-if="showReply">
|
||||
<button class="comment__button button" @click="setIsCommenting(true)">Reply</button>
|
||||
<button class="comment__button button" @click="setIsCommenting(true)">评论</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -4,18 +4,18 @@
|
||||
<div class="current-discussion__inner">
|
||||
<div class="flex flex--row flex--space-between">
|
||||
<div class="current-discussion__buttons flex flex--row flex--end">
|
||||
<button class="current-discussion__button button" v-if="showNext" @click="goToDiscussion(previousDiscussionId)" v-title="'上一个讨论'">
|
||||
<button class="current-discussion__button button" v-if="showNext" @click="goToDiscussion(previousDiscussionId)" v-title="'上一个批注'">
|
||||
<icon-arrow-left></icon-arrow-left>
|
||||
</button>
|
||||
<button class="current-discussion__button current-discussion__button--rotate button" v-if="showNext" @click="goToDiscussion(nextDiscussionId)" v-title="'下一个讨论'">
|
||||
<button class="current-discussion__button current-discussion__button--rotate button" v-if="showNext" @click="goToDiscussion(nextDiscussionId)" v-title="'下一个批注'">
|
||||
<icon-arrow-left></icon-arrow-left>
|
||||
</button>
|
||||
</div>
|
||||
<div class="current-discussion__buttons flex flex--row flex--end">
|
||||
<button class="current-discussion__button current-discussion__button--remove button" v-if="showRemove" @click="removeDiscussion" v-title="'删除讨论'">
|
||||
<button class="current-discussion__button current-discussion__button--remove button" v-if="showRemove" @click="removeDiscussion" v-title="'删除批注'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
<button class="current-discussion__button button" @click="setCurrentDiscussionId()" v-title="'关闭讨论'">
|
||||
<button class="current-discussion__button button" @click="setCurrentDiscussionId()" v-title="'关闭批注'">
|
||||
<icon-close></icon-close>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'开始讨论'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'开始批注'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||
<icon-message></icon-message>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'开始讨论'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||
<a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'添加批注'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
|
||||
<icon-message></icon-message>
|
||||
</a>
|
||||
</template>
|
||||
|
116
src/components/menus/EditThemeMenu.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="edit-theme side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info">
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||
<span v-if="currEditTheme==='custom'">
|
||||
下面的自定义主题样式可编辑,可参考其他主题样式填入自己喜欢的编辑样式。<br>
|
||||
主题class为:edit-theme--custom
|
||||
</span>
|
||||
<span v-else>
|
||||
下面的主题样式不可编辑。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-bar__content">
|
||||
<template v-if="currEditTheme === 'default'">
|
||||
默认主题无额外样式,请选择其他主题。
|
||||
</template>
|
||||
<template v-else>
|
||||
<code-editor v-for="(value, index) in styleEles" :key="index"
|
||||
v-if="value.id === `edit-theme-${currEditTheme}`" lang="css" :value="value.innerHTML"
|
||||
:disabled="value.id!=='edit-theme-custom'" @changed="changeText" scrollClass="side-bar__inner"></code-editor>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex flex--row flex--end" v-if="currEditTheme==='custom'">
|
||||
<button class="edit-theme__button button" @click="saveStyleText">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import store from '../../store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
themeStyleText: '',
|
||||
styleEles: [],
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('theme', [
|
||||
'currEditTheme',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
saveStyleText() {
|
||||
const typeEle = this.findByTheme(this.currEditTheme);
|
||||
if (!typeEle || !this.themeStyleText) {
|
||||
return;
|
||||
}
|
||||
typeEle.innerHTML = this.themeStyleText;
|
||||
store.dispatch('theme/setCustomEditThemeStyle', this.themeStyleText);
|
||||
store.dispatch('notification/info', '保存自定义主题样式成功!');
|
||||
},
|
||||
findByTheme(theme) {
|
||||
const findEles = this.styleEles.filter(it => it.id === `edit-theme-${theme}`);
|
||||
return findEles.length ? findEles[0] : null;
|
||||
},
|
||||
changeText(text) {
|
||||
this.themeStyleText = text;
|
||||
},
|
||||
close() {
|
||||
store.dispatch('data/setSideBarPanel', 'menu');
|
||||
},
|
||||
initStyle(theme) {
|
||||
if (theme === 'default') {
|
||||
return;
|
||||
}
|
||||
const value = theme || this.currEditTheme;
|
||||
if (this.findByTheme(value)) {
|
||||
return;
|
||||
}
|
||||
const styleId = `edit-theme-${value}`;
|
||||
const styleEle = document.getElementById(styleId);
|
||||
if (!styleEle) {
|
||||
setTimeout(() => this.initStyle(value), 1000);
|
||||
return;
|
||||
}
|
||||
this.styleEles.push(styleEle);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currEditTheme: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.initStyle(val);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initStyle();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.side-bar__panel--menu {
|
||||
.side-bar__content {
|
||||
.code-editor {
|
||||
min-height: 400px !important;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-theme__button {
|
||||
font-size: 14px;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</p>
|
||||
<p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Gitee</a> 以同步您的主文档空间。</p>
|
||||
<p v-if="!historyContext">同步 <b>{{currentFileName}}</b> 以启用修订历史 或者 <a href="javascript:void(0)" @click="signin">登录 Gitee</a> 或 <a href="javascript:void(0)" @click="signinWithGithub">登录 GitHub</a> 以同步您的主文档空间。</p>
|
||||
<p v-else-if="loading">历史版本加载中…</p>
|
||||
<p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> 没有历史版本.</p>
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
|
||||
@ -33,6 +33,7 @@
|
||||
<div class="revision__header flex flex--column">
|
||||
<user-name :user-id="revision.sub"></user-name>
|
||||
<div class="revision__created">{{revision.created | formatTime}}</div>
|
||||
<div class="revision__msg">{{revision.message}}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@ -54,6 +55,7 @@ import EditorClassApplier from '../common/EditorClassApplier';
|
||||
import PreviewClassApplier from '../common/PreviewClassApplier';
|
||||
import utils from '../../services/utils';
|
||||
import giteeHelper from '../../services/providers/helpers/giteeHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
@ -167,6 +169,16 @@ export default {
|
||||
async signin() {
|
||||
try {
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
async signinWithGithub() {
|
||||
try {
|
||||
await githubHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
@ -412,6 +424,14 @@ export default {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.revision__msg {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.layout--revision {
|
||||
.cledit-section *,
|
||||
.cl-preview-section * {
|
||||
|
@ -14,6 +14,9 @@
|
||||
<span v-if="currentWorkspace.providerId === 'giteeAppData'">
|
||||
<b>{{currentWorkspace.name}}</b> 与您的 Gitee 默认文档空间仓库同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'githubAppData'">
|
||||
<b>{{currentWorkspace.name}}</b> 与您的 GitHub 默认文档空间仓库同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">Google Drive 文件夹</a>同步。
|
||||
</span>
|
||||
@ -21,10 +24,10 @@
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">CouchDB 数据库</a>同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a> 同步。
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">GitHub 仓库</a> 同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'giteeWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">Gitee repo</a> 同步。
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">Gitee 仓库</a> 同步。
|
||||
</span>
|
||||
<span v-else-if="currentWorkspace.providerId === 'gitlabWorkspace'">
|
||||
<b>{{currentWorkspace.name}}</b> 与 <a :href="workspaceLocationUrl" target="_blank">GitLab 项目</a>同步。
|
||||
@ -45,6 +48,11 @@
|
||||
<div>使用 Gitee 登录</div>
|
||||
<span>同步您的主文档空间并解锁功能。</span>
|
||||
</menu-entry>
|
||||
<menu-entry v-if="!loginToken" @click.native="signinWithGithub">
|
||||
<icon-login slot="icon"></icon-login>
|
||||
<div>使用 GitHub 登录</div>
|
||||
<span>同步您的主文档空间并解锁功能。</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('workspaces')">
|
||||
<icon-database slot="icon"></icon-database>
|
||||
<div><div class="menu-entry__label menu-entry__label--count" v-if="workspaceCount">{{workspaceCount}}</div> 文档空间</div>
|
||||
@ -105,6 +113,16 @@
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> 模板</div>
|
||||
<span>为您的导出配置 Handlebars 模板。</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('editTheme')">
|
||||
<icon-select-theme slot="icon"></icon-select-theme>
|
||||
编辑区主题
|
||||
<span>编辑区主题样式(自定义主题可编辑)。</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('previewTheme')">
|
||||
<icon-select-theme slot="icon"></icon-select-theme>
|
||||
预览区主题
|
||||
<span>预览区主题样式(自定义主题可编辑)。</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="settings">
|
||||
<icon-settings slot="icon"></icon-settings>
|
||||
<div>配置</div>
|
||||
@ -132,6 +150,7 @@ import MenuEntry from './common/MenuEntry';
|
||||
import providerRegistry from '../../services/providers/common/providerRegistry';
|
||||
import UserImage from '../UserImage';
|
||||
import giteeHelper from '../../services/providers/helpers/giteeHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import userSvc from '../../services/userSvc';
|
||||
import store from '../../store';
|
||||
@ -184,6 +203,16 @@ export default {
|
||||
async signin() {
|
||||
try {
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
async signinWithGithub() {
|
||||
try {
|
||||
await githubHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
|
116
src/components/menus/PreviewThemeMenu.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="preview-theme side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info">
|
||||
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||
<span v-if="currPreviewTheme==='custom'">
|
||||
下面的自定义主题样式可编辑,可参考其他主题样式填入自己喜欢的预览样式。<br>
|
||||
主题class为:preview-theme--custom
|
||||
</span>
|
||||
<span v-else>
|
||||
下面的主题样式不可编辑。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-bar__content">
|
||||
<template v-if="currPreviewTheme === 'default'">
|
||||
默认主题无额外样式,请选择其他主题。
|
||||
</template>
|
||||
<template v-else>
|
||||
<code-editor v-for="(value, index) in styleEles" :key="index"
|
||||
v-if="value.id === `preview-theme-${currPreviewTheme}`" lang="css" :value="value.innerHTML"
|
||||
:disabled="value.id!=='preview-theme-custom'" @changed="changeText" scrollClass="side-bar__inner"></code-editor>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex flex--row flex--end" v-if="currPreviewTheme==='custom'">
|
||||
<button class="preview-theme__button button" @click="saveStyleText">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import store from '../../store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
themeStyleText: '',
|
||||
styleEles: [],
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('theme', [
|
||||
'currPreviewTheme',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
saveStyleText() {
|
||||
const typeEle = this.findByTheme(this.currPreviewTheme);
|
||||
if (!typeEle || !this.themeStyleText) {
|
||||
return;
|
||||
}
|
||||
typeEle.innerHTML = this.themeStyleText;
|
||||
store.dispatch('theme/setCustomPreviewThemeStyle', this.themeStyleText);
|
||||
store.dispatch('notification/info', '保存自定义主题样式成功!');
|
||||
},
|
||||
findByTheme(theme) {
|
||||
const findEles = this.styleEles.filter(it => it.id === `preview-theme-${theme}`);
|
||||
return findEles.length ? findEles[0] : null;
|
||||
},
|
||||
changeText(text) {
|
||||
this.themeStyleText = text;
|
||||
},
|
||||
close() {
|
||||
store.dispatch('data/setSideBarPanel', 'menu');
|
||||
},
|
||||
initStyle(theme) {
|
||||
if (theme === 'default') {
|
||||
return;
|
||||
}
|
||||
const value = theme || this.currPreviewTheme;
|
||||
if (this.findByTheme(value)) {
|
||||
return;
|
||||
}
|
||||
const styleId = `preview-theme-${value}`;
|
||||
const styleEle = document.getElementById(styleId);
|
||||
if (!styleEle) {
|
||||
setTimeout(() => this.initStyle(value), 1000);
|
||||
return;
|
||||
}
|
||||
this.styleEles.push(styleEle);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currPreviewTheme: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.initStyle(val);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initStyle();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.side-bar__panel--menu {
|
||||
.side-bar__content {
|
||||
.code-editor {
|
||||
min-height: 400px !important;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-theme__button {
|
||||
font-size: 14px;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -43,7 +43,7 @@
|
||||
<div v-for="token in githubTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>发布到 Gist</div>
|
||||
<div>发布到 GitHubGist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishGithub(token)">
|
||||
@ -53,6 +53,11 @@
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in giteeTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGiteeGist(token)">
|
||||
<icon-provider slot="icon" provider-id="giteegist"></icon-provider>
|
||||
<div>发布到 GiteeGist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishGitee(token)">
|
||||
<icon-provider slot="icon" provider-id="gitee"></icon-provider>
|
||||
<div>发布到 Gitee</div>
|
||||
@ -258,14 +263,14 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
const applicationInfo = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(applicationInfo);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
@ -289,8 +294,9 @@ export default {
|
||||
publishBloggerPage: publishModalOpener('bloggerPagePublish', 'publishToBloggerPage'),
|
||||
publishDropbox: publishModalOpener('dropboxPublish', 'publishToDropbox'),
|
||||
publishGithub: publishModalOpener('githubPublish', 'publishToGithub'),
|
||||
publishGitee: publishModalOpener('giteePublish', 'publishToGitee'),
|
||||
publishGist: publishModalOpener('gistPublish', 'publishToGist'),
|
||||
publishGitee: publishModalOpener('giteePublish', 'publishToGitee'),
|
||||
publishGiteeGist: publishModalOpener('giteeGistPublish', 'publishGiteeGist'),
|
||||
publishGitlab: publishModalOpener('gitlabPublish', 'publishToGitlab'),
|
||||
publishGitea: publishModalOpener('giteaPublish', 'publishToGitea'),
|
||||
publishGoogleDrive: publishModalOpener('googleDrivePublish', 'publishToGoogleDrive'),
|
||||
|
@ -46,7 +46,7 @@
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>在Gist上保存</div>
|
||||
<div>在GitHubGist上保存</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
@ -61,6 +61,11 @@
|
||||
<div>在Gitee上保存</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGiteeGist(token)">
|
||||
<icon-provider slot="icon" provider-id="giteegist"></icon-provider>
|
||||
<div>在GiteeGist上保存</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in gitlabTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGitlab(token)">
|
||||
@ -234,14 +239,14 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
const applicationInfo = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(applicationInfo);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
@ -330,6 +335,12 @@ export default {
|
||||
badgeSvc.addBadge('saveOnGist');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGiteeGist(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'giteeGistSync');
|
||||
badgeSvc.addBadge('saveOnGiteeGist');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGitlab(token) {
|
||||
try {
|
||||
const syncLocation = await store.dispatch('modal/open', {
|
||||
|
@ -8,7 +8,8 @@
|
||||
<hr>
|
||||
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
|
||||
<menu-entry :href="workspace.url" target="_blank">
|
||||
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||
<icon-provider v-if="id === 'main' && !workspace.sub" slot="icon" :provider-id="'stackedit'"></icon-provider>
|
||||
<icon-provider v-else slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">当前</div>{{workspace.name}}</div>
|
||||
</menu-entry>
|
||||
</div>
|
||||
@ -85,8 +86,8 @@ export default {
|
||||
},
|
||||
async addGitlabWorkspace() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
const token = await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
const token = await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
store.dispatch('modal/open', {
|
||||
type: 'gitlabWorkspace',
|
||||
token,
|
||||
@ -95,8 +96,8 @@ export default {
|
||||
},
|
||||
async addGiteaWorkspace() {
|
||||
try {
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
const token = await giteaHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
const applicationInfo = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
const token = await giteaHelper.addAccount(applicationInfo);
|
||||
store.dispatch('modal/open', {
|
||||
type: 'giteaWorkspace',
|
||||
token,
|
||||
|
@ -73,6 +73,10 @@
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-entry__label--warning {
|
||||
|
93
src/components/menus/common/MenuItem.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="menu-item flex flex--row flex--align-center">
|
||||
<div class="menu-item__icon flex flex--column flex--center">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div class="menu-item__text flex flex--column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.menu-item {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
height: auto;
|
||||
font-size: 17px;
|
||||
line-height: 1.4;
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.67;
|
||||
line-height: 1.3;
|
||||
|
||||
.menu-item__label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item--info {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.menu-item__icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: 12px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.menu-item__icon--disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menu-item__icon--image {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hidden-file {
|
||||
position: fixed;
|
||||
top: -999px;
|
||||
}
|
||||
|
||||
.menu-item__label {
|
||||
float: right;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 0.15em 0.25em;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.menu-item__label--warning {
|
||||
color: #fff;
|
||||
background-color: darken($error-color, 10);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item__label--count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.menu-item__text {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -4,8 +4,8 @@
|
||||
<div class="modal__image">
|
||||
<icon-key></icon-key>
|
||||
</div>
|
||||
<p v-if="entries.length">Stackedit可以访问以下外部账号:</p>
|
||||
<p v-else>Stackedit尚未访问任何外部账号。</p>
|
||||
<p v-if="entries.length">StackEdit中文版可以访问以下外部账号:</p>
|
||||
<p v-else>StackEdit中文版尚未访问任何外部账号。</p>
|
||||
<div>
|
||||
<div class="account-entry flex flex--column" v-for="entry in entries" :key="entry.token.sub">
|
||||
<div class="account-entry__header flex flex--row flex--align-center">
|
||||
@ -30,6 +30,14 @@
|
||||
<b>URL:</b>
|
||||
{{entry.url}}
|
||||
</span>
|
||||
<span class="account-entry__field line-entry" v-if="entry.customHeaders">
|
||||
<b>自定义请求头:</b>
|
||||
{{entry.customHeaders}}
|
||||
</span>
|
||||
<span class="account-entry__field line-entry" v-if="entry.customParams">
|
||||
<b>自定义Form参数:</b>
|
||||
{{entry.customParams}}
|
||||
</span>
|
||||
<span class="account-entry__field" v-if="entry.scopes">
|
||||
<b>权限范围:</b>
|
||||
{{entry.scopes.join(', ')}}
|
||||
@ -77,6 +85,14 @@
|
||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||
<span>添加Zendesk账号</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addSmmsAccount">
|
||||
<icon-provider slot="icon" provider-id="smms"></icon-provider>
|
||||
<span>添加SM.MS账号</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addCustomAccount">
|
||||
<icon-provider slot="icon" provider-id="custom"></icon-provider>
|
||||
<span>添加自定义图床账号</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--resolve" @click="config.resolve()">关闭</button>
|
||||
@ -98,6 +114,8 @@ import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
|
||||
import giteaHelper from '../../services/providers/helpers/giteaHelper';
|
||||
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
||||
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||
import smmsHelper from '../../services/providers/helpers/smmsHelper';
|
||||
import customHelper from '../../services/providers/helpers/customHelper';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
@ -176,6 +194,23 @@ export default {
|
||||
name: token.name,
|
||||
scopes: ['read', 'hc:write'],
|
||||
})),
|
||||
...Object.values(store.getters['data/smmsTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'smms',
|
||||
userId: token.sub,
|
||||
name: token.name,
|
||||
scopes: ['api'],
|
||||
})),
|
||||
...Object.values(store.getters['data/customTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'custom',
|
||||
url: token.uploadUrl,
|
||||
userId: token.name,
|
||||
name: token.name,
|
||||
customHeaders: token.customHeaders && JSON.stringify(token.customHeaders),
|
||||
customParams: token.customParams && JSON.stringify(token.customParams),
|
||||
scopes: ['upload'],
|
||||
})),
|
||||
];
|
||||
},
|
||||
},
|
||||
@ -213,14 +248,14 @@ export default {
|
||||
},
|
||||
async addGitlabAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||
await gitlabHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGiteaAccount() {
|
||||
try {
|
||||
const { serverUrl, applicationId, applicationSecret } = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(serverUrl, applicationId, applicationSecret);
|
||||
const applicationInfo = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
await giteaHelper.addAccount(applicationInfo);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addGoogleDriveAccount() {
|
||||
@ -245,6 +280,18 @@ export default {
|
||||
await zendeskHelper.addAccount(subdomain, clientId);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addSmmsAccount() {
|
||||
try {
|
||||
const { proxyUrl, apiSecretToken } = await store.dispatch('modal/open', { type: 'smmsAccount' });
|
||||
await smmsHelper.addAccount(proxyUrl, apiSecretToken);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async addCustomAccount() {
|
||||
try {
|
||||
const accountInfo = await store.dispatch('modal/open', { type: 'customAccount' });
|
||||
await customHelper.addAccount(accountInfo);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -252,6 +299,18 @@ export default {
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.line-entry {
|
||||
word-break: break-word; /* 文本行的任意字内断开,就算是一个单词也会分开 */
|
||||
word-wrap: break-word; /* IE */
|
||||
white-space: -moz-pre-wrap; /* Mozilla */
|
||||
white-space: -hp-pre-wrap; /* HP printers */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: pre; /* CSS2 */
|
||||
white-space: pre-wrap; /* CSS 2.1 */
|
||||
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
|
||||
}
|
||||
|
||||
.account-entry {
|
||||
margin: 1.5em 0;
|
||||
height: auto;
|
||||
|
131
src/components/modals/ChatGptModal.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--chatgpt" aria-label="chatgpt">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-chat-gpt></icon-chat-gpt>
|
||||
</div>
|
||||
<p><b>ChatGPT内容生成</b><br>生成时长受ChatGPT服务响应与网络响应时长影响,时间可能较长</p>
|
||||
<form-entry label="生成内容要求详细描述" error="content">
|
||||
<textarea slot="field" class="text-input" type="text" placeholder="输入内容(支持换行)" v-model.trim="content" :disabled="generating"></textarea>
|
||||
<div class="form-entry__info">
|
||||
使用 <a href="https://api35.pxj123.cn/" target="_blank">api35.pxj123.cn</a> 的免费接口生成内容,AI模型是:GPT-3.5 Turbo。
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__result">
|
||||
<pre class="result_pre" v-if="generating && !result">(等待生成中...)</pre>
|
||||
<pre class="result_pre" v-else v-text="result"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">{{ generating ? '停止' : '关闭' }}</button>
|
||||
<button class="button button--resolve" @click="generate" v-if="!generating && !!content">{{ !!result ? '重新生成' : '开始生成' }}</button>
|
||||
<button class="button button--resolve" @click="resolve" v-if="!generating && !!result">确认插入</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import chatGptSvc from '../../services/chatGptSvc';
|
||||
import store from '../../store';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
generating: false,
|
||||
content: '',
|
||||
result: '',
|
||||
xhr: null,
|
||||
}),
|
||||
methods: {
|
||||
resolve(evt) {
|
||||
evt.preventDefault();
|
||||
const { callback } = this.config;
|
||||
this.config.resolve();
|
||||
callback(this.result);
|
||||
},
|
||||
process({ done, content, error }) {
|
||||
if (done) {
|
||||
this.generating = false;
|
||||
// 已结束
|
||||
} else if (content) {
|
||||
this.result = this.result + content;
|
||||
const container = document.querySelector('.result_pre');
|
||||
container.scrollTo(0, container.scrollHeight); // 滚动到最底部
|
||||
} else if (error) {
|
||||
this.generating = false;
|
||||
}
|
||||
},
|
||||
generate() {
|
||||
this.generating = true;
|
||||
this.result = '';
|
||||
try {
|
||||
this.xhr = chatGptSvc.chat({
|
||||
content: `${this.content}\n(使用Markdown方式输出结果)`,
|
||||
}, this.process);
|
||||
} catch (err) {
|
||||
this.generating = false;
|
||||
store.dispatch('notification/error', err);
|
||||
}
|
||||
},
|
||||
reject() {
|
||||
if (this.generating) {
|
||||
if (this.xhr) {
|
||||
this.xhr.abort();
|
||||
this.generating = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { callback } = this.config;
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://api35.pxj123.cn/js/chat.js?t=${new Date().getTime()}`;
|
||||
script.onload = () => {
|
||||
/* eslint-disable */
|
||||
console.log('加载外部chatgpt的js成功!');
|
||||
};
|
||||
this.$el.appendChild(script);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1.modal__inner-1--chatgpt {
|
||||
max-width: 560px;
|
||||
|
||||
.result_pre {
|
||||
font-size: 0.9em;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
line-height: 1.25;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
height: 300px;
|
||||
border: 1px solid rgb(126, 126, 126);
|
||||
border-radius: $border-radius-base;
|
||||
padding: 10px;
|
||||
overflow-y: scroll; /* 开启垂直滚动条 */
|
||||
}
|
||||
|
||||
.result_pre::-webkit-scrollbar {
|
||||
display: none; /* 隐藏滚动条 */
|
||||
}
|
||||
|
||||
.result_pre.scroll-bottom {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.config-warning {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
</style>
|
43
src/components/modals/CommitMessageModal.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<modal-inner aria-label="提交信息">
|
||||
<p>自定义 <b>{{ config.name }}</b> 提交信息。</p>
|
||||
<div class="modal__content">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label">提交信息</label>
|
||||
<div class="form-entry__field">
|
||||
<input slot="field" class="textfield" placeholder="提交信息非必填" type="text" v-model.trim="commitMessage" @keydown.enter="resolve()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">取消</button>
|
||||
<button class="button button--resolve" @click="resolve()">确认</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
},
|
||||
data: () => ({
|
||||
commitMessage: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
this.config.resolve({
|
||||
commitMessage: this.commitMessage,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,46 +1,173 @@
|
||||
<template>
|
||||
<modal-inner aria-label="插入图像">
|
||||
<div class="modal__content">
|
||||
<p>请为您的图像提供<b> url </b>。</p>
|
||||
<p>请为您的图像提供<b> url </b>。<span v-if="uploading">(图片上传中...)</span></p>
|
||||
<form-entry label="URL" error="url">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keydown.enter="resolve">
|
||||
</form-entry>
|
||||
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
||||
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||
<div>从Google照片打开</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGooglePhotosAccount">
|
||||
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||
<span>添加Google Photos账号</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<input class="hidden-file" id="upload-image-file-input" type="file" accept="image/*" :disabled="uploading" @change="uploadImage">
|
||||
<label for="upload-image-file-input"><a class="button">上传图片</a></label>
|
||||
<button class="button" @click="reject()">取消</button>
|
||||
<button class="button button--resolve" @click="resolve">确认</button>
|
||||
<button class="button button--resolve" @click="resolve" :disabled="uploading">确认</button>
|
||||
</div>
|
||||
<div>
|
||||
<hr />
|
||||
<p>添加并选择图床后可在编辑区中粘贴/拖拽图片自动上传</p>
|
||||
|
||||
<menu-entry @click.native="checkedImgDest(path)" v-for="path in workspaceImgPath" :key="path">
|
||||
<icon-check-circle v-if="checkedStorage.sub === path" slot="icon"></icon-check-circle>
|
||||
<icon-check-circle-un v-if="checkedStorage.sub !== path" slot="icon"></icon-check-circle-un>
|
||||
<menu-item>
|
||||
<icon-provider slot="icon" :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||
<div>
|
||||
当前文档空间图片路径
|
||||
<button class="menu-item__button button" @click.stop="removeByPath(path)" v-title="'删除'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
<span>路径:{{path}}</span>
|
||||
</menu-item>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="checkedImgDest(token.sub, token.providerId)" v-for="token in imageTokens" :key="token.sub">
|
||||
<icon-check-circle v-if="checkedStorage.sub === token.sub" slot="icon"></icon-check-circle>
|
||||
<icon-check-circle-un v-if="checkedStorage.sub !== token.sub" slot="icon"></icon-check-circle-un>
|
||||
<menu-item>
|
||||
<icon-provider slot="icon" :provider-id="token.providerId"></icon-provider>
|
||||
<div>
|
||||
{{ token.remark }}
|
||||
<button class="menu-item__button button" @click.stop="remove(token.providerId, token)" v-title="'删除'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span class="line-entry" v-if="token.uploadUrl">上传地址:{{token.uploadUrl}}</span>
|
||||
<span class="line-entry" v-if="token.headers">自定义请求头:{{token.headers}}</span>
|
||||
<span class="line-entry" v-if="token.params">自定义Form参数:{{token.params}}</span>
|
||||
</menu-item>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="checkedImgDest(tokenStorage.token.sub, tokenStorage.providerId, tokenStorage.sid)" v-for="tokenStorage in tokensImgStorages" :key="tokenStorage.sid">
|
||||
<icon-check-circle v-if="checkedStorage.sid === tokenStorage.sid" slot="icon"></icon-check-circle>
|
||||
<icon-check-circle-un v-if="checkedStorage.sid !== tokenStorage.sid" slot="icon"></icon-check-circle-un>
|
||||
<menu-item>
|
||||
<icon-provider slot="icon" :provider-id="tokenStorage.providerId"></icon-provider>
|
||||
<div>{{tokenStorage.providerName}}
|
||||
<button class="menu-item__button button" @click.stop="remove(tokenStorage.providerId, tokenStorage)" v-title="'删除'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
<span> {{tokenStorage.uname}}, 仓库URL: {{tokenStorage.repoUrl}}, 路径: {{tokenStorage.path}}, 分支: {{tokenStorage.branch}}</span>
|
||||
</menu-item>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addWorkspaceImgPath">
|
||||
<icon-provider slot="icon" :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||
<span>添加当前文档空间图片路径</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addSmmsAccount">
|
||||
<icon-provider slot="icon" provider-id="smms"></icon-provider>
|
||||
<span>添加SM.MS图床账号</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addCustomAccount">
|
||||
<icon-provider slot="icon" provider-id="custom"></icon-provider>
|
||||
<span>添加自定义图床账号</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGiteaImgStorage">
|
||||
<icon-provider slot="icon" provider-id="gitea"></icon-provider>
|
||||
<span>添加Gitea图床仓库</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGithubImgStorage">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<span>添加GitHub图床仓库</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import MenuEntry from '../menus/common/MenuEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import MenuItem from '../menus/common/MenuItem';
|
||||
import smmsHelper from '../../services/providers/helpers/smmsHelper';
|
||||
import store from '../../store';
|
||||
import giteaHelper from '../../services/providers/helpers/giteaHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import customHelper from '../../services/providers/helpers/customHelper';
|
||||
import utils from '../../services/utils';
|
||||
import imageSvc from '../../services/imageSvc';
|
||||
|
||||
export default modalTemplate({
|
||||
components: {
|
||||
MenuEntry,
|
||||
MenuItem,
|
||||
},
|
||||
data: () => ({
|
||||
uploading: false,
|
||||
url: '',
|
||||
}),
|
||||
computed: {
|
||||
googlePhotosTokens() {
|
||||
const googleTokensBySub = store.getters['data/googleTokensBySub'];
|
||||
return Object.values(googleTokensBySub)
|
||||
.filter(token => token.isPhotos)
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
...mapGetters('workspace', [
|
||||
'currentWorkspace',
|
||||
'currentWorkspaceIsGit',
|
||||
]),
|
||||
checkedStorage() {
|
||||
return store.getters['img/getCheckedStorage'];
|
||||
},
|
||||
workspaceImgPath() {
|
||||
if (!this.currentWorkspaceIsGit) {
|
||||
return [];
|
||||
}
|
||||
const workspaceImgPath = store.getters['img/getWorkspaceImgPath'];
|
||||
return Object.keys(workspaceImgPath || {});
|
||||
},
|
||||
imageTokens() {
|
||||
return [
|
||||
...Object.values(store.getters['data/smmsTokensBySub']).map(token => ({
|
||||
...token,
|
||||
providerId: 'smms',
|
||||
remark: 'SM.MS图床',
|
||||
})),
|
||||
...Object.values(store.getters['data/customTokensBySub']).map(token => ({
|
||||
...token,
|
||||
providerId: 'custom',
|
||||
headers: token.customHeaders && JSON.stringify(token.customHeaders),
|
||||
params: token.customParams && JSON.stringify(token.customParams),
|
||||
remark: '自定义图床',
|
||||
})),
|
||||
];
|
||||
},
|
||||
tokensImgStorages() {
|
||||
const providerTokens = [
|
||||
...Object.values(store.getters['data/giteaTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'gitea',
|
||||
providerName: 'Gitea图床',
|
||||
})),
|
||||
...Object.values(store.getters['data/githubTokensBySub']).map(token => ({
|
||||
token,
|
||||
providerId: 'github',
|
||||
providerName: 'GitHub图床',
|
||||
})),
|
||||
];
|
||||
const imgStorages = [];
|
||||
Object.values(providerTokens)
|
||||
.sort((item1, item2) => item1.token.name.localeCompare(item2.token.name))
|
||||
.forEach((it) => {
|
||||
if (!it.token.imgStorages || it.token.imgStorages.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 拼接上当前用户名
|
||||
it.token.imgStorages.forEach(storage => imgStorages.push({
|
||||
...storage,
|
||||
token: it.token,
|
||||
uname: it.token.name,
|
||||
providerId: it.providerId,
|
||||
providerName: it.providerName,
|
||||
repoUrl: it.providerId === 'gitea' ? `${it.token.serverUrl}/${storage.repoUri}` : `${storage.owner}/${storage.repo}`,
|
||||
}));
|
||||
});
|
||||
return imgStorages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@ -59,23 +186,129 @@ export default modalTemplate({
|
||||
this.config.reject();
|
||||
callback(null);
|
||||
},
|
||||
async addGooglePhotosAccount() {
|
||||
try {
|
||||
await googleHelper.addPhotosAccount();
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGooglePhotos(token) {
|
||||
const { callback } = this.config;
|
||||
this.config.reject();
|
||||
const res = await googleHelper.openPicker(token, 'img');
|
||||
if (res[0]) {
|
||||
store.dispatch('modal/open', {
|
||||
type: 'googlePhoto',
|
||||
url: res[0].url,
|
||||
callback,
|
||||
});
|
||||
async uploadImage(evt) {
|
||||
if (!evt.target.files || !evt.target.files.length) {
|
||||
return;
|
||||
}
|
||||
const imgFile = evt.target.files[0];
|
||||
try {
|
||||
this.uploading = true;
|
||||
const { url, error } = await imageSvc.updateImg(imgFile);
|
||||
if (error) {
|
||||
store.dispatch('notification/error', error);
|
||||
return;
|
||||
}
|
||||
this.url = url;
|
||||
} catch (err) {
|
||||
store.dispatch('notification/error', err);
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
// 上传后清空
|
||||
evt.target.value = '';
|
||||
}
|
||||
},
|
||||
async remove(proivderId, item) {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'imgStorageDeletion');
|
||||
if (proivderId === 'smms' || proivderId === 'custom') {
|
||||
const tokensBySub = utils.deepCopy(store.getters[`data/${proivderId}TokensBySub`]);
|
||||
delete tokensBySub[item.sub];
|
||||
// 删除账号
|
||||
await store.dispatch('data/patchTokensByType', {
|
||||
[proivderId]: tokensBySub,
|
||||
});
|
||||
} else if (proivderId === 'gitea') {
|
||||
giteaHelper.removeTokenImgStorage(item.token, item.sid);
|
||||
} else if (proivderId === 'github') {
|
||||
githubHelper.removeTokenImgStorage(item.token, item.sid);
|
||||
}
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
async removeByPath(path) {
|
||||
store.dispatch('img/removeWorkspaceImgPath', path);
|
||||
},
|
||||
async addWorkspaceImgPath() {
|
||||
const { path } = await store.dispatch('modal/open', { type: 'workspaceImgPath' });
|
||||
store.dispatch('img/addWorkspaceImgPath', path);
|
||||
},
|
||||
async addSmmsAccount() {
|
||||
const { proxyUrl, apiSecretToken } = await store.dispatch('modal/open', { type: 'smmsAccount' });
|
||||
await smmsHelper.addAccount(proxyUrl, apiSecretToken);
|
||||
},
|
||||
async addCustomAccount() {
|
||||
const accountInfo = await store.dispatch('modal/open', { type: 'customAccount' });
|
||||
await customHelper.addAccount(accountInfo);
|
||||
},
|
||||
async addGiteaImgStorage() {
|
||||
try {
|
||||
const applicationInfo = await store.dispatch('modal/open', { type: 'giteaAccount' });
|
||||
const token = await giteaHelper.addAccount(applicationInfo);
|
||||
const imgStorageInfo = await store.dispatch('modal/open', {
|
||||
type: 'giteaImgStorage',
|
||||
token,
|
||||
});
|
||||
giteaHelper.updateToken(token, imgStorageInfo);
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async addGithubImgStorage() {
|
||||
try {
|
||||
await store.dispatch('modal/open', { type: 'githubAccount' });
|
||||
const token = await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
|
||||
const imgStorageInfo = await store.dispatch('modal/open', {
|
||||
type: 'githubImgStorage',
|
||||
token,
|
||||
});
|
||||
githubHelper.updateToken(token, imgStorageInfo);
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async checkedImgDest(sub, provider, sid) {
|
||||
let type = 'token';
|
||||
// 当前文档空间存储
|
||||
if (!provider) {
|
||||
type = 'workspace';
|
||||
} else if (provider === 'gitea' || provider === 'github') {
|
||||
type = 'tokenRepo';
|
||||
}
|
||||
store.dispatch('img/changeCheckedStorage', {
|
||||
type,
|
||||
provider,
|
||||
sub,
|
||||
sid,
|
||||
});
|
||||
// const { callback } = this.config;
|
||||
// this.config.reject();
|
||||
// const res = await googleHelper.openPicker(token, 'img');
|
||||
// if (res[0]) {
|
||||
// store.dispatch('modal/open', {
|
||||
// type: 'googlePhoto',
|
||||
// url: res[0].url,
|
||||
// callback,
|
||||
// });
|
||||
// }
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.line-entry {
|
||||
word-break: break-word; /* 文本行的任意字内断开,就算是一个单词也会分开 */
|
||||
word-wrap: break-word; /* IE */
|
||||
white-space: -moz-pre-wrap; /* Mozilla */
|
||||
white-space: -hp-pre-wrap; /* HP printers */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: pre; /* CSS2 */
|
||||
white-space: pre-wrap; /* CSS 2.1 */
|
||||
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
|
||||
}
|
||||
|
||||
.menu-item__button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 4px;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<modal-inner aria-label="导出到PDF">
|
||||
<div class="modal__content">
|
||||
<p>请为您的<b> pdf导出</b>选择模板。</p>
|
||||
<p>请为您的<b> pdf导出</b>选择模板。(该导出很消耗服务器资源,文档太大或图片太多可能会导出超时失败!可参考 <a href="https://gitee.com/mafgwo/stackedit/blob/master/docs/大文档导出PDF方式.md" target="_blank">大文档导出PDF方式</a> 自行导出大文档!)</p>
|
||||
<form-entry label="模板">
|
||||
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
|
||||
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
|
||||
|
@ -4,8 +4,8 @@
|
||||
<div class="modal__image">
|
||||
<icon-upload></icon-upload>
|
||||
</div>
|
||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> 被发布到了以下位置:</p>
|
||||
<p v-else><b>{{currentFileName}}</b> 还没有被发布.</p>
|
||||
<div>
|
||||
<div class="publish-entry flex flex--column" v-for="location in publishLocations" :key="location.id">
|
||||
<div class="publish-entry__header flex flex--row flex--align-center">
|
||||
@ -26,7 +26,7 @@
|
||||
{{location.url}}
|
||||
</div>
|
||||
<div class="publish-entry__buttons flex flex--row flex--center" v-if="location.url">
|
||||
<button class="publish-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'复制URL'">
|
||||
<button class="publish-entry__button button" v-clipboard="location.url" @click="info('位置URL已复制到剪贴板!')" v-title="'复制URL'">
|
||||
<icon-content-copy></icon-content-copy>
|
||||
</button>
|
||||
<a class="publish-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'打开位置'">
|
||||
@ -34,14 +34,27 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="publish-entry__row flex flex--row flex--align-center" v-if="shareUrl(location)">
|
||||
<div class="publish-entry__url">
|
||||
分享链接: {{shareUrl(location)}}
|
||||
</div>
|
||||
<div class="publish-entry__buttons flex flex--row flex--center">
|
||||
<button class="publish-entry__button button" v-clipboard="shareUrl(location)" @click="info('分享URL已复制到剪贴板!')" v-title="'复制分享URL'">
|
||||
<icon-content-copy></icon-content-copy>
|
||||
</button>
|
||||
<a class="publish-entry__button button" :href="shareUrl(location)" target="_blank" v-title="'打开分享'">
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__info" v-if="publishLocations.length">
|
||||
<b>Tip:</b> Removing a location won't delete any file.
|
||||
<b>提示:</b> 删除位置不会删除任何文件。
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
<button class="button button--resolve" @click="config.resolve()">关闭</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -75,6 +88,16 @@ export default {
|
||||
store.commit('publishLocation/deleteItem', location.id);
|
||||
badgeSvc.addBadge('removePublishLocation');
|
||||
},
|
||||
shareUrl(location) {
|
||||
if (location.providerId !== 'giteegist' && location.providerId !== 'gist') {
|
||||
return null;
|
||||
}
|
||||
if (!location.url || !location.gistId) {
|
||||
return null;
|
||||
}
|
||||
const sharePage = location.providerId === 'gist' ? 'gistshare.html' : 'share.html';
|
||||
return `${window.location.protocol}//${window.location.host}/${sharePage}?id=${location.gistId}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
48
src/components/modals/WorkspaceImgPathModal.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<modal-inner aria-label="文档空间图片路径">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||
</div>
|
||||
<p>在当前文档空间增加图片上传路径。</p>
|
||||
<form-entry label="图片上传路径" error="path">
|
||||
<input slot="field" class="textfield" type="text" placeholder="如:/imgs/{YYYY}-{MM}-{DD}" v-model.trim="path" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
如果不提供,默认为 /imgs/{YYYY}-{MM}-{DD}。<br/>
|
||||
支持相对路径,如 ./imgs、../imgs 或 imgs 都是相对当前编辑中文档的路径。<br/>
|
||||
变量说明:{YYYY}为年变量、{MM}为月变量、{DD}为日变量、{MDNAME}为当前文档名称
|
||||
</div>
|
||||
</form-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">取消</button>
|
||||
<button class="button button--resolve" @click="resolve()">确认</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
computedLocalSettings: {
|
||||
path: '',
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('workspace', [
|
||||
'currentWorkspace',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (!this.path) {
|
||||
this.setError('path');
|
||||
}
|
||||
this.config.resolve({
|
||||
path: this.path || '/imgs/{YYYY}-{MM}-{DD}',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -9,7 +9,8 @@
|
||||
<div class="flex flex--column">
|
||||
<div class="workspace-entry__header flex flex--row flex--align-center">
|
||||
<div class="workspace-entry__icon">
|
||||
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||
<icon-provider v-if="id === 'main' && !workspace.sub" :provider-id="'stackedit'"></icon-provider>
|
||||
<icon-provider v-else :provider-id="workspace.providerId"></icon-provider>
|
||||
</div>
|
||||
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
|
||||
<div class="workspace-entry__name" v-else>{{workspace.name}}</div>
|
||||
@ -17,6 +18,15 @@
|
||||
<button class="workspace-entry__button button" @click="edit(id)" v-title="'编辑名称'">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<template v-if="workspace.providerId === 'giteeAppData' || workspace.providerId === 'githubAppData' || workspace.providerId === 'githubWorkspace'
|
||||
|| workspace.providerId === 'giteeWorkspace' || workspace.providerId === 'gitlabWorkspace' || workspace.providerId === 'giteaWorkspace'">
|
||||
<button class="workspace-entry__button button" @click="stopAutoSync(id)" v-if="workspace.autoSync == undefined || workspace.autoSync" v-title="'关闭自动同步'">
|
||||
<icon-sync-auto></icon-sync-auto>
|
||||
</button>
|
||||
<button class="workspace-entry__button button" @click="startAutoSync(id)" v-if="workspace.autoSync != undefined && !workspace.autoSync" v-title="'启动自动同步'">
|
||||
<icon-sync-stop></icon-sync-stop>
|
||||
</button>
|
||||
</template>
|
||||
<button class="workspace-entry__button button" @click="remove(id)" v-title="'删除'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
@ -122,12 +132,53 @@ export default {
|
||||
this.info('请先关闭文档空间,然后再将其删除。');
|
||||
} else {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'removeWorkspace');
|
||||
const workspace = this.workspacesById[id];
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'removeWorkspace',
|
||||
name: workspace.name,
|
||||
});
|
||||
workspaceSvc.removeWorkspace(id);
|
||||
badgeSvc.addBadge('removeWorkspace');
|
||||
} catch (e) { /* Cancel */ }
|
||||
}
|
||||
},
|
||||
async stopAutoSync(id) {
|
||||
const workspace = this.workspacesById[id];
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'stopAutoSyncWorkspace',
|
||||
name: workspace.name,
|
||||
});
|
||||
store.dispatch('workspace/patchWorkspacesById', {
|
||||
[id]: {
|
||||
...workspace,
|
||||
autoSync: false,
|
||||
},
|
||||
});
|
||||
badgeSvc.addBadge('stopAutoSyncWorkspace');
|
||||
},
|
||||
async startAutoSync(id) {
|
||||
const workspace = this.workspacesById[id];
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
await store.dispatch('modal/open', {
|
||||
type: 'autoSyncWorkspace',
|
||||
name: workspace.name,
|
||||
});
|
||||
store.dispatch('workspace/patchWorkspacesById', {
|
||||
[id]: {
|
||||
...workspace,
|
||||
autoSync: true,
|
||||
},
|
||||
});
|
||||
badgeSvc.addBadge('autoSyncWorkspace');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
Object.keys(this.workspacesById).forEach(async (workspaceId) => {
|
||||
|
@ -17,7 +17,7 @@ export default {
|
||||
uid: utils.uid(),
|
||||
}),
|
||||
mounted() {
|
||||
this.$el.querySelector('input,select').id = this.uid;
|
||||
this.$el.querySelector('input,select,textarea').id = this.uid;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
109
src/components/modals/providers/CustomAccountModal.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<modal-inner aria-label="链接自定义图床账号">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="custom"></icon-provider>
|
||||
</div>
|
||||
<p>将您的<b>自定义图床</b>账号链接到<b>StackEdit中文版</b>。</p>
|
||||
<form-entry label="自定义标识" error="name">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="name" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
自定义标识如果一样会覆盖之前的自定义图床账号。
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="上传图片接口地址" error="uploadUrl">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="uploadUrl" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
填入您个人的图床上传接口地址,上传接口仅支持POST提交。
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="文件参数名" error="fileParamName">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="fileParamName" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
文件参数名如:file
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="自定义请求头配置" error="customHeaders">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="customHeaders" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
非必填,自定义请求头是JSON字符串格式,如:{"token": "..."}
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="自定义FORM参数设置" error="customParams">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="customParams" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
非必填,自定义FORM参数是JSON字符串格式,如:{"param1": "..."}
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="响应图片URL参数" error="resultUrlParam">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="resultUrlParam" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
响应JSON中图片URL的路径,如 data.url
|
||||
</div>
|
||||
</form-entry>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">取消</button>
|
||||
<button class="button button--resolve" @click="resolve()">确认</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
computedLocalSettings: {
|
||||
name: 'name',
|
||||
uploadUrl: 'uploadUrl',
|
||||
fileParamName: 'fileParamName',
|
||||
customHeaders: 'customHeaders',
|
||||
customParams: 'customParams',
|
||||
resultUrlParam: 'resultUrlParam',
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (!this.name) {
|
||||
this.setError('name');
|
||||
}
|
||||
if (!this.uploadUrl) {
|
||||
this.setError('uploadUrl');
|
||||
}
|
||||
if (!this.fileParamName) {
|
||||
this.setError('fileParamName');
|
||||
}
|
||||
if (!this.resultUrlParam) {
|
||||
this.setError('resultUrlParam');
|
||||
}
|
||||
let customHeaders = null;
|
||||
if (this.customHeaders) {
|
||||
try {
|
||||
customHeaders = JSON.parse(this.customHeaders);
|
||||
} catch (err) {
|
||||
this.setError('customHeaders');
|
||||
return;
|
||||
}
|
||||
}
|
||||
let customParams = null;
|
||||
if (this.customParams) {
|
||||
try {
|
||||
customParams = JSON.parse(this.customParams);
|
||||
} catch (err) {
|
||||
this.setError('customParams');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.uploadUrl && this.fileParamName) {
|
||||
this.config.resolve({
|
||||
name: this.name,
|
||||
uploadUrl: this.uploadUrl,
|
||||
fileParamName: this.fileParamName,
|
||||
resultUrlParam: this.resultUrlParam,
|
||||
customHeaders,
|
||||
customParams,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -4,7 +4,7 @@
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
<p>将您的<b>Dropbox</b>链接到<b>StackEdit</b>。</p>
|
||||
<p>将您的<b>Dropbox</b>链接到<b>StackEdit中文版</b>。</p>
|
||||
<div class="form-entry">
|
||||
<div class="form-entry__checkbox">
|
||||
<label>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<modal-inner aria-label="发布到Gist">
|
||||
<modal-inner aria-label="发布到GitHubGist">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</div>
|
||||
<p>发布<b> {{CurrentFileName}} </b>到<b>Gist</b>。</p>
|
||||
<form-entry label="Filename" error="filename">
|
||||
<p>发布<b> {{CurrentFileName}} </b>到<b>GitHubGist</b>。</p>
|
||||
<form-entry label="文件名" error="filename">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
|
||||
</form-entry>
|
||||
<div class="form-entry">
|
||||
@ -15,10 +15,10 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<form-entry label="Existing Gist ID" info="可选的">
|
||||
<form-entry label="存在Gist ID" info="可选的">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
|
||||
<div class="form-entry__info">
|
||||
如果文件存在于Gist中,则将被覆盖。
|
||||
如果文件存在于GitHubGist中,则将被覆盖。
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="Template">
|
||||
|