Compare commits
471 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 | ||
![]() |
613ff29076 | ||
![]() |
1cb3810af8 | ||
![]() |
eaa37bab02 | ||
![]() |
f489a611b0 | ||
![]() |
9651197c5c | ||
![]() |
8b55b32ec8 | ||
![]() |
f0612d1120 | ||
![]() |
b94898960a | ||
![]() |
5789b2993f | ||
![]() |
45bee6bbf6 | ||
![]() |
e53ee489b4 | ||
![]() |
74b0fc4635 | ||
![]() |
198c8cb647 | ||
![]() |
cb58d05147 | ||
![]() |
97b436534f | ||
![]() |
a33cef478b | ||
![]() |
dc0a0521ec | ||
![]() |
c1976545ef | ||
![]() |
0656045f67 | ||
![]() |
3ee3cf6470 | ||
![]() |
dd8191efe7 | ||
![]() |
a8afbc69c2 | ||
![]() |
666db76f3c | ||
![]() |
329d00c707 | ||
![]() |
9e5d865f6a | ||
![]() |
4ef712b988 | ||
![]() |
d52c677acd | ||
![]() |
e7e335d958 | ||
![]() |
46383b5b6a | ||
![]() |
bad9abd1c5 | ||
![]() |
97c97c5137 | ||
![]() |
f335c386e5 | ||
![]() |
260f085585 | ||
![]() |
e7bb627385 | ||
![]() |
bc688eab71 | ||
![]() |
c8987a8e84 | ||
![]() |
0f0c9bed6c | ||
![]() |
b75deaf8a4 | ||
![]() |
a61a35be75 | ||
![]() |
2fdbb67c67 | ||
![]() |
52cd051198 | ||
![]() |
3dd66ae280 | ||
![]() |
ede9551356 | ||
![]() |
c6de74220a | ||
![]() |
8d70a4f438 | ||
![]() |
b4d34a9390 | ||
![]() |
0001e8c178 | ||
![]() |
90520abf93 | ||
![]() |
f611060d05 | ||
![]() |
d2867ea1c0 | ||
![]() |
b2158fc1c3 | ||
![]() |
5a4fb796e0 | ||
![]() |
043a3cd709 | ||
![]() |
8a6df3ef10 | ||
![]() |
e9e3cb809d | ||
![]() |
07d824faca | ||
![]() |
3f0597601e | ||
![]() |
274a800742 | ||
![]() |
72a15b5039 | ||
![]() |
dc08ee4da4 | ||
![]() |
a98e9a382e | ||
![]() |
a8ee8cdccd | ||
![]() |
f05b089485 | ||
![]() |
d6fb85a5ad | ||
![]() |
78802448c3 | ||
![]() |
1b2d48ff22 | ||
![]() |
2a865ddb44 | ||
![]() |
c462a13ab5 | ||
![]() |
8cf0b87f5f | ||
![]() |
835ef7f5bc | ||
![]() |
8156038c53 | ||
![]() |
91f8cf3c10 | ||
![]() |
1ac1e057cf | ||
![]() |
da0ccf8d82 | ||
![]() |
cf18184e26 | ||
![]() |
deba3c12be | ||
![]() |
547f44043c | ||
![]() |
daadae1720 | ||
![]() |
0e8aa0a58a | ||
![]() |
f7542965b6 | ||
![]() |
6fd4513c34 | ||
![]() |
fa3d9b30c2 | ||
![]() |
4be3200961 | ||
![]() |
da9b87620f | ||
![]() |
6b91c2bafb | ||
![]() |
2e832fd766 | ||
![]() |
fd6ac907bb | ||
![]() |
7828649b3f | ||
![]() |
d2af43ac1c | ||
![]() |
f7f66ba249 | ||
![]() |
02de03fc9f | ||
![]() |
5bc0f37ad1 | ||
![]() |
fc8892d990 | ||
![]() |
72e5ac8072 | ||
![]() |
e9638b9251 | ||
![]() |
0b3600c0b4 | ||
![]() |
e07df09129 | ||
![]() |
89bc00b39f | ||
![]() |
907410a2ca | ||
![]() |
e76f28294b | ||
![]() |
10a2d57f75 | ||
![]() |
7b623cc77f | ||
![]() |
d443be2a47 | ||
![]() |
beb695e53c | ||
![]() |
87f281c700 | ||
![]() |
f85e9621b3 | ||
![]() |
b524171b51 | ||
![]() |
c1b80def91 | ||
![]() |
30b1585231 | ||
![]() |
1273ec7cb3 | ||
![]() |
9a6d0bddc8 | ||
![]() |
0d39ca85fc | ||
![]() |
3fc974c14c | ||
![]() |
d4624eba9d | ||
![]() |
bdb01f407b | ||
![]() |
b3b1010ebd | ||
![]() |
e773d7ebff | ||
![]() |
bc9c2ca666 | ||
![]() |
e0284b8072 | ||
![]() |
cda25587d1 | ||
![]() |
7488856489 | ||
![]() |
a4f0c11726 | ||
![]() |
fe111c207c | ||
![]() |
218cb99920 | ||
![]() |
9809685e53 | ||
![]() |
6dbd81c406 | ||
![]() |
489e9a5717 | ||
![]() |
e4e9631b31 | ||
![]() |
b3f46bec11 | ||
![]() |
6989d421ed | ||
![]() |
0632445a22 | ||
![]() |
952ceaa76c | ||
![]() |
a9d1199628 | ||
![]() |
133419b1b4 | ||
![]() |
79e87dbcf3 | ||
![]() |
987d66ef26 | ||
![]() |
a1673d3e87 | ||
![]() |
7a87015af1 | ||
![]() |
e05e7717eb | ||
![]() |
597c747b00 | ||
![]() |
e971082768 | ||
![]() |
1d8b67c321 | ||
![]() |
b896a2e086 | ||
![]() |
53ccee0d84 | ||
![]() |
790ac996dd | ||
![]() |
c6eb9bb479 | ||
![]() |
28165e662b | ||
![]() |
a6d07e1cad | ||
![]() |
67a80fdc5b | ||
![]() |
642a0a0078 | ||
![]() |
8997b0940e | ||
![]() |
4efedee172 | ||
![]() |
80d9f8b020 | ||
![]() |
83bc227d9a | ||
![]() |
c3de3c1f84 | ||
![]() |
89880ed1a4 | ||
![]() |
088a032f64 | ||
![]() |
c42a7787c1 | ||
![]() |
e709da443f | ||
![]() |
ab97d9d9fc | ||
![]() |
fb13a52157 | ||
![]() |
6d59514835 | ||
![]() |
0a361a5ca0 | ||
![]() |
12c43b960b | ||
![]() |
e578f83d8e | ||
![]() |
762913c400 | ||
![]() |
83666b0449 | ||
![]() |
a2f1f0fa88 | ||
![]() |
a09d06e961 | ||
![]() |
6a3434c889 | ||
![]() |
0afdaa557f | ||
![]() |
3625d5e17b | ||
![]() |
e2ec937ac7 | ||
![]() |
907605a2b5 | ||
![]() |
12c453867f | ||
![]() |
8e4279edc1 | ||
![]() |
760c7e3e0d | ||
![]() |
ef3d3b6a74 | ||
![]() |
2d179f34dd | ||
![]() |
0cf15a36c6 | ||
![]() |
c67f2c88b9 | ||
![]() |
d6a67f7309 | ||
![]() |
bde9cf0596 | ||
![]() |
83a47007b0 | ||
![]() |
0c97f16587 | ||
![]() |
88294ffa79 | ||
![]() |
9c4d821812 | ||
![]() |
d133fc5c28 | ||
![]() |
d604501974 | ||
![]() |
5973a0acc6 | ||
![]() |
32520d02da | ||
![]() |
78c547777e | ||
![]() |
c0adf31801 | ||
![]() |
14e35f0eb1 | ||
![]() |
0091bcbbed | ||
![]() |
e992364904 | ||
![]() |
beac7fb1a3 | ||
![]() |
7c0bf68ad3 | ||
![]() |
c5e0f3e2e4 | ||
![]() |
88cf11972f | ||
![]() |
53f2076585 | ||
![]() |
de2e145a9a | ||
![]() |
623c265adc | ||
![]() |
59a05141ca | ||
![]() |
906e672e49 | ||
![]() |
7d5b9d63e3 | ||
![]() |
e590eaa222 | ||
![]() |
fabceeb626 | ||
![]() |
b06a6a37eb | ||
![]() |
4b5cd7aef7 | ||
![]() |
34e15ee1c4 | ||
![]() |
57f45f0c5f | ||
![]() |
0b74d48bc8 | ||
![]() |
d57d2bb969 | ||
![]() |
f0721c9405 | ||
![]() |
efce97c16c | ||
![]() |
b4e274df66 | ||
![]() |
1bf7ebe199 | ||
![]() |
c4651fcb3f | ||
![]() |
1e8fa944a1 | ||
![]() |
5b6b9ebeb8 | ||
![]() |
95b2bae631 | ||
![]() |
bb67113996 | ||
![]() |
8d3c4d65b4 | ||
![]() |
b51a772299 | ||
![]() |
d151b9a687 | ||
![]() |
c798df1a67 | ||
![]() |
f92ba9340a | ||
![]() |
3fa2d7b73e | ||
![]() |
56b715cd3f | ||
![]() |
4d07cbb1e3 | ||
![]() |
f66f120afc | ||
![]() |
2374f459df | ||
![]() |
aac305e410 | ||
![]() |
ec0d5aac3e | ||
![]() |
455e88f183 | ||
![]() |
ed67f14f6f | ||
![]() |
97c6346953 | ||
![]() |
5aec35cb43 | ||
![]() |
cd97855c87 | ||
![]() |
c511805cca | ||
![]() |
ce1014b848 | ||
![]() |
e65dc094a0 | ||
![]() |
cc1e3e8379 | ||
![]() |
1e371473ad | ||
![]() |
752ecd0dce | ||
![]() |
ba258a96f6 | ||
![]() |
2ad55c34ea | ||
![]() |
174902d58d | ||
![]() |
dfb0d0c382 | ||
![]() |
19f8c69527 | ||
![]() |
99d87fce66 | ||
![]() |
dcd8e1c6c5 | ||
![]() |
344772c1fa | ||
![]() |
6037b15589 | ||
![]() |
2d75c48332 | ||
![]() |
1b79c739d3 | ||
![]() |
4b9bb3b833 | ||
![]() |
d0ea2f7850 | ||
![]() |
2f4781bc5d | ||
![]() |
9a1be27e59 | ||
![]() |
2784252597 | ||
![]() |
32aa259790 | ||
![]() |
abbe1804e2 | ||
![]() |
8263e14bcc | ||
![]() |
9596339684 | ||
![]() |
3a08bc617e | ||
![]() |
6b6ef52bb4 | ||
![]() |
815c87b5e3 | ||
![]() |
5c7e0ae662 | ||
![]() |
794cd94db6 | ||
![]() |
0f9f6b6034 | ||
![]() |
f950229708 | ||
![]() |
920f34d454 | ||
![]() |
505df59204 | ||
![]() |
1bc7de32be | ||
![]() |
bbd257f07e | ||
![]() |
5b251b61e3 | ||
![]() |
2972ffb97f | ||
![]() |
3350ba61db | ||
![]() |
2e4c1b22ff | ||
![]() |
9a220790ef | ||
![]() |
dd336fab3f | ||
![]() |
217b17c279 | ||
![]() |
fdfafc9519 | ||
![]() |
0dd787a4e6 | ||
![]() |
fcff116d92 | ||
![]() |
8767adc505 | ||
![]() |
167f3f50bc | ||
![]() |
8c02d1e221 | ||
![]() |
40f9b6da92 | ||
![]() |
56959b98d7 | ||
![]() |
e6d8cca4fe | ||
![]() |
eaac28d4ee | ||
![]() |
801c344c6c | ||
![]() |
abd0890512 | ||
![]() |
43af45a6ed | ||
![]() |
42607a8e65 | ||
![]() |
83a94dbdff | ||
![]() |
e15b9fae16 | ||
![]() |
3e9b75d3e8 | ||
![]() |
6e9e5f552a | ||
![]() |
8d182f9362 | ||
![]() |
74bceaf1ee | ||
![]() |
cbb44f4ea6 | ||
![]() |
0a2d8396d6 | ||
![]() |
35cc2952fb | ||
![]() |
a3171399c0 | ||
![]() |
b40a4106d5 |
2
.babelrc
@ -8,7 +8,7 @@
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": [ "istanbul" ]
|
||||
"plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.git
|
||||
dist
|
||||
.history
|
||||
images
|
||||
docs
|
||||
Dockerfile
|
||||
README.md
|
||||
build.sh
|
5
.gitignore
vendored
@ -2,8 +2,9 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.history
|
||||
.idea
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.vscode
|
||||
stackedit_v4
|
||||
chrome-app/*.zip
|
||||
/test/unit/coverage/
|
||||
|
22
.travis.yml
Normal file
@ -0,0 +1,22 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "12"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_deploy:
|
||||
# Run docker build
|
||||
- docker build -t benweet/stackedit .
|
||||
# Install Helm
|
||||
- curl -SL -o /tmp/get_helm.sh https://git.io/get_helm.sh
|
||||
- chmod 700 /tmp/get_helm.sh
|
||||
- /tmp/get_helm.sh
|
||||
- helm init --client-only
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
script: bash build/deploy.sh
|
||||
on:
|
||||
tags: true
|
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM mafgwo/wkhtmltopdf-nodejs:11.15.0
|
||||
|
||||
WORKDIR /opt/stackedit
|
||||
|
||||
COPY package*json /opt/stackedit/
|
||||
COPY gulpfile.js /opt/stackedit/
|
||||
|
||||
RUN npm install --unsafe-perm \
|
||||
&& npm cache clean --force
|
||||
COPY . /opt/stackedit
|
||||
ENV NODE_ENV production
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "node", "." ]
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
186
README.md
@ -1,15 +1,187 @@
|
||||
# my-project
|
||||
<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 />
|
||||
|
||||
> A Vue.js project
|
||||
## 说明
|
||||
|
||||
## Build Setup
|
||||
本项目为本人clone修改自用,如果你也喜欢,请至原作者处获取及交流。
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
## 截图
|
||||
|
||||
**亮暗主题切换、编辑主题切换**
|
||||

|
||||
|
||||
**支持的文档空间**
|
||||

|
||||
|
||||
**拖拽粘贴上传图片**
|
||||

|
||||
|
||||
**支持文档搜索**
|
||||

|
||||
|
||||
**ChatGPT集成协助写作**
|
||||

|
||||
|
||||
## 相比国外开源版本的区别:
|
||||
|
||||
- 修复了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)
|
||||
|
||||
## 国外开源版本弊端:
|
||||
|
||||
- 作者已经不维护了或很少维护了
|
||||
- 不支持国内常用Gitee
|
||||
- 强依赖GoogleDrive,而Google Drive在国内不能正常访问
|
||||
|
||||
## 部署说明
|
||||
|
||||
> 建议docker-compose方式部署,其他部署方式如遇到问题欢迎提issue。
|
||||
|
||||
docker官方仓库下载太慢可以使用阿里云的镜像仓库,镜像仓库地址:registry.cn-hangzhou.aliyuncs.com/mafgwo/stackedit:【版本号】
|
||||
|
||||
`docker-compose.yml`如下:
|
||||
|
||||
```yaml
|
||||
version: "3.7"
|
||||
services:
|
||||
stackedit:
|
||||
image: mafgwo/stackedit:【docker中央仓库找到最新版本】
|
||||
container_name: stackedit
|
||||
environment:
|
||||
- LISTENING_PORT=8080
|
||||
- ROOT_URL=/
|
||||
- USER_BUCKET_NAME=root
|
||||
- DROPBOX_APP_KEY=【不需要支持则删掉】
|
||||
- DROPBOX_APP_KEY_FULL=【不需要支持则删掉】
|
||||
- GITHUB_CLIENT_ID=【不需要支持则删掉】
|
||||
- GITHUB_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GITEE_CLIENT_ID=【不需要支持则删掉】
|
||||
- GITEE_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GOOGLE_CLIENT_ID=【不需要支持则删掉】
|
||||
- GOOGLE_API_KEY=【不需要支持则删掉】
|
||||
- GITEA_CLIENT_ID=【不需要支持则删掉】
|
||||
- GITEA_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GITEA_URL=【不需要支持则删掉】
|
||||
- GITLAB_CLIENT_ID=【不需要支持则删掉】
|
||||
- GITLAB_CLIENT_SECRET=【不需要支持则删掉】
|
||||
- GITLAB_URL=【不需要支持则删掉】
|
||||
ports:
|
||||
- 8080:8080/tcp
|
||||
network_mode: bridge
|
||||
restart: always
|
||||
```
|
||||
|
||||
docker-compose方式的启动或停止命令
|
||||
|
||||
```bash
|
||||
# 在 docker-compose.yml 文件目录下 启动命令
|
||||
docker-compose up -d
|
||||
# 在 docker-compose.yml 文件目录下 停止命令
|
||||
docker-compose down
|
||||
# 更新镜像只需要修改docker-compose.yml中镜像版本执行再停止、启动命令即可
|
||||
```
|
||||
|
||||
或者可以直接通过Docker命名直接启动,命令如下:
|
||||
|
||||
```bash
|
||||
docker run -itd --name stackedit \
|
||||
-p 8080:8080 \
|
||||
-e LISTENING_PORT=8080 \
|
||||
-e ROOT_URL=/ \
|
||||
-e USER_BUCKET_NAME=root \
|
||||
-e DROPBOX_APP_KEY=【不需要支持则删掉】 \
|
||||
-e DROPBOX_APP_KEY_FULL=【不需要支持则删掉】 \
|
||||
-e GITHUB_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GITHUB_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GITEE_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GITEE_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GOOGLE_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GOOGLE_API_KEY=【不需要支持则删掉】 \
|
||||
-e GITEA_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GITEA_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GITEA_URL=【不需要支持则删掉】 \
|
||||
-e GITLAB_CLIENT_ID=【不需要支持则删掉】 \
|
||||
-e GITLAB_CLIENT_SECRET=【不需要支持则删掉】 \
|
||||
-e GITLAB_URL=【不需要支持则删掉】 \
|
||||
mafgwo/stackedit:【docker中央仓库找到最新版本】
|
||||
|
||||
```
|
||||
|
||||
## 如何创建三方平台应用
|
||||
|
||||
> 部署时,如果需要支持Gitee或GitHub,则需要自行到对应三方平台创建应用,获取到应用ID和秘钥,替换到以上的环境变量中,再启动应用。
|
||||
|
||||
- Gitee的环境变量:GITEE_CLIENT_ID、GITEE_CLIENT_SECRET,**[如何创建Gitee应用](./docs/部署之Gitee应用创建.md)**
|
||||
- GitHub的环境变量:GITHUB_CLIENT_ID、GITEE_CLIENT_SECRET,**[如何创建GitHub应用](./docs/部署之GitHub应用创建.md)**
|
||||
- Gitea可选择性配置环境变量(未配置则在关联时前端指定,有配置则仅允许配置的应用信息):GITEA_CLIENT_ID、GITEA_CLIENT_SECRET、GITEA_URL,**[如何创建Gitea应用](./docs/部署之Gitea应用创建.md)**
|
||||
- Gitlab可选择性配置环境变量(未配置则在关联时前端指定,有配置则仅允许配置的应用信息):GITLAB_CLIENT_ID、GITLAB_CLIENT_SECRET、GITLAB_URL **如何创建Gitlab应用(待补充文档)**
|
||||
|
||||
(特别说明:自建的Gitea、Gitlab要能接入stackedit必须支持跨域)
|
||||
|
||||
## 编译与运行
|
||||
|
||||
> 编译运行的nodejs版本选择11.15.0版本
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
npm run dev
|
||||
npm start
|
||||
|
||||
# build for production with minification
|
||||
npm run build
|
||||
@ -17,5 +189,3 @@ npm run build
|
||||
# build for production and view the bundle analyzer report
|
||||
npm run build --report
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
|
||||
|
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 "操作完成"
|
24
build/deploy.sh
Normal file
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Tag and push docker image
|
||||
docker login -u benweet -p "$DOCKER_PASSWORD"
|
||||
docker tag benweet/stackedit "benweet/stackedit:$TRAVIS_TAG"
|
||||
docker push benweet/stackedit:$TRAVIS_TAG
|
||||
docker tag benweet/stackedit:$TRAVIS_TAG benweet/stackedit:latest
|
||||
docker push benweet/stackedit:latest
|
||||
|
||||
# Build the chart
|
||||
cd "$TRAVIS_BUILD_DIR"
|
||||
npm run chart
|
||||
|
||||
# Add chart to helm repository
|
||||
git clone --branch master "https://benweet:$GITHUB_TOKEN@github.com/benweet/stackedit-charts.git" /tmp/charts
|
||||
cd /tmp/charts
|
||||
helm package "$TRAVIS_BUILD_DIR/dist/stackedit"
|
||||
helm repo index --url https://benweet.github.io/stackedit-charts/ .
|
||||
git config user.name "Benoit Schweblin"
|
||||
git config user.email "benoit.schweblin@gmail.com"
|
||||
git add .
|
||||
git commit -m "Added $TRAVIS_TAG"
|
||||
git push origin master
|
@ -1,9 +1,11 @@
|
||||
require('./check-versions')()
|
||||
|
||||
var config = require('../config')
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||
}
|
||||
Object.keys(config.dev.env).forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = JSON.parse(config.dev.env[key]);
|
||||
}
|
||||
});
|
||||
|
||||
var opn = require('opn')
|
||||
var path = require('path')
|
||||
|
@ -2,6 +2,7 @@ var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||
|
||||
@ -13,6 +14,10 @@ module.exports = {
|
||||
entry: {
|
||||
app: './src/'
|
||||
},
|
||||
node: {
|
||||
// For mermaid
|
||||
fs: 'empty' // jison generated code requires 'fs'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
@ -42,10 +47,34 @@ module.exports = {
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
// We can't pass graphlibrary to babel
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'string-replace-loader',
|
||||
include: [
|
||||
resolve('node_modules/graphlibrary')
|
||||
],
|
||||
options: {
|
||||
search: '^\\s*(?:let|const) ',
|
||||
replace: 'var ',
|
||||
flags: 'gm'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: [resolve('src'), resolve('test')]
|
||||
include: [
|
||||
resolve('src'),
|
||||
resolve('test'),
|
||||
resolve('node_modules/mermaid')
|
||||
],
|
||||
exclude: [
|
||||
resolve('node_modules/mermaid/src/diagrams/class/parser'),
|
||||
resolve('node_modules/mermaid/src/diagrams/flowchart/parser'),
|
||||
resolve('node_modules/mermaid/src/diagrams/gantt/parser'),
|
||||
resolve('node_modules/mermaid/src/diagrams/git/parser'),
|
||||
resolve('node_modules/mermaid/src/diagrams/sequence/parser')
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
@ -56,13 +85,9 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\.woff(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
@ -73,6 +98,7 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new StylelintPlugin({
|
||||
files: ['**/*.vue', '**/*.scss']
|
||||
}),
|
||||
|
@ -9,6 +9,12 @@ var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
var OfflinePlugin = require('offline-plugin');
|
||||
var WebpackPwaManifest = require('webpack-pwa-manifest')
|
||||
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
var env = config.build.env
|
||||
|
||||
@ -28,7 +34,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
NODE_ENV: env.NODE_ENV
|
||||
NODE_ENV: env.NODE_ENV,
|
||||
GOOGLE_CLIENT_ID: env.GOOGLE_CLIENT_ID,
|
||||
GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
@ -92,13 +100,31 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||
ignore: ['.*']
|
||||
}
|
||||
]),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: resolve('src/assets/favicon.png'),
|
||||
title: 'StackEdit',
|
||||
}),
|
||||
new WebpackPwaManifest({
|
||||
name: 'StackEdit',
|
||||
description: 'Full-featured, open-source Markdown editor',
|
||||
display: 'standalone',
|
||||
orientation: 'any',
|
||||
start_url: 'app',
|
||||
background_color: '#ffffff',
|
||||
crossorigin: 'use-credentials',
|
||||
icons: [{
|
||||
src: resolve('src/assets/favicon.png'),
|
||||
sizes: [96, 128, 192, 256, 384, 512]
|
||||
}]
|
||||
}),
|
||||
new OfflinePlugin({
|
||||
ServiceWorker: {
|
||||
events: true
|
||||
},
|
||||
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html'],
|
||||
externals: ['/app', '/oauth2/callback']
|
||||
})
|
||||
AppCache: true,
|
||||
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
|
||||
externals: ['/', '/app', '/oauth2/callback']
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
|
56
build/webpack.style.conf.js
Normal file
@ -0,0 +1,56 @@
|
||||
var path = require('path')
|
||||
var utils = require('./utils')
|
||||
var webpack = require('webpack')
|
||||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
style: './src/styles/'
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}]
|
||||
.concat(utils.styleLoaders({
|
||||
sourceMap: config.build.productionSourceMap,
|
||||
extract: true
|
||||
})),
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
publicPath: config.build.assetsPublicPath
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
sourceMap: true
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: '[name].css',
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
22
chart/.helmignore
Normal file
@ -0,0 +1,22 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
5
chart/Chart.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
appVersion: vSTACKEDIT_VERSION
|
||||
description: In-browser Markdown editor
|
||||
name: stackedit
|
||||
version: STACKEDIT_VERSION
|
21
chart/templates/NOTES.txt
Normal file
@ -0,0 +1,21 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stackedit.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "stackedit.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stackedit.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "stackedit.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:80
|
||||
{{- end }}
|
45
chart/templates/_helpers.tpl
Normal file
@ -0,0 +1,45 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "stackedit.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "stackedit.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "stackedit.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "stackedit.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "stackedit.name" . }}
|
||||
helm.sh/chart: {{ include "stackedit.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
87
chart/templates/deployment.yaml
Normal file
@ -0,0 +1,87 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "stackedit.fullname" . }}
|
||||
labels:
|
||||
{{ include "stackedit.labels" . | indent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "stackedit.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "stackedit.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
volumeMounts:
|
||||
- mountPath: /run
|
||||
name: run-volume
|
||||
- mountPath: /tmp
|
||||
name: tmp-volume
|
||||
env:
|
||||
- name: PORT
|
||||
value: "80"
|
||||
- name: PAYPAL_RECEIVER_EMAIL
|
||||
value: {{ .Values.paypalReceiverEmail }}
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
value: {{ .Values.awsAccessKeyId }}
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
value: {{ .Values.awsSecretAccessKey }}
|
||||
- name: DROPBOX_APP_KEY
|
||||
value: {{ .Values.dropboxAppKey }}
|
||||
- name: DROPBOX_APP_KEY_FULL
|
||||
value: {{ .Values.dropboxAppKeyFull }}
|
||||
- name: GOOGLE_CLIENT_ID
|
||||
value: {{ .Values.googleClientId }}
|
||||
- name: GOOGLE_API_KEY
|
||||
value: {{ .Values.googleApiKey }}
|
||||
- name: GITHUB_CLIENT_ID
|
||||
value: {{ .Values.githubClientId }}
|
||||
- name: GITHUB_CLIENT_SECRET
|
||||
value: {{ .Values.githubClientSecret }}
|
||||
- name: WORDPRESS_CLIENT_ID
|
||||
value: {{ .Values.wordpressClientId }}
|
||||
- name: WORDPRESS_SECRET
|
||||
value: {{ .Values.wordpressSecret }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumes:
|
||||
- name: run-volume
|
||||
emptyDir: {}
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
39
chart/templates/ingress.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "stackedit.fullname" . -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{ include "stackedit.labels" . | indent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
16
chart/templates/service.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "stackedit.fullname" . }}
|
||||
labels:
|
||||
{{ include "stackedit.labels" . | indent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "stackedit.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
15
chart/templates/tests/test-connection.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "stackedit.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{ include "stackedit.labels" . | indent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "stackedit.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
71
chart/values.yaml
Normal file
@ -0,0 +1,71 @@
|
||||
# Default values for stackedit.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
dropboxAppKey: ""
|
||||
dropboxAppKeyFull: ""
|
||||
googleClientId: ""
|
||||
googleApiKey: ""
|
||||
githubClientId: ""
|
||||
githubClientSecret: ""
|
||||
giteeClientId: ""
|
||||
giteeClientSecret: ""
|
||||
wordpressClientId: ""
|
||||
wordpressSecret: ""
|
||||
paypalReceiverEmail: ""
|
||||
awsAccessKeyId: ""
|
||||
awsSecretAccessKey: ""
|
||||
giteaClientId: ""
|
||||
giteaClientSecret: ""
|
||||
giteaUrl: ""
|
||||
gitlabClientId: ""
|
||||
gitlabUrl: ""
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: benweet/stackedit
|
||||
tag: vSTACKEDIT_VERSION
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations:
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# certmanager.k8s.io/issuer: letsencrypt-prod
|
||||
# certmanager.k8s.io/acme-challenge-type: http01
|
||||
hosts: []
|
||||
# - host: stackedit.example.com
|
||||
# paths:
|
||||
# - /
|
||||
|
||||
tls: []
|
||||
# - secretName: stackedit-tls
|
||||
# hosts:
|
||||
# - stackedit.example.com
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
BIN
chrome-app/icon-128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
chrome-app/icon-16.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
chrome-app/icon-256.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
chrome-app/icon-32.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
chrome-app/icon-512.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
chrome-app/icon-64.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
28
chrome-app/manifest.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "StackEdit中文版",
|
||||
"description": "支持Gitee仓库/粘贴图片自动上传的浏览器内 Markdown 编辑器",
|
||||
"version": "5.15.17",
|
||||
"manifest_version": 2,
|
||||
"container": "GITEE",
|
||||
"api_console_project_id": "241271498917",
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"64": "icon-64.png",
|
||||
"128": "icon-128.png",
|
||||
"256": "icon-256.png",
|
||||
"512": "icon-512.png"
|
||||
},
|
||||
"app": {
|
||||
"urls": [
|
||||
"https://md.jonylee.top/"
|
||||
],
|
||||
"launch": {
|
||||
"web_url": "https://md.jonylee.top/app"
|
||||
}
|
||||
},
|
||||
"offline_enabled": true,
|
||||
"permissions": [
|
||||
"unlimitedStorage"
|
||||
]
|
||||
}
|
@ -2,5 +2,17 @@ var merge = require('webpack-merge')
|
||||
var prodEnv = require('./prod.env')
|
||||
|
||||
module.exports = merge(prodEnv, {
|
||||
NODE_ENV: '"development"'
|
||||
NODE_ENV: '"development"',
|
||||
// 以下配置是开发临时用的配置 随时可能失效 请替换为自己的
|
||||
GITHUB_CLIENT_ID: '"845b8f75df48f2ee0563"',
|
||||
GITHUB_CLIENT_SECRET: '"80df676597abded1450926861965cc3f9bead6a0"',
|
||||
GITEE_CLIENT_ID: '"925ba7c78b85dec984f7877e4aca5cab10ae333c6d68e761bdb0b9dfb8f55672"',
|
||||
GITEE_CLIENT_SECRET: '"f05731066e42d307339dc8ebbb037a103881dafc7207a359a393b87749f1c562"',
|
||||
CLIENT_ID: '"thF3qCGLN39OtafjGnqHyj6n02WwE6xD"',
|
||||
// GITEA_CLIENT_ID: '"fe30f8f9-b1e8-4531-8f72-c1a5d3912805"',
|
||||
// GITEA_CLIENT_SECRET: '"lus7oMnb3H6M1hsChndphArE20Txr7erwJLf7SDBQWTw"',
|
||||
// GITEA_URL: '"https://gitea.test.com"',
|
||||
GITLAB_CLIENT_ID: '"074cd5103c62dea0f479dac861039656ac80935e304c8113a02cc64c629496ae"',
|
||||
GITLAB_CLIENT_SECRET: '"6f406f24216b686d55d28313dec1913c2a8e599afdb08380d5e8ce838e16e41e"',
|
||||
GITLAB_URL: '"http://gitlab.qicoder.com"',
|
||||
})
|
@ -23,8 +23,8 @@ module.exports = {
|
||||
},
|
||||
dev: {
|
||||
env: require('./dev.env'),
|
||||
port: 8080,
|
||||
autoOpenBrowser: true,
|
||||
port: 80,
|
||||
autoOpenBrowser: false,
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/',
|
||||
proxyTable: {},
|
||||
@ -33,6 +33,7 @@ module.exports = {
|
||||
// (https://github.com/webpack/css-loader#sourcemaps)
|
||||
// In our experience, they generally work as expected,
|
||||
// just be aware of this issue when enabling this option.
|
||||
cssSourceMap: false
|
||||
// cssSourceMap: false
|
||||
cssSourceMap: true
|
||||
}
|
||||
}
|
||||
|
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应用
|
||||
|
||||
按下面图的指示创建
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
35
docs/部署之Gitea应用创建.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Gitea应用配置说明
|
||||
|
||||
> StackEdit中文版支持Gitea,则需要到Gitea创建一个应用,在StackEdit中文版绑定Gitea账号的时候填入。
|
||||
|
||||
|
||||
# 如何创建Gitea应用
|
||||
|
||||
按下面图的指示创建
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
创建成功后即可看到应用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应用
|
||||
|
||||
按下面图的指示创建
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
创建成功后即可看到client id 和 client secret。
|
@ -10,6 +10,7 @@ const prismScripts = [
|
||||
'prismjs/components/prism-javascript',
|
||||
'prismjs/components/prism-css',
|
||||
'prismjs/components/prism-ruby',
|
||||
'prismjs/components/prism-cpp',
|
||||
].map(require.resolve);
|
||||
prismScripts.push(
|
||||
path.join(path.dirname(require.resolve('prismjs/components/prism-core')), 'prism-!(*.min).js'));
|
||||
|
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 |
33
index.html
@ -1,22 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>StackEdit</title>
|
||||
<link rel="canonical" href="https://stackedit.io/app">
|
||||
<!-- <link rel="icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
|
||||
<!-- <link rel="icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
|
||||
<!-- <link rel="shortcut icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
|
||||
<!-- <link rel="shortcut icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
|
||||
<!-- <link rel="apple-touch-icon-precomposed" sizes="152x152" href="res-min/img/logo-ipad-retina.png"> -->
|
||||
<meta name="description" content="Free, open-source, full-featured Markdown editor.">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
<!-- baidu统计 -->
|
||||
<script>
|
||||
var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?dad4b4383b13eedea1ab45ee323df1c3";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
</script>
|
||||
<!-- baidu统计结束 -->
|
||||
</body>
|
||||
|
||||
</html>
|
34
index.js
@ -1,29 +1,27 @@
|
||||
process.env.NODE_ENV = 'production';
|
||||
const env = require('./config/prod.env');
|
||||
|
||||
Object.keys(env).forEach((key) => {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = JSON.parse(env[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
|
||||
require('./server')(app, process.env.SERVE_V4);
|
||||
require('./server')(app);
|
||||
|
||||
let port = parseInt(process.env.PORT || 8080, 10);
|
||||
if (port === 443) {
|
||||
const credentials = {
|
||||
key: fs.readFileSync(path.join(__dirname, 'ssl.key'), 'utf8'),
|
||||
cert: fs.readFileSync(path.join(__dirname, 'ssl.crt'), 'utf8'),
|
||||
ca: fs.readFileSync(path.join(__dirname, 'ssl.ca'), 'utf8').split('\n\n'),
|
||||
};
|
||||
const httpsServer = https.createServer(credentials, app);
|
||||
httpsServer.listen(port, null, () => {
|
||||
console.log('HTTPS server started: https://localhost');
|
||||
});
|
||||
port = 80;
|
||||
}
|
||||
const port = parseInt(process.env.PORT || 8080, 10);
|
||||
const httpServer = http.createServer(app);
|
||||
httpServer.listen(port, null, () => {
|
||||
console.log(`HTTP server started: http://localhost:${port}`);
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
httpServer.close(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
22028
package-lock.json
generated
Normal file
143
package.json
@ -1,116 +1,141 @@
|
||||
{
|
||||
"name": "stackedit",
|
||||
"version": "5.0.4",
|
||||
"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/benweet/stackedit/issues"
|
||||
"url": "https://github.com/mafgwo/stackedit/issues"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"postinstall": "gulp build-prism",
|
||||
"start": "node build/dev-server.js",
|
||||
"build": "node build/build.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"preversion": "npm run lint",
|
||||
"build": "node build/build.js && npm run build-style",
|
||||
"build-style": "webpack --config build/webpack.style.conf.js",
|
||||
"lint": "eslint --ext .js,.vue src server",
|
||||
"unit": "jest --config test/unit/jest.conf.js --runInBand",
|
||||
"unit-with-coverage": "jest --config test/unit/jest.conf.js --runInBand --coverage",
|
||||
"test": "npm run lint && npm run unit",
|
||||
"preversion": "npm run test",
|
||||
"postversion": "git push origin master --tags && npm publish",
|
||||
"patch": "npm version patch -m \"Tag v%s\"",
|
||||
"minor": "npm version minor -m \"Tag v%s\"",
|
||||
"major": "npm version major -m \"Tag v%s\""
|
||||
"major": "npm version major -m \"Tag v%s\"",
|
||||
"chart": "mkdir -p dist && rm -rf dist/stackedit && cp -r chart dist/stackedit && sed -i.bak -e s/STACKEDIT_VERSION/$npm_package_version/g dist/stackedit/*.yaml && rm dist/stackedit/*.yaml.bak"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/test-utils": "^1.0.0-beta.16",
|
||||
"abcjs": "^5.2.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bezier-easing": "^1.1.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"clipboard": "^1.7.1",
|
||||
"clunderscore": "^1.0.3",
|
||||
"compression": "^1.7.0",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"file-saver": "^1.3.8",
|
||||
"handlebars": "^4.0.10",
|
||||
"indexeddbshim": "^3.0.4",
|
||||
"js-yaml": "^3.9.1",
|
||||
"katex": "^0.7.1",
|
||||
"markdown-it": "^8.3.1",
|
||||
"indexeddbshim": "^3.6.2",
|
||||
"js-yaml": "^3.11.0",
|
||||
"katex": "^0.16.2",
|
||||
"markdown-it": "^8.4.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-deflist": "^2.0.2",
|
||||
"markdown-it-emoji": "^1.3.0",
|
||||
"markdown-it-footnote": "^3.0.1",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
"markdown-it-mark": "^2.0.0",
|
||||
"markdown-it-pandoc-renderer": "1.1.3",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"mermaid": "^8.9.2",
|
||||
"mousetrap": "^1.6.1",
|
||||
"normalize-scss": "^7.0.0",
|
||||
"normalize-scss": "^7.0.1",
|
||||
"prismjs": "^1.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"request": "^2.82.0",
|
||||
"serve-static": "^1.12.6",
|
||||
"vue": "^2.3.3",
|
||||
"vuex": "^2.3.1"
|
||||
"request": "^2.85.0",
|
||||
"serve-static": "^1.13.2",
|
||||
"tmp": "^0.0.33",
|
||||
"turndown": "^7.1.1",
|
||||
"vue": "^2.5.16",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.7.2",
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-jest": "^21.0.2",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-dynamic-import-node": "^1.2.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"babel-preset-env": "^1.3.2",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-stage-2": "^6.22.0",
|
||||
"babel-register": "^6.22.0",
|
||||
"chalk": "^1.1.3",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb-base": "^11.1.3",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
"eslint-import-resolver-webpack": "^0.8.1",
|
||||
"eslint-loader": "^1.7.1",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-import-resolver-webpack": "^0.9.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-html": "^4.0.3",
|
||||
"eslint-plugin-import": "^2.11.0",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.15.5",
|
||||
"express": "^4.16.3",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-bump": "^2.7.0",
|
||||
"favicons-webpack-plugin": "^0.0.9",
|
||||
"file-loader": "^1.1.11",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-util": "^3.0.8",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"http-proxy-middleware": "^0.17.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-proxy-middleware": "^0.18.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"node-sass": "^4.5.3",
|
||||
"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": "^4.8.4",
|
||||
"offline-plugin": "^5.0.3",
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.2",
|
||||
"ora": "^1.2.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"replace-in-file": "^4.1.0",
|
||||
"rimraf": "^2.6.0",
|
||||
"sass-loader": "^6.0.5",
|
||||
"semver": "^5.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"sass-loader": "^7.0.1",
|
||||
"semver": "^5.5.0",
|
||||
"shelljs": "^0.8.1",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-standard": "^16.0.0",
|
||||
"stylelint-processor-html": "^1.0.0",
|
||||
"stylelint-webpack-plugin": "^0.7.0",
|
||||
"url-loader": "^0.5.8",
|
||||
"vue-loader": "^12.1.0",
|
||||
"vue-style-loader": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.3",
|
||||
"stylelint-webpack-plugin": "^0.10.4",
|
||||
"url-loader": "^1.0.1",
|
||||
"vue-jest": "^1.0.2",
|
||||
"vue-loader": "^15.0.9",
|
||||
"vue-style-loader": "^4.1.0",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.18.0",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"worker-loader": "^0.8.1"
|
||||
"webpack-merge": "^4.1.2",
|
||||
"webpack-pwa-manifest": "^3.7.1",
|
||||
"worker-loader": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
"node": ">= 8.0.0",
|
||||
"npm": ">= 5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
"not ie <= 10"
|
||||
]
|
||||
}
|
||||
|
54
server/conf.js
Normal file
@ -0,0 +1,54 @@
|
||||
const pandocPath = process.env.PANDOC_PATH || 'pandoc';
|
||||
const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||
const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;
|
||||
|
||||
const dropboxAppKey = process.env.DROPBOX_APP_KEY;
|
||||
const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
|
||||
const githubClientId = process.env.GITHUB_CLIENT_ID;
|
||||
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
|
||||
const giteeClientId = process.env.GITEE_CLIENT_ID;
|
||||
const giteeClientSecret = process.env.GITEE_CLIENT_SECRET;
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const googleApiKey = process.env.GOOGLE_API_KEY;
|
||||
const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;
|
||||
const giteaClientId = process.env.GITEA_CLIENT_ID;
|
||||
const giteaClientSecret = process.env.GITEA_CLIENT_SECRET;
|
||||
const giteaUrl = process.env.GITEA_URL;
|
||||
const gitlabClientId = process.env.GITLAB_CLIENT_ID;
|
||||
const gitlabClientSecret = process.env.GITLAB_CLIENT_SECRET;
|
||||
const gitlabUrl = process.env.GITLAB_URL;
|
||||
|
||||
exports.values = {
|
||||
pandocPath,
|
||||
wkhtmltopdfPath,
|
||||
paypalReceiverEmail,
|
||||
dropboxAppKey,
|
||||
dropboxAppKeyFull,
|
||||
githubClientId,
|
||||
githubClientSecret,
|
||||
giteeClientId,
|
||||
giteeClientSecret,
|
||||
googleClientId,
|
||||
googleApiKey,
|
||||
wordpressClientId,
|
||||
giteaClientId,
|
||||
giteaClientSecret,
|
||||
giteaUrl,
|
||||
gitlabClientId,
|
||||
gitlabClientSecret,
|
||||
gitlabUrl,
|
||||
};
|
||||
|
||||
exports.publicValues = {
|
||||
dropboxAppKey,
|
||||
dropboxAppKeyFull,
|
||||
githubClientId,
|
||||
googleClientId,
|
||||
googleApiKey,
|
||||
wordpressClientId,
|
||||
allowSponsorship: !!paypalReceiverEmail,
|
||||
giteaClientId,
|
||||
giteaUrl,
|
||||
gitlabClientId,
|
||||
gitlabUrl,
|
||||
};
|
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'),
|
||||
);
|
||||
};
|
42
server/gitee.js
Normal file
@ -0,0 +1,42 @@
|
||||
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
|
||||
const request = require('request');
|
||||
const conf = require('./conf');
|
||||
|
||||
function giteeToken(clientId, code, oauth2RedirectUri) {
|
||||
const clientIndex = conf.values.giteeClientId.split(',').indexOf(clientId);
|
||||
const clientSecret = conf.values.giteeClientSecret.split(',')[clientIndex];
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'https://gitee.com/oauth/token',
|
||||
form: {
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: oauth2RedirectUri,
|
||||
},
|
||||
json: true
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
const token = body.access_token;
|
||||
if (token) {
|
||||
resolve(body);
|
||||
} else {
|
||||
reject(res.statusCode + ',body:' + JSON.stringify(body));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exports.giteeToken = (req, res) => {
|
||||
giteeToken(req.query.clientId, req.query.code, req.query.oauth2RedirectUri)
|
||||
.then(
|
||||
tokenBody => res.send(tokenBody),
|
||||
err => res
|
||||
.status(400)
|
||||
.send(err ? err.message || err.toString() : 'bad_code'),
|
||||
);
|
||||
};
|
@ -1,21 +1,22 @@
|
||||
var qs = require('qs');
|
||||
var request = require('request');
|
||||
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
|
||||
const request = require('request');
|
||||
const conf = require('./conf');
|
||||
|
||||
function githubToken(clientId, code) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'https://github.com/login/oauth/access_token',
|
||||
qs: {
|
||||
client_id: clientId,
|
||||
client_secret: process.env.GITHUB_SECRET,
|
||||
code: code
|
||||
client_secret: conf.values.githubClientSecret,
|
||||
code,
|
||||
},
|
||||
}, function(err, res, body) {
|
||||
}, (err, res, body) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
var token = qs.parse(body).access_token;
|
||||
const token = qs.parse(body).access_token;
|
||||
if (token) {
|
||||
resolve(token);
|
||||
} else {
|
||||
@ -25,11 +26,12 @@ function githubToken(clientId, code) {
|
||||
});
|
||||
}
|
||||
|
||||
exports.githubToken = function (req, res) {
|
||||
exports.githubToken = (req, res) => {
|
||||
githubToken(req.query.clientId, req.query.code)
|
||||
.then(function (token) {
|
||||
res.send(token);
|
||||
}, function (err) {
|
||||
res.status(400).send(err ? err.message || err.toString() : 'bad_code');
|
||||
});
|
||||
.then(
|
||||
token => res.send(token),
|
||||
err => res
|
||||
.status(400)
|
||||
.send(err ? err.message || err.toString() : 'bad_code'),
|
||||
);
|
||||
};
|
||||
|
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'),
|
||||
);
|
||||
};
|
120
server/index.js
@ -1,69 +1,89 @@
|
||||
var compression = require('compression');
|
||||
var serveStatic = require('serve-static');
|
||||
var path = require('path');
|
||||
const compression = require('compression');
|
||||
const serveStatic = require('serve-static');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
const github = require('./github');
|
||||
const gitee = require('./gitee');
|
||||
const gitea = require('./gitea');
|
||||
const gitlab = require('./gitlab');
|
||||
const pdf = require('./pdf');
|
||||
const pandoc = require('./pandoc');
|
||||
const conf = require('./conf');
|
||||
|
||||
module.exports = function (app, serveV4) {
|
||||
// Use gzip compression
|
||||
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
|
||||
|
||||
module.exports = (app) => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.all('*', function(req, res, next) {
|
||||
// Force HTTPS on stackedit.io
|
||||
if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
|
||||
return res.redirect('https://stackedit.io' + req.url);
|
||||
}
|
||||
// Enable CORS for fonts
|
||||
if (/\.(eot|ttf|woff|svg)$/.test(req.url)) {
|
||||
// Enable CORS for fonts
|
||||
app.all('*', (req, res, next) => {
|
||||
if (/\.(eot|ttf|woff2?|svg)$/.test(req.url)) {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Use gzip compression
|
||||
app.use(compression());
|
||||
}
|
||||
|
||||
app.get('/oauth2/githubToken', require('./github').githubToken);
|
||||
if (serveV4) {
|
||||
app.post('/pdfExport', require('../stackedit_v4/app/pdf').export);
|
||||
app.post('/sshPublish', require('../stackedit_v4/app/ssh').publish);
|
||||
app.post('/picasaImportImg', require('../stackedit_v4/app/picasa').importImg);
|
||||
app.get('/downloadImport', require('../stackedit_v4/app/download').importPublic);
|
||||
}
|
||||
|
||||
// Serve callback.html in /app
|
||||
app.get('/oauth2/callback', function(req, res) {
|
||||
res.sendFile(path.join(__dirname, '../static/oauth2/callback.html'));
|
||||
app.get('/oauth2/githubToken', github.githubToken);
|
||||
app.get('/oauth2/giteeToken', gitee.giteeToken);
|
||||
app.get('/oauth2/giteaToken', gitea.giteaToken);
|
||||
app.get('/oauth2/gitlabToken', gitlab.gitlabToken);
|
||||
app.get('/conf', (req, res) => res.send(conf.publicValues));
|
||||
app.post('/pdfExport', pdf.generate);
|
||||
app.post('/pandocExport', pandoc.generate);
|
||||
app.get('/giteeClientId', (req, res) => {
|
||||
const giteeClientIds = conf.values.giteeClientId.split(',');
|
||||
// 仅一个 则直接返回
|
||||
if (giteeClientIds.length === 1) {
|
||||
res.send(giteeClientIds[0]);
|
||||
return;
|
||||
}
|
||||
// 是否随机一个clientId 默认第一个 如果random 为1 则使用随机 避免单个应用接口次数用满无法自动切换其他应用
|
||||
const random = req.query.random;
|
||||
if (!random) {
|
||||
res.send(giteeClientIds[0]);
|
||||
return;
|
||||
}
|
||||
// 随机一个 排除第一个 因为第一个应用接口次数用完了
|
||||
const clientId = giteeClientIds[Math.floor(((giteeClientIds.length - 1) * Math.random())) + 1];
|
||||
res.send(clientId);
|
||||
});
|
||||
// Serve landing.html
|
||||
app.get('/', (req, res) => res.sendFile(resolvePath('static/landing/index.html')));
|
||||
// Serve privacy_policy.html
|
||||
app.get('/privacy_policy.html', (req, res) => res.sendFile(resolvePath('static/landing/privacy_policy.html')));
|
||||
// Serve sitemap.xml
|
||||
app.get('/sitemap.xml', (req, res) => res.sendFile(resolvePath('static/sitemap.xml')));
|
||||
// Serve callback.html
|
||||
app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
|
||||
// Google Drive action receiver
|
||||
app.get('/googleDriveAction', (req, res) =>
|
||||
res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));
|
||||
// Serve the static folder with 30 day max-age
|
||||
app.use('/themes', serveStatic(resolvePath('static/themes'), {
|
||||
maxAge: '5d',
|
||||
}));
|
||||
|
||||
// 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') {
|
||||
if (serveV4) {
|
||||
// Serve landing.html in /
|
||||
app.get('/', function(req, res) {
|
||||
res.sendFile(require.resolve('../stackedit_v4/views/landing.html'));
|
||||
});
|
||||
// Serve editor.html in /viewer
|
||||
app.get('/editor', function(req, res) {
|
||||
res.sendFile(require.resolve('../stackedit_v4/views/editor.html'));
|
||||
});
|
||||
// Serve viewer.html in /viewer
|
||||
app.get('/viewer', function(req, res) {
|
||||
res.sendFile(require.resolve('../stackedit_v4/views/viewer.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Serve index.html in /app
|
||||
app.get('/app', function(req, res) {
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
||||
});
|
||||
app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
|
||||
|
||||
app.use(serveStatic(path.join(__dirname, '../dist')));
|
||||
// Serve the static folder with 1 year max-age
|
||||
app.use('/static', serveStatic(resolvePath('dist/static'), {
|
||||
maxAge: '1y',
|
||||
}));
|
||||
|
||||
if (serveV4) {
|
||||
app.use(serveStatic(path.dirname(require.resolve('../stackedit_v4/public/cache.manifest'))));
|
||||
|
||||
// Error 404
|
||||
app.use(function(req, res) {
|
||||
res.status(404).sendFile(require.resolve('../stackedit_v4/views/error_404.html'));
|
||||
});
|
||||
}
|
||||
app.use(serveStatic(resolvePath('dist')));
|
||||
}
|
||||
};
|
||||
|
141
server/pandoc.js
Normal file
@ -0,0 +1,141 @@
|
||||
/* global window */
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const tmp = require('tmp');
|
||||
const conf = require('./conf');
|
||||
|
||||
const outputFormats = {
|
||||
asciidoc: 'text/plain',
|
||||
context: 'application/x-latex',
|
||||
epub: 'application/epub+zip',
|
||||
epub3: 'application/epub+zip',
|
||||
latex: 'application/x-latex',
|
||||
odt: 'application/vnd.oasis.opendocument.text',
|
||||
pdf: 'application/pdf',
|
||||
rst: 'text/plain',
|
||||
rtf: 'application/rtf',
|
||||
textile: 'text/plain',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
};
|
||||
|
||||
const highlightStyles = [
|
||||
'pygments',
|
||||
'kate',
|
||||
'monochrome',
|
||||
'espresso',
|
||||
'zenburn',
|
||||
'haddock',
|
||||
'tango',
|
||||
];
|
||||
|
||||
const readJson = (str) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
exports.generate = (req, res) => {
|
||||
let pandocError = '';
|
||||
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
|
||||
? req.query.format
|
||||
: 'pdf';
|
||||
new Promise((resolve, reject) => {
|
||||
tmp.file({
|
||||
postfix: `.${outputFormat}`,
|
||||
}, (err, filePath, fd, cleanupCallback) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
filePath,
|
||||
cleanupCallback,
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
|
||||
const options = readJson(req.query.options);
|
||||
const metadata = readJson(req.query.metadata);
|
||||
const params = [];
|
||||
|
||||
params.push('--pdf-engine=xelatex');
|
||||
params.push('--webtex=http://chart.apis.google.com/chart?cht=tx&chf=bg,s,FFFFFF00&chco=000000&chl=');
|
||||
if (options.toc) {
|
||||
params.push('--toc');
|
||||
}
|
||||
options.tocDepth = parseInt(options.tocDepth, 10);
|
||||
if (!Number.isNaN(options.tocDepth)) {
|
||||
params.push('--toc-depth', options.tocDepth);
|
||||
}
|
||||
options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
|
||||
params.push('--highlight-style', options.highlightStyle);
|
||||
Object.keys(metadata).forEach((key) => {
|
||||
params.push('-M', `${key}=${metadata[key]}`);
|
||||
});
|
||||
|
||||
let finished = false;
|
||||
|
||||
function onError(error) {
|
||||
finished = true;
|
||||
cleanupCallback();
|
||||
reject(error);
|
||||
}
|
||||
|
||||
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
|
||||
params.push('-f', 'json', '-t', format, '-o', filePath);
|
||||
const pandoc = spawn(conf.values.pandocPath, params, {
|
||||
stdio: [
|
||||
'pipe',
|
||||
'ignore',
|
||||
'pipe',
|
||||
],
|
||||
});
|
||||
let timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
pandoc.kill();
|
||||
}, 50000);
|
||||
pandoc.on('error', onError);
|
||||
pandoc.stdin.on('error', onError);
|
||||
pandoc.stderr.on('data', (data) => {
|
||||
pandocError += `${data}`;
|
||||
});
|
||||
pandoc.on('close', (code) => {
|
||||
if (!finished) {
|
||||
clearTimeout(timeoutId);
|
||||
if (!timeoutId) {
|
||||
res.statusCode = 408;
|
||||
cleanupCallback();
|
||||
reject(new Error('timeout'));
|
||||
} else if (code) {
|
||||
cleanupCallback();
|
||||
reject();
|
||||
} else {
|
||||
res.set('Content-Type', outputFormats[outputFormat]);
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
readStream.on('open', () => readStream.pipe(res));
|
||||
readStream.on('close', () => cleanupCallback());
|
||||
readStream.on('error', () => {
|
||||
cleanupCallback();
|
||||
reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
req.pipe(pandoc.stdin);
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
const message = err && err.message;
|
||||
if (message === 'unauthorized') {
|
||||
res.statusCode = 401;
|
||||
res.end('Unauthorized.');
|
||||
} else if (message === 'timeout') {
|
||||
res.statusCode = 408;
|
||||
res.end('Request timeout.');
|
||||
} else {
|
||||
res.statusCode = 400;
|
||||
res.end(pandocError || 'Unknown error.');
|
||||
}
|
||||
});
|
||||
};
|
307
server/pdf.js
@ -1,145 +1,178 @@
|
||||
/* global window,MathJax */
|
||||
var spawn = require('child_process').spawn;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var os = require('os');
|
||||
var request = require('request');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const tmp = require('tmp');
|
||||
const conf = require('./conf');
|
||||
|
||||
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
||||
function waitForJavaScript() {
|
||||
if(window.MathJax) {
|
||||
// Amazon EC2: fix TeX font detection
|
||||
MathJax.Hub.Register.StartupHook("HTML-CSS Jax Startup",function () {
|
||||
var HTMLCSS = MathJax.OutputJax["HTML-CSS"];
|
||||
HTMLCSS.Font.checkWebFont = function (check,font,callback) {
|
||||
if (check.time(callback)) {
|
||||
return;
|
||||
}
|
||||
if (check.total === 0) {
|
||||
HTMLCSS.Font.testFont(font);
|
||||
setTimeout(check,200);
|
||||
} else {
|
||||
callback(check.STATUS.OK);
|
||||
}
|
||||
};
|
||||
});
|
||||
MathJax.Hub.Queue(function () {
|
||||
window.status = 'done';
|
||||
});
|
||||
}
|
||||
else {
|
||||
setTimeout(function() {
|
||||
window.status = 'done';
|
||||
}, 2000);
|
||||
}
|
||||
if (window.MathJax) {
|
||||
// Amazon EC2: fix TeX font detection
|
||||
MathJax.Hub.Register.StartupHook('HTML-CSS Jax Startup', function () {
|
||||
var htmlCss = MathJax.OutputJax['HTML-CSS'];
|
||||
htmlCss.Font.checkWebFont = function (check, font, callback) {
|
||||
if (check.time(callback)) {
|
||||
return;
|
||||
}
|
||||
if (check.total === 0) {
|
||||
htmlCss.Font.testFont(font);
|
||||
setTimeout(check, 200);
|
||||
} else {
|
||||
callback(check.STATUS.OK);
|
||||
}
|
||||
};
|
||||
});
|
||||
MathJax.Hub.Queue(function () {
|
||||
window.status = 'done';
|
||||
});
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
window.status = 'done';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
||||
|
||||
var authorizedPageSizes = [
|
||||
'A3',
|
||||
'A4',
|
||||
'Legal',
|
||||
'Letter'
|
||||
const authorizedPageSizes = [
|
||||
'A3',
|
||||
'A4',
|
||||
'Legal',
|
||||
'Letter',
|
||||
];
|
||||
|
||||
exports.export = function(req, res, next) {
|
||||
function onError(err) {
|
||||
next(err);
|
||||
}
|
||||
function onUnknownError() {
|
||||
res.statusCode = 400;
|
||||
res.end('Unknown error');
|
||||
}
|
||||
function onUnauthorizedError() {
|
||||
res.statusCode = 401;
|
||||
res.end('Unauthorized');
|
||||
}
|
||||
function onTimeout() {
|
||||
res.statusCode = 408;
|
||||
res.end('Request timeout');
|
||||
}
|
||||
request({
|
||||
uri: 'https://monetizejs.com/api/payments',
|
||||
qs: {
|
||||
access_token: req.query.token
|
||||
},
|
||||
json: true
|
||||
}, function (err, paymentsRes, payments) {
|
||||
var authorized = payments && payments.app == 'ESTHdCYOi18iLhhO' && (
|
||||
(payments.chargeOption && payments.chargeOption.alias == 'once') ||
|
||||
(payments.subscriptionOption && payments.subscriptionOption.alias == 'yearly'));
|
||||
if(err || paymentsRes.statusCode != 200 || !authorized) {
|
||||
return onUnauthorizedError();
|
||||
}
|
||||
var options, params = [];
|
||||
try {
|
||||
options = JSON.parse(req.query.options);
|
||||
}
|
||||
catch(e) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
// Margins
|
||||
var marginTop = parseInt(options.marginTop);
|
||||
params.push('-T', isNaN(marginTop) ? 25 : marginTop);
|
||||
var marginRight = parseInt(options.marginRight);
|
||||
params.push('-R', isNaN(marginRight) ? 25 : marginRight);
|
||||
var marginBottom = parseInt(options.marginBottom);
|
||||
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom);
|
||||
var marginLeft = parseInt(options.marginLeft);
|
||||
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft);
|
||||
|
||||
// Header
|
||||
options.headerCenter && params.push('--header-center', options.headerCenter);
|
||||
options.headerLeft && params.push('--header-left', options.headerLeft);
|
||||
options.headerRight && params.push('--header-right', options.headerRight);
|
||||
options.headerFontName && params.push('--header-font-name', options.headerFontName);
|
||||
options.headerFontSize && params.push('--header-font-size', options.headerFontSize);
|
||||
|
||||
// Footer
|
||||
options.footerCenter && params.push('--footer-center', options.footerCenter);
|
||||
options.footerLeft && params.push('--footer-left', options.footerLeft);
|
||||
options.footerRight && params.push('--footer-right', options.footerRight);
|
||||
options.footerFontName && params.push('--footer-font-name', options.footerFontName);
|
||||
options.footerFontSize && params.push('--footer-font-size', options.footerFontSize);
|
||||
|
||||
// Page size
|
||||
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize);
|
||||
|
||||
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||
var filePath = path.join(os.tmpDir(), Date.now() + '.pdf');
|
||||
var binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||
params.push('--run-script', waitForJavaScript.toString() + 'waitForJavaScript()');
|
||||
params.push('--window-status', 'done');
|
||||
var wkhtmltopdf = spawn(binPath, params.concat('-', filePath), {
|
||||
stdio: [
|
||||
'pipe',
|
||||
'ignore',
|
||||
'ignore'
|
||||
]
|
||||
});
|
||||
var timeoutId = setTimeout(function() {
|
||||
timeoutId = undefined;
|
||||
wkhtmltopdf.kill();
|
||||
}, 30000);
|
||||
wkhtmltopdf.on('error', onError);
|
||||
wkhtmltopdf.stdin.on('error', onError);
|
||||
wkhtmltopdf.on('close', function(code) {
|
||||
if(!timeoutId) {
|
||||
return onTimeout();
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
if(code) {
|
||||
return onUnknownError();
|
||||
}
|
||||
var readStream = fs.createReadStream(filePath);
|
||||
readStream.on('open', function() {
|
||||
readStream.pipe(res);
|
||||
});
|
||||
readStream.on('close', function() {
|
||||
fs.unlink(filePath, function() {
|
||||
});
|
||||
});
|
||||
readStream.on('error', onUnknownError);
|
||||
});
|
||||
req.pipe(wkhtmltopdf.stdin);
|
||||
});
|
||||
const readJson = (str) => {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
exports.generate = (req, res) => {
|
||||
let wkhtmltopdfError = '';
|
||||
new Promise((resolve, reject) => {
|
||||
tmp.file((err, filePath, fd, cleanupCallback) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({
|
||||
filePath,
|
||||
cleanupCallback,
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
|
||||
let finished = false;
|
||||
|
||||
function onError(err) {
|
||||
finished = true;
|
||||
cleanupCallback();
|
||||
reject(err);
|
||||
}
|
||||
const options = readJson(req.query.options);
|
||||
const params = [];
|
||||
|
||||
// Margins
|
||||
const marginTop = parseInt(`${options.marginTop}`, 10);
|
||||
params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
|
||||
const marginRight = parseInt(`${options.marginRight}`, 10);
|
||||
params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
|
||||
const marginBottom = parseInt(`${options.marginBottom}`, 10);
|
||||
params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
|
||||
const marginLeft = parseInt(`${options.marginLeft}`, 10);
|
||||
params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);
|
||||
|
||||
// Header
|
||||
if (options.headerCenter) {
|
||||
params.push('--header-center', `${options.headerCenter}`);
|
||||
}
|
||||
if (options.headerLeft) {
|
||||
params.push('--header-left', `${options.headerLeft}`);
|
||||
}
|
||||
if (options.headerRight) {
|
||||
params.push('--header-right', `${options.headerRight}`);
|
||||
}
|
||||
if (options.headerFontName) {
|
||||
params.push('--header-font-name', `${options.headerFontName}`);
|
||||
}
|
||||
if (options.headerFontSize) {
|
||||
params.push('--header-font-size', `${options.headerFontSize}`);
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (options.footerCenter) {
|
||||
params.push('--footer-center', `${options.footerCenter}`);
|
||||
}
|
||||
if (options.footerLeft) {
|
||||
params.push('--footer-left', `${options.footerLeft}`);
|
||||
}
|
||||
if (options.footerRight) {
|
||||
params.push('--footer-right', `${options.footerRight}`);
|
||||
}
|
||||
if (options.footerFontName) {
|
||||
params.push('--footer-font-name', `${options.footerFontName}`);
|
||||
}
|
||||
if (options.footerFontSize) {
|
||||
params.push('--footer-font-size', `${options.footerFontSize}`);
|
||||
}
|
||||
|
||||
// Page size
|
||||
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
|
||||
|
||||
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
|
||||
params.push('--window-status', 'done');
|
||||
const wkhtmltopdf = spawn(conf.values.wkhtmltopdfPath, params.concat('-', filePath), {
|
||||
stdio: [
|
||||
'pipe',
|
||||
'ignore',
|
||||
'pipe',
|
||||
],
|
||||
});
|
||||
let timeoutId = setTimeout(function () {
|
||||
timeoutId = null;
|
||||
wkhtmltopdf.kill();
|
||||
}, 50000);
|
||||
wkhtmltopdf.on('error', onError);
|
||||
wkhtmltopdf.stdin.on('error', onError);
|
||||
wkhtmltopdf.stderr.on('data', (data) => {
|
||||
wkhtmltopdfError += `${data}`;
|
||||
});
|
||||
wkhtmltopdf.on('close', (code) => {
|
||||
if (!finished) {
|
||||
clearTimeout(timeoutId);
|
||||
if (!timeoutId) {
|
||||
cleanupCallback();
|
||||
reject(new Error('timeout'));
|
||||
} else if (code) {
|
||||
cleanupCallback();
|
||||
reject();
|
||||
} else {
|
||||
res.set('Content-Type', 'application/pdf');
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
readStream.on('open', () => readStream.pipe(res));
|
||||
readStream.on('close', () => cleanupCallback());
|
||||
readStream.on('error', () => {
|
||||
cleanupCallback();
|
||||
reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
req.pipe(wkhtmltopdf.stdin);
|
||||
}))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
const message = err && err.message;
|
||||
if (message === 'unauthorized') {
|
||||
res.statusCode = 401;
|
||||
res.end('Unauthorized.');
|
||||
} else if (message === 'timeout') {
|
||||
res.statusCode = 408;
|
||||
res.end('Request timeout.');
|
||||
} else {
|
||||
res.statusCode = 400;
|
||||
res.end(wkhtmltopdfError || 'Unknown error.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
BIN
src/assets/favicon.png
Normal file
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 |
4
src/assets/iconCouchdb.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M405.365,303.996c0,20.375 -11.207,30.563 -31.582,31.582l-248.584,0c-20.376,0 -31.583,-10.188 -31.583,-31.582c0,-20.376 11.207,-30.564 31.583,-31.583l249.602,0c20.376,1.019 30.564,11.207 30.564,31.583Zm-30.564,46.864l-249.602,0c-20.376,0 -31.583,10.188 -31.583,31.582c0,20.376 11.207,30.564 31.583,31.583l249.602,0c20.376,0 31.583,-10.188 31.583,-31.583c-1.019,-21.394 -11.207,-31.582 -31.583,-31.582Zm77.428,-172.175c-20.376,0 -31.582,10.188 -31.582,30.563l0,172.175c0,20.376 11.206,30.564 31.582,31.583c30.564,-1.019 46.864,-31.583 46.864,-93.729l0,-77.427c0,-41.771 -16.3,-62.146 -46.864,-63.165Zm-404.458,0c-30.564,1.019 -46.864,21.394 -46.864,63.165l0,77.427c0,62.146 16.3,92.71 46.864,93.729c20.376,0 31.582,-10.188 31.582,-31.583l0,-171.156c-1.019,-20.375 -11.206,-30.563 -31.582,-31.582Zm404.458,-15.282c0,-51.958 -27.507,-76.409 -77.428,-77.428l-249.602,0c-50.94,1.019 -77.428,26.489 -77.428,77.428c30.563,0 46.864,16.301 46.864,46.864c0,30.564 16.301,46.864 46.864,46.864l218.021,0c30.563,0 46.864,-16.3 46.864,-46.864c-1.019,-31.582 16.3,-45.845 45.845,-46.864Z" style="fill:#e42528;fill-rule:nonzero;"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 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
src/assets/iconGitea.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.1 KiB |
5
src/assets/iconGitee.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1652950823759" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2991" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs><style type="text/css"></style></defs>
|
||||
<path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" fill="#C71D23" p-id="2992"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 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 |
12
src/assets/iconGitlab.svg
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
<svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<path d="M14.581,28.019l5.369,-16.526l-10.738,0l5.369,16.526l0,0Z" style="fill:#e24329;"/>
|
||||
<path d="M14.581,28.019l-5.37,-16.526l-7.525,0l12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
|
||||
<path d="M1.686,11.493l-1.632,5.022c-0.148,0.458 0.015,0.96 0.404,1.243l14.123,10.261l-12.895,-16.526l0,0Z" style="fill:#fca326;"/>
|
||||
<path d="M1.686,11.493l7.526,0l-3.235,-9.953c-0.166,-0.512 -0.89,-0.512 -1.057,0l-3.234,9.953l0,0Z" style="fill:#e24329;"/>
|
||||
<path d="M14.581,28.019l5.369,-16.526l7.526,0l-12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
|
||||
<path d="M27.476,11.493l1.631,5.022c0.149,0.458 -0.014,0.96 -0.404,1.243l-14.122,10.261l12.895,-16.526l0,0Z" style="fill:#fca326;"/>
|
||||
<path d="M27.476,11.493l-7.526,0l3.234,-9.953c0.167,-0.512 0.891,-0.512 1.058,0l3.234,9.953l0,0Z" style="fill:#e24329;"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1005 B |
13
src/assets/iconGoogle.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
|
||||
<defs>
|
||||
<path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
|
||||
</defs>
|
||||
<clipPath id="b">
|
||||
<use xlink:href="#a" overflow="visible"/>
|
||||
</clipPath>
|
||||
<path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
|
||||
<path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
|
||||
<path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
|
||||
<path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 729 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,19 +1 @@
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126 126">
|
||||
<path d="M103.5,0c3.762,0.003 7.395,0.971 10.725,2.716c0.966,0.506 1.808,1.221 2.712,1.831l-53.937,40.453l-53.937,-40.453c4.402,-3.289 8.02,-4.273 13.437,-4.547l81,0Z" style="fill:#ffe600;fill-rule:nonzero;"/>
|
||||
<path d="M9.063,4.547l53.937,40.453l-58.04,72.55c-3.44,-4.417 -4.681,-8.528 -4.96,-14.05l0,-81c0.064,-5.221 1.801,-10.265 5.138,-14.312c0.914,-1.109 2.033,-2.033 3.05,-3.05l0.875,-0.591l0,0Z" style="fill:#bbd500;fill-rule:nonzero;"/>
|
||||
<path d="M63,45l58.04,72.549l-0.178,0.263c-4.901,5.465 -10.068,7.82 -17.362,8.188l-81,0c-5.221,-0.065 -10.265,-1.801 -14.312,-5.138c-1.109,-0.915 -2.033,-2.034 -3.05,-3.05l-0.177,-0.262l58.039,-72.55l0,0Z" style="fill:#ff8a00;fill-rule:nonzero;"/>
|
||||
<path d="M116.937,4.547c3.844,2.631 6.684,6.83 8.051,11.262c0.441,1.427 0.673,2.914 0.896,4.391c0.114,0.759 0.077,1.533 0.116,2.3l0,81c-0.023,3.748 -0.939,7.415 -2.716,10.725c-0.632,1.178 -1.496,2.216 -2.245,3.325l-58.039,-72.55l53.937,-40.453l0,0Z" style="fill:#75b7fd;fill-rule:nonzero;"/>
|
||||
<path d="M32.063,12l61.874,0c7.767,0 14.063,6.296 14.063,14.063l0,61.874c0,7.767 -6.296,14.063 -14.063,14.063l-61.875,0c-7.766,0 -14.062,-6.296 -14.062,-14.063l0,-61.875c0,-7.766 6.296,-14.062 14.062,-14.062l0.001,0Z" style="fill:#fff;fill-rule:nonzero;"/>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="40.711" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
|
||||
<rect x="56.263" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M71.278,44.466l0,-8.841l-38.556,0l0,8.841l38.556,0Z" style="fill:#737373;fill-rule:nonzero;"/>
|
||||
<rect x="32.722" y="55.195" width="38.556" height="8.842" style="fill:#737373;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<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: 1.7 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 5.4 KiB |
@ -1,53 +1,109 @@
|
||||
<template>
|
||||
<splash-screen v-if="!ready"></splash-screen>
|
||||
<div v-else class="app">
|
||||
<layout></layout>
|
||||
<modal v-if="showModal"></modal>
|
||||
<div class="app" :class="classes" @keydown.esc="close">
|
||||
<splash-screen v-if="!ready"></splash-screen>
|
||||
<layout v-else></layout>
|
||||
<modal></modal>
|
||||
<notification></notification>
|
||||
<context-menu></context-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapState } from 'vuex';
|
||||
import '../styles';
|
||||
import '../styles/markdownHighlighting.scss';
|
||||
import '../styles/app.scss';
|
||||
import Layout from './Layout';
|
||||
import Modal from './Modal';
|
||||
import Notification from './Notification';
|
||||
import ContextMenu from './ContextMenu';
|
||||
import SplashScreen from './SplashScreen';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import networkSvc from '../services/networkSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import store from '../store';
|
||||
import './common/vueGlobals';
|
||||
import utils from '../services/utils';
|
||||
import providerRegistry from '../services/providers/common/providerRegistry';
|
||||
|
||||
// Global directives
|
||||
Vue.directive('focus', {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
},
|
||||
});
|
||||
const themeClasses = {
|
||||
light: ['app--light'],
|
||||
dark: ['app--dark'],
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
Modal,
|
||||
Notification,
|
||||
ContextMenu,
|
||||
SplashScreen,
|
||||
},
|
||||
data: () => ({
|
||||
ready: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
'ready',
|
||||
]),
|
||||
showModal() {
|
||||
return !!this.$store.getters['modal/config'];
|
||||
classes() {
|
||||
const result = themeClasses[store.getters['data/computedSettings'].colorTheme];
|
||||
return Array.isArray(result) ? result : themeClasses.light;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
tempFileSvc.close();
|
||||
},
|
||||
// 通过路径查看文件 支持相对路径
|
||||
viewFileByPath(path) {
|
||||
// 如果是md结尾
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const currDirNode = store.getters['explorer/selectedNodeFolder'];
|
||||
if (path.slice(-3) === '.md') {
|
||||
const rootNode = store.getters['explorer/rootNode'];
|
||||
const node = utils.findNodeByPath(rootNode, currDirNode, path);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
store.commit('explorer/setSelectedId', node.item.id);
|
||||
// Prevent from freezing the UI while loading the file
|
||||
setTimeout(() => {
|
||||
store.commit('file/setCurrentId', node.item.id);
|
||||
}, 10);
|
||||
} else {
|
||||
const workspace = store.getters['workspace/currentWorkspace'];
|
||||
const provider = providerRegistry.providersById[workspace.providerId];
|
||||
if (provider == null) {
|
||||
return;
|
||||
}
|
||||
const absolutePath = utils.getAbsoluteFilePath(currDirNode, path);
|
||||
const url = provider.getFilePathUrl(absolutePath);
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
window.viewFileByPath = this.viewFileByPath;
|
||||
try {
|
||||
await syncSvc.init();
|
||||
await networkSvc.init();
|
||||
// store 编辑主题
|
||||
const editTheme = localStorage.getItem('theme/currEditTheme');
|
||||
store.dispatch('theme/setEditTheme', editTheme || 'default');
|
||||
// store 预览主题
|
||||
const previewTheme = localStorage.getItem('theme/currPreviewTheme');
|
||||
store.dispatch('theme/setPreviewTheme', previewTheme || 'default');
|
||||
this.ready = true;
|
||||
tempFileSvc.setReady();
|
||||
} catch (err) {
|
||||
if (err && err.message === 'RELOAD') {
|
||||
window.location.reload();
|
||||
} else if (err && err.message !== 'RELOAD') {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
store.dispatch('notification/error', err);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/app';
|
||||
|
||||
.app__spash-screen {
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
height: 100%;
|
||||
background: no-repeat center url('../assets/logo.svg');
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,37 +1,42 @@
|
||||
<template>
|
||||
<div class="button-bar">
|
||||
<div class="button-bar__inner button-bar__inner--top">
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()">
|
||||
<button class="button-bar__button button-bar__button--navigation-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'切换导航栏'">
|
||||
<icon-navigation-bar></icon-navigation-bar>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()">
|
||||
</button>
|
||||
<button class="button-bar__button button-bar__button--side-preview-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'切换侧边预览'">
|
||||
<icon-side-preview></icon-side-preview>
|
||||
</div>
|
||||
<div class="button-bar__button" @click="toggleEditor(false)">
|
||||
</button>
|
||||
<button class="button-bar__button button-bar__button--editor-toggler button" @click="toggleEditor(false)" v-title="'阅读模式'">
|
||||
<icon-eye></icon-eye>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-bar__inner button-bar__inner--bottom">
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()">
|
||||
<button class="button-bar__button button-bar__button--focus-mode-toggler button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'切换对焦模式'">
|
||||
<icon-target></icon-target>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()">
|
||||
</button>
|
||||
<button class="button-bar__button button-bar__button--scroll-sync-toggler button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'切换滚动同步'">
|
||||
<icon-scroll-sync></icon-scroll-sync>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()">
|
||||
</button>
|
||||
<button class="button-bar__button button-bar__button--status-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'切换状态栏'">
|
||||
<icon-status-bar></icon-status-bar>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapGetters('data', [
|
||||
'localSettings',
|
||||
]),
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'layoutSettings',
|
||||
]),
|
||||
},
|
||||
methods: mapActions('data', [
|
||||
'toggleNavigationBar',
|
||||
'toggleEditor',
|
||||
@ -43,8 +48,9 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.button-bar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
@ -60,19 +66,44 @@ export default {
|
||||
}
|
||||
|
||||
.button-bar__button {
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 2px;
|
||||
margin: 3px 0;
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.15);
|
||||
background-color: $navbar-hover-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-bar__button--on {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,23 +4,24 @@
|
||||
|
||||
<script>
|
||||
import Prism from 'prismjs';
|
||||
import cledit from '../libs/cledit';
|
||||
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) {
|
||||
const clEditor = cledit(preElt, scrollElt);
|
||||
clEditor.on('contentChanged', value => this.$emit('changed', value));
|
||||
clEditor.init({
|
||||
content: this.value,
|
||||
sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
|
||||
});
|
||||
clEditor.on('contentChanged', value => this.$emit('changed', value));
|
||||
clEditor.toggleEditable(!this.disabled);
|
||||
}
|
||||
},
|
||||
@ -28,14 +29,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.code-editor {
|
||||
margin: 0;
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: normal;
|
||||
height: auto;
|
||||
@ -44,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;
|
||||
|
79
src/components/ContextMenu.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="context-menu" v-if="items.length" @click="close()" @contextmenu.prevent="close()">
|
||||
<div class="context-menu__inner flex flex--column" :style="{ left: coordinates.left + 'px', top: coordinates.top + 'px' }" @click.stop>
|
||||
<div v-for="(item, idx) in items" :key="idx">
|
||||
<div class="context-menu__separator" v-if="item.type === 'separator'"></div>
|
||||
<div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
|
||||
<a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import store from '../store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState('contextMenu', [
|
||||
'coordinates',
|
||||
'items',
|
||||
'resolve',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
close(item = null) {
|
||||
this.resolve(item);
|
||||
store.dispatch('contextMenu/close');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
$padding: 5px;
|
||||
|
||||
.context-menu__inner {
|
||||
position: absolute;
|
||||
background-color: #ebebeb;
|
||||
border-radius: $padding;
|
||||
padding: $padding 0;
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.16), 0 3px 10px 1px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.context-menu__item {
|
||||
display: block;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
padding: 0 25px;
|
||||
}
|
||||
|
||||
a.context-menu__item {
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: #338dfc;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu__item--disabled {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.context-menu__separator {
|
||||
border-top: 2px solid #dcdcdd;
|
||||
margin: $padding 0;
|
||||
}
|
||||
</style>
|
@ -1,15 +1,31 @@
|
||||
<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>
|
||||
<editor-new-discussion-button v-if="!isCurrentTemp"></editor-new-discussion-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import CommentList from './gutters/CommentList';
|
||||
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
|
||||
import store from '../store';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import imageSvc from '../services/imageSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommentList,
|
||||
EditorNewDiscussionButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
@ -17,11 +33,104 @@ 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})]`, ``);
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
editorSvc.clEditor.replaceAll(`[图片上传中...(image-${imgId})]`, `[图片上传失败...(image-${imgId})]`);
|
||||
store.dispatch('notification/error', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 当前选择的图片存储图床
|
||||
const currImgStorageStr = localStorage.getItem('img/checkedStorage');
|
||||
if (currImgStorageStr) {
|
||||
store.commit('img/changeCheckedStorage', JSON.parse(currImgStorageStr));
|
||||
}
|
||||
// 当前本地图片路径配置
|
||||
const workspaceImgPath = localStorage.getItem('img/workspaceImgPath');
|
||||
if (workspaceImgPath) {
|
||||
store.commit('img/setWorkspaceImgPath', JSON.parse(workspaceImgPath));
|
||||
}
|
||||
const editorElt = this.$el.querySelector('.editor__inner');
|
||||
const onDiscussionEvt = cb => (evt) => {
|
||||
let elt = evt.target;
|
||||
while (elt && elt !== editorElt) {
|
||||
if (elt.discussionId) {
|
||||
cb(elt.discussionId);
|
||||
return;
|
||||
}
|
||||
elt = elt.parentNode;
|
||||
}
|
||||
};
|
||||
|
||||
const classToggler = toggle => (discussionId) => {
|
||||
editorElt.getElementsByClassName(`discussion-editor-highlighting--${discussionId}`)
|
||||
.cl_each(elt => elt.classList.toggle('discussion-editor-highlighting--hover', toggle));
|
||||
document.getElementsByClassName(`comment--discussion-${discussionId}`)
|
||||
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
|
||||
};
|
||||
|
||||
editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
|
||||
editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
|
||||
editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||
store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||
}));
|
||||
|
||||
editorElt.addEventListener('drop', (event) => {
|
||||
const transItems = event.dataTransfer.items;
|
||||
this.processUpload(transItems);
|
||||
});
|
||||
editorElt.addEventListener('paste', (event) => {
|
||||
const pasteItems = (event.clipboardData || window.clipboardData).items;
|
||||
this.processUpload(pasteItems);
|
||||
});
|
||||
|
||||
this.$watch(
|
||||
() => store.state.discussion.currentDiscussionId,
|
||||
(discussionId, oldDiscussionId) => {
|
||||
if (oldDiscussionId) {
|
||||
editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`)
|
||||
.cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
|
||||
}
|
||||
if (discussionId) {
|
||||
editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
|
||||
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.editor {
|
||||
position: absolute;
|
||||
@ -37,7 +146,6 @@ export default {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
caret-color: #000;
|
||||
|
||||
* {
|
||||
line-height: $line-height-base;
|
||||
@ -47,11 +155,6 @@ export default {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.discussion-highlight,
|
||||
.find-replace-highlight {
|
||||
background-color: transparentize(#ffe400, 0.5);
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
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,102 +1,174 @@
|
||||
<template>
|
||||
<div class="explorer flex flex--column">
|
||||
<div class="side-title flex flex--row flex--space-between">
|
||||
<div class="flex flex--row">
|
||||
<button class="side-title__button button" @click="newItem()">
|
||||
<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>
|
||||
<button class="side-title__button button" @click="newItem(true)">
|
||||
<button class="side-title__button side-title__button--new-folder button" @click="newItem(true)" v-title="'创建文件夹'">
|
||||
<icon-folder-plus></icon-folder-plus>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="editItem()">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="deleteItem()">
|
||||
<button class="side-title__button side-title__button--delete button" @click="deleteItem()" v-title="'删除'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
<button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'重命名'">
|
||||
<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>
|
||||
<button class="side-title__button button" @click="toggleExplorer(false)">
|
||||
<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}" tabindex="0">
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import ExplorerNode from './ExplorerNode';
|
||||
import explorerSvc from '../services/explorerSvc';
|
||||
import store from '../store';
|
||||
import MenuEntry from './menus/common/MenuEntry';
|
||||
import localDbSvc from '../services/localDbSvc';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExplorerNode,
|
||||
MenuEntry,
|
||||
},
|
||||
data: () => ({
|
||||
currentFileId: '',
|
||||
showSearch: false,
|
||||
searching: false,
|
||||
searchText: '',
|
||||
searchItems: [],
|
||||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapState('explorer', [
|
||||
'newChildNode',
|
||||
]),
|
||||
...mapGetters('explorer', [
|
||||
'rootNode',
|
||||
'selectedNode',
|
||||
]),
|
||||
workspaceId: () => store.getters['workspace/currentWorkspace'].id,
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
'toggleExplorer',
|
||||
]),
|
||||
newItem(isFolder) {
|
||||
const parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
|
||||
this.$store.dispatch('explorer/openNode', parentId);
|
||||
this.$store.commit('explorer/setNewItem', {
|
||||
type: isFolder ? 'folder' : 'file',
|
||||
parentId,
|
||||
newItem: isFolder => explorerSvc.newItem(isFolder),
|
||||
deleteItem: () => explorerSvc.deleteItem(),
|
||||
editItem() {
|
||||
const node = this.selectedNode;
|
||||
if (!node.isTrash && !node.isTemp) {
|
||||
store.commit('explorer/setEditingId', node.item.id);
|
||||
}
|
||||
},
|
||||
back() {
|
||||
this.showSearch = false;
|
||||
},
|
||||
toSearch() {
|
||||
this.showSearch = true;
|
||||
},
|
||||
search() {
|
||||
this.searchItems = [];
|
||||
if (!this.searchText) {
|
||||
return;
|
||||
}
|
||||
this.searching = true;
|
||||
const allFileById = {};
|
||||
const filterIds = [];
|
||||
localDbSvc.getWorkspaceItems(this.workspaceId, (item) => {
|
||||
if (item.type !== 'file' && item.type !== 'content') {
|
||||
return;
|
||||
}
|
||||
if (item.type === 'file') {
|
||||
allFileById[item.id] = item;
|
||||
}
|
||||
if (filterIds.length >= 50) {
|
||||
return;
|
||||
}
|
||||
const fileId = item.id.split('/')[0];
|
||||
// 包含了直接跳过
|
||||
if (filterIds.indexOf(fileId) > -1) {
|
||||
return;
|
||||
}
|
||||
if (item.name && item.name.indexOf(this.searchText) > -1) {
|
||||
filterIds.push(fileId);
|
||||
}
|
||||
if (item.text && item.text.indexOf(this.searchText) > -1) {
|
||||
filterIds.push(fileId);
|
||||
}
|
||||
}, () => {
|
||||
filterIds.forEach((it) => {
|
||||
const file = allFileById[it];
|
||||
if (file) {
|
||||
this.searchItems.push(file);
|
||||
}
|
||||
});
|
||||
this.searching = false;
|
||||
badgeSvc.addBadge('searchFile');
|
||||
});
|
||||
},
|
||||
editItem() {
|
||||
const selectedNode = this.$store.getters['explorer/selectedNode'];
|
||||
this.$store.commit('explorer/setEditingId', selectedNode.item.id);
|
||||
},
|
||||
deleteItem() {
|
||||
const selectedNode = this.$store.getters['explorer/selectedNode'];
|
||||
if (!selectedNode.isNil) {
|
||||
this.$store.dispatch(selectedNode.isFolder
|
||||
? 'modal/folderDeletion'
|
||||
: 'modal/fileDeletion',
|
||||
selectedNode.item)
|
||||
.then(() => {
|
||||
if (selectedNode === this.$store.getters['explorer/selectedNode']) {
|
||||
if (selectedNode.isFolder) {
|
||||
const recursiveDelete = (folderNode) => {
|
||||
folderNode.folders.forEach(recursiveDelete);
|
||||
folderNode.files.forEach((fileNode) => {
|
||||
this.$store.commit('file/deleteItem', fileNode.item.id);
|
||||
});
|
||||
this.$store.commit('folder/deleteItem', folderNode.item.id);
|
||||
};
|
||||
recursiveDelete(selectedNode);
|
||||
} else {
|
||||
this.$store.commit('file/deleteItem', selectedNode.item.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
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.$store.watch(
|
||||
() => this.$store.getters['file/current'].id,
|
||||
this.$watch(
|
||||
() => store.getters['file/current'].id,
|
||||
(currentFileId) => {
|
||||
this.$store.commit('explorer/setSelectedId', currentFileId);
|
||||
this.$store.dispatch('explorer/openNode', currentFileId);
|
||||
this.currentFileId = currentFileId;
|
||||
store.commit('explorer/setSelectedId', currentFileId);
|
||||
store.dispatch('explorer/openNode', currentFileId);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.explorer,
|
||||
.explorer__tree {
|
||||
height: 100%;
|
||||
@ -111,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>
|
||||
|
@ -1,28 +1,33 @@
|
||||
<template>
|
||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
|
||||
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
|
||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node)" @dragleave.stop="isDragTarget && setDragTarget()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
|
||||
<div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
|
||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingNodeName">
|
||||
</div>
|
||||
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
|
||||
<div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTarget()">
|
||||
{{node.item.name}}
|
||||
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
||||
</div>
|
||||
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
|
||||
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}">
|
||||
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
|
||||
<div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
|
||||
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc.stop="submitNewChild(true)" v-model.trim="newChildName">
|
||||
</div>
|
||||
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||
</div>
|
||||
<button ref="copyId" v-clipboard="copyPath()" @click="info('路径已复制到剪切板!')" style="display: none;"></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations, mapActions } from 'vuex';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
import explorerSvc from '../services/explorerSvc';
|
||||
import store from '../store';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default {
|
||||
name: 'explorer-node',
|
||||
name: 'explorer-node', // Required for recursivity
|
||||
props: ['node', 'depth'],
|
||||
data: () => ({
|
||||
editingValue: '',
|
||||
@ -35,35 +40,35 @@ export default {
|
||||
return `${(this.depth + 1) * 15}px`;
|
||||
},
|
||||
isSelected() {
|
||||
return this.$store.getters['explorer/selectedNode'] === this.node;
|
||||
return store.getters['explorer/selectedNode'] === this.node;
|
||||
},
|
||||
isEditing() {
|
||||
return this.$store.getters['explorer/editingNode'] === this.node;
|
||||
return store.getters['explorer/editingNode'] === this.node;
|
||||
},
|
||||
isDragTarget() {
|
||||
return this.$store.getters['explorer/dragTargetNode'] === this.node;
|
||||
return store.getters['explorer/dragTargetNode'] === this.node;
|
||||
},
|
||||
isDragTargetFolder() {
|
||||
return this.$store.getters['explorer/dragTargetNodeFolder'] === this.node;
|
||||
return store.getters['explorer/dragTargetNodeFolder'] === this.node;
|
||||
},
|
||||
isOpen() {
|
||||
return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
|
||||
return store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
|
||||
},
|
||||
newChild() {
|
||||
return this.$store.getters['explorer/newChildNodeParent'] === this.node
|
||||
&& this.$store.state.explorer.newChildNode;
|
||||
return store.getters['explorer/newChildNodeParent'] === this.node
|
||||
&& store.state.explorer.newChildNode;
|
||||
},
|
||||
newChildName: {
|
||||
get() {
|
||||
return this.$store.state.explorer.newChildNode.item.name;
|
||||
return store.state.explorer.newChildNode.item.name;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('explorer/setNewItemName', value && value.slice(0, 250));
|
||||
store.commit('explorer/setNewItemName', value);
|
||||
},
|
||||
},
|
||||
editingNodeName: {
|
||||
get() {
|
||||
return this.$store.getters['explorer/editingNode'].item.name;
|
||||
return store.getters['explorer/editingNode'].item.name;
|
||||
},
|
||||
set(value) {
|
||||
this.editingValue = value.trim();
|
||||
@ -72,86 +77,132 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('explorer', [
|
||||
'setDragTargetId',
|
||||
'setEditingId',
|
||||
]),
|
||||
...mapActions('explorer', [
|
||||
'setDragTarget',
|
||||
]),
|
||||
select(id) {
|
||||
const node = this.$store.getters['explorer/nodeMap'][id];
|
||||
if (node) {
|
||||
this.$store.commit('explorer/setSelectedId', id);
|
||||
if (node.isFolder) {
|
||||
this.$store.commit('explorer/toggleOpenNode', id);
|
||||
} else {
|
||||
this.$store.commit('file/setCurrentId', id);
|
||||
}
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
select(id = this.node.item.id, doOpen = true) {
|
||||
const node = store.getters['explorer/nodeMap'][id];
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
store.commit('explorer/setSelectedId', id);
|
||||
if (doOpen) {
|
||||
// Prevent from freezing the UI while loading the file
|
||||
setTimeout(() => {
|
||||
if (node.isFolder) {
|
||||
store.commit('explorer/toggleOpenNode', id);
|
||||
} else if (store.state.file.currentId !== id) {
|
||||
store.commit('file/setCurrentId', id);
|
||||
badgeSvc.addBadge('switchFile');
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
submitNewChild(cancel) {
|
||||
const newChildNode = this.$store.state.explorer.newChildNode;
|
||||
async submitNewChild(cancel) {
|
||||
const { newChildNode } = store.state.explorer;
|
||||
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
||||
const id = utils.uid();
|
||||
if (newChildNode.isFolder) {
|
||||
this.$store.commit('folder/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
// Add empty line at the end if needed
|
||||
const ensureFinalNewLine = text => `${text}\n`.replace(/\n\n$/, '\n');
|
||||
const text = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileContent);
|
||||
const properties = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileProperties);
|
||||
this.$store.commit('content/setItem', {
|
||||
id: `${id}/content`,
|
||||
text,
|
||||
properties,
|
||||
});
|
||||
this.$store.commit('file/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
});
|
||||
try {
|
||||
if (newChildNode.isFolder) {
|
||||
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||
this.select(item.id);
|
||||
badgeSvc.addBadge('createFolder');
|
||||
} else {
|
||||
const item = await workspaceSvc.createFile(newChildNode.item);
|
||||
this.select(item.id);
|
||||
badgeSvc.addBadge('createFile');
|
||||
}
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
this.select(id);
|
||||
}
|
||||
this.$store.commit('explorer/setNewItem', null);
|
||||
store.commit('explorer/setNewItem', null);
|
||||
},
|
||||
submitEdit(cancel) {
|
||||
const editingNode = this.$store.getters['explorer/editingNode'];
|
||||
const id = editingNode.item.id;
|
||||
async submitEdit(cancel) {
|
||||
const { item, isFolder } = store.getters['explorer/editingNode'];
|
||||
const value = this.editingValue;
|
||||
if (!cancel && id && value) {
|
||||
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
||||
id,
|
||||
name: value.slice(0, 250),
|
||||
});
|
||||
this.setEditingId(null);
|
||||
if (!cancel && item.id && value && item.name !== value) {
|
||||
try {
|
||||
await workspaceSvc.storeItem({
|
||||
...item,
|
||||
name: value,
|
||||
});
|
||||
badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
}
|
||||
this.$store.commit('explorer/setEditingId', null);
|
||||
},
|
||||
setDragSourceId(evt) {
|
||||
const id = this.node.item.id;
|
||||
if (id === 'fake') {
|
||||
if (this.node.noDrag) {
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.$store.commit('explorer/setDragSourceId', id);
|
||||
store.commit('explorer/setDragSourceId', this.node.item.id);
|
||||
// Fix for Firefox
|
||||
// See https://stackoverflow.com/a/3977637/1333165
|
||||
evt.dataTransfer.setData('Text', '');
|
||||
},
|
||||
copyPath() {
|
||||
let path = utils.getAbsoluteDir(this.node).replaceAll(' ', '%20');
|
||||
path = path.indexOf('/') === 0 ? path : `/${path}`;
|
||||
return this.node.isFolder ? path : `${path}.md`;
|
||||
},
|
||||
onDrop() {
|
||||
const sourceNode = this.$store.getters['explorer/dragSourceNode'];
|
||||
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
|
||||
this.setDragTargetId();
|
||||
const sourceNode = store.getters['explorer/dragSourceNode'];
|
||||
const targetNode = store.getters['explorer/dragTargetNodeFolder'];
|
||||
this.setDragTarget();
|
||||
if (!sourceNode.isNil
|
||||
&& !targetNode.isNil
|
||||
&& sourceNode.item.id !== targetNode.item.id
|
||||
) {
|
||||
const patch = {
|
||||
id: sourceNode.item.id,
|
||||
workspaceSvc.storeItem({
|
||||
...sourceNode.item,
|
||||
parentId: targetNode.item.id,
|
||||
};
|
||||
if (sourceNode.isFolder) {
|
||||
this.$store.commit('folder/patchItem', patch);
|
||||
} else {
|
||||
this.$store.commit('file/patchItem', patch);
|
||||
});
|
||||
badgeSvc.addBadge(sourceNode.isFolder ? 'moveFolder' : 'moveFile');
|
||||
}
|
||||
},
|
||||
async onContextMenu(evt) {
|
||||
if (this.select(undefined, false)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const item = await store.dispatch('contextMenu/open', {
|
||||
coordinates: {
|
||||
left: evt.clientX,
|
||||
top: evt.clientY,
|
||||
},
|
||||
items: [{
|
||||
name: '新建文件',
|
||||
disabled: !this.node.isFolder || this.node.isTrash,
|
||||
perform: () => explorerSvc.newItem(false),
|
||||
}, {
|
||||
name: '新建文件夹',
|
||||
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
|
||||
perform: () => explorerSvc.newItem(true),
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
name: '重命名',
|
||||
disabled: this.node.isTrash || this.node.isTemp,
|
||||
perform: () => this.setEditingId(this.node.item.id),
|
||||
}, {
|
||||
name: '删除',
|
||||
perform: () => explorerSvc.deleteItem(),
|
||||
}, {
|
||||
name: '复制路径',
|
||||
disabled: this.node.isTrash || this.node.isTemp,
|
||||
perform: () => this.$refs.copyId.click(),
|
||||
}],
|
||||
});
|
||||
if (item) {
|
||||
item.perform();
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -167,6 +218,7 @@ $item-font-size: 14px;
|
||||
}
|
||||
|
||||
.explorer-node__item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: $item-font-size;
|
||||
overflow: hidden;
|
||||
@ -177,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;
|
||||
@ -195,17 +251,29 @@ $item-font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-node__item--folder,
|
||||
.explorer-node__item-editor--folder,
|
||||
.explorer-node--trash,
|
||||
.explorer-node--temp {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-node--folder > .explorer-node__item,
|
||||
.explorer-node--folder > .explorer-node__item-editor,
|
||||
.explorer-node__new-child--folder {
|
||||
&::before {
|
||||
content: '▸';
|
||||
content: '▹';
|
||||
position: absolute;
|
||||
margin-left: -13px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-node--open > & {
|
||||
content: '▾';
|
||||
}
|
||||
.explorer-node--folder.explorer-node--open > .explorer-node__item,
|
||||
.explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
|
||||
&::before {
|
||||
content: '▾';
|
||||
}
|
||||
}
|
||||
|
||||
|
395
src/components/FindReplace.vue
Normal file
@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<div class="find-replace" @keydown.esc.stop="onEscape">
|
||||
<button class="find-replace__close-button button not-tabbable" @click="close()" v-title="'关闭'">
|
||||
<icon-close></icon-close>
|
||||
</button>
|
||||
<div class="find-replace__row">
|
||||
<input type="text" class="find-replace__text-input find-replace__text-input--find text-input" @keydown.enter="find('forward')" v-model="findText">
|
||||
<div class="find-replace__find-stats">
|
||||
{{findPosition}} of {{findCount}}
|
||||
</div>
|
||||
<div class="flex flex--row flex--space-between">
|
||||
<div class="flex flex--row">
|
||||
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findCaseSensitive}" @click="findCaseSensitive = !findCaseSensitive" title="Case sensitive">Aa</button>
|
||||
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findUseRegexp}" @click="findUseRegexp = !findUseRegexp" title="Regular expression">.<sup>⁕</sup></button>
|
||||
</div>
|
||||
<div class="flex flex--row">
|
||||
<button class="find-replace__button button" @click="find('backward')">上一个</button>
|
||||
<button class="find-replace__button button" @click="find('forward')">下一个</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type === 'replace'">
|
||||
<div class="find-replace__row">
|
||||
<input type="text" class="find-replace__text-input find-replace__text-input--replace text-input" @keydown.enter="replace" v-model="replaceText">
|
||||
</div>
|
||||
<div class="find-replace__row flex flex--row flex--end">
|
||||
<button class="find-replace__button button" @click="replace">替换</button>
|
||||
<button class="find-replace__button button" @click="replaceAll">全部替换</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import cledit from '../services/editor/cledit';
|
||||
import store from '../store';
|
||||
import EditorClassApplier from './common/EditorClassApplier';
|
||||
|
||||
const accessor = (fieldName, setterName) => ({
|
||||
get() {
|
||||
return store.state.findReplace[fieldName];
|
||||
},
|
||||
set(value) {
|
||||
store.commit(`findReplace/${setterName}`, value);
|
||||
},
|
||||
});
|
||||
|
||||
const computedLayoutSetting = key => ({
|
||||
get() {
|
||||
return store.getters['data/layoutSettings'][key];
|
||||
},
|
||||
set(value) {
|
||||
store.dispatch('data/patchLayoutSettings', {
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class DynamicClassApplier {
|
||||
constructor(cssClass, offset, silent) {
|
||||
this.startMarker = new cledit.Marker(offset.start);
|
||||
this.endMarker = new cledit.Marker(offset.end);
|
||||
editorSvc.clEditor.addMarker(this.startMarker);
|
||||
editorSvc.clEditor.addMarker(this.endMarker);
|
||||
if (!silent) {
|
||||
this.classApplier = new EditorClassApplier(
|
||||
[`find-replace-${this.startMarker.id}`, cssClass],
|
||||
() => ({
|
||||
start: this.startMarker.offset,
|
||||
end: this.endMarker.offset,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clean = () => {
|
||||
editorSvc.clEditor.removeMarker(this.startMarker);
|
||||
editorSvc.clEditor.removeMarker(this.endMarker);
|
||||
if (this.classApplier) {
|
||||
this.classApplier.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
findCount: 0,
|
||||
findPosition: 0,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('findReplace', [
|
||||
'type',
|
||||
'lastOpen',
|
||||
]),
|
||||
findText: accessor('findText', 'setFindText'),
|
||||
replaceText: accessor('replaceText', 'setReplaceText'),
|
||||
findCaseSensitive: computedLayoutSetting('findCaseSensitive'),
|
||||
findUseRegexp: computedLayoutSetting('findUseRegexp'),
|
||||
},
|
||||
methods: {
|
||||
highlightOccurrences() {
|
||||
const oldClassAppliers = {};
|
||||
Object.entries(this.classAppliers).forEach(([, classApplier]) => {
|
||||
const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
|
||||
oldClassAppliers[newKey] = classApplier;
|
||||
});
|
||||
const offsetList = [];
|
||||
this.classAppliers = {};
|
||||
if (this.state !== 'destroyed' && this.findText) {
|
||||
try {
|
||||
this.searchRegex = this.findText;
|
||||
if (!this.findUseRegexp) {
|
||||
this.searchRegex = this.searchRegex.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||
}
|
||||
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
|
||||
this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
|
||||
editorSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
|
||||
const match = params[0];
|
||||
const offset = params[params.length - 2];
|
||||
offsetList.push({
|
||||
start: offset,
|
||||
end: offset + match.length,
|
||||
});
|
||||
});
|
||||
offsetList.forEach((offset, i) => {
|
||||
const key = `${offset.start}:${offset.end}`;
|
||||
this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
|
||||
'find-replace-highlighting',
|
||||
offset,
|
||||
i > 200,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
if (this.state !== 'created') {
|
||||
this.find('selection');
|
||||
this.state = 'created';
|
||||
}
|
||||
}
|
||||
Object.entries(oldClassAppliers).forEach(([key, classApplier]) => {
|
||||
if (!this.classAppliers[key]) {
|
||||
classApplier.clean();
|
||||
if (classApplier === this.selectedClassApplier) {
|
||||
this.selectedClassApplier.child.clean();
|
||||
this.selectedClassApplier = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.findCount = offsetList.length;
|
||||
},
|
||||
unselectClassApplier() {
|
||||
if (this.selectedClassApplier) {
|
||||
this.selectedClassApplier.child.clean();
|
||||
this.selectedClassApplier.child = null;
|
||||
this.selectedClassApplier = null;
|
||||
}
|
||||
this.findPosition = 0;
|
||||
},
|
||||
find(mode = 'forward') {
|
||||
const { selectedClassApplier } = this;
|
||||
this.unselectClassApplier();
|
||||
const { selectionMgr } = editorSvc.clEditor;
|
||||
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||
const keys = Object.keys(this.classAppliers);
|
||||
const finder = checker => (key) => {
|
||||
if (checker(this.classAppliers[key]) && selectedClassApplier !== this.classAppliers[key]) {
|
||||
this.selectedClassApplier = this.classAppliers[key];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (mode === 'backward') {
|
||||
this.selectedClassApplier = this.classAppliers[keys[keys.length - 1]];
|
||||
keys.reverse().some(finder(classApplier => classApplier.startMarker.offset <= startOffset));
|
||||
} else if (mode === 'selection') {
|
||||
keys.some(finder(classApplier => classApplier.startMarker.offset === startOffset &&
|
||||
classApplier.endMarker.offset === endOffset));
|
||||
} else if (mode === 'forward') {
|
||||
this.selectedClassApplier = this.classAppliers[keys[0]];
|
||||
keys.some(finder(classApplier => classApplier.endMarker.offset >= endOffset));
|
||||
}
|
||||
if (this.selectedClassApplier) {
|
||||
selectionMgr.setSelectionStartEnd(
|
||||
this.selectedClassApplier.startMarker.offset,
|
||||
this.selectedClassApplier.endMarker.offset,
|
||||
);
|
||||
this.selectedClassApplier.child = new DynamicClassApplier('find-replace-selection', {
|
||||
start: this.selectedClassApplier.startMarker.offset,
|
||||
end: this.selectedClassApplier.endMarker.offset,
|
||||
});
|
||||
selectionMgr.updateCursorCoordinates(this.$el.parentNode.clientHeight);
|
||||
// Deduce the findPosition
|
||||
Object.keys(this.classAppliers).forEach((key, i) => {
|
||||
if (this.selectedClassApplier !== this.classAppliers[key]) {
|
||||
return false;
|
||||
}
|
||||
this.findPosition = i + 1;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
},
|
||||
replace() {
|
||||
if (this.searchRegex) {
|
||||
if (!this.selectedClassApplier) {
|
||||
this.find();
|
||||
return;
|
||||
}
|
||||
editorSvc.clEditor.replaceAll(
|
||||
this.replaceRegex,
|
||||
this.replaceText,
|
||||
this.selectedClassApplier.startMarker.offset,
|
||||
);
|
||||
this.$nextTick(() => this.find());
|
||||
}
|
||||
},
|
||||
replaceAll() {
|
||||
if (this.searchRegex) {
|
||||
editorSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
store.commit('findReplace/setType');
|
||||
},
|
||||
onEscape() {
|
||||
editorSvc.clEditor.focus();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.classAppliers = {};
|
||||
|
||||
// Highlight occurences
|
||||
this.debouncedHighlightOccurrences = cledit.Utils.debounce(
|
||||
() => this.highlightOccurrences(),
|
||||
25,
|
||||
);
|
||||
// Refresh highlighting when find text changes or changing options
|
||||
this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
|
||||
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
|
||||
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
|
||||
// Refresh highlighting when content changes
|
||||
editorSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);
|
||||
|
||||
// Last open changes trigger focus on text input and find occurence in selection
|
||||
this.$watch(() => this.lastOpen, () => {
|
||||
const elt = this.$el.querySelector(`.find-replace__text-input--${this.type}`);
|
||||
elt.focus();
|
||||
elt.setSelectionRange(0, this[`${this.type}Text`].length);
|
||||
// Highlight and find in selection
|
||||
this.state = null;
|
||||
this.debouncedHighlightOccurrences();
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
// Close on escape
|
||||
this.onKeyup = (evt) => {
|
||||
if (evt.which === 27) {
|
||||
// Esc key
|
||||
store.commit('findReplace/setType');
|
||||
}
|
||||
};
|
||||
window.addEventListener('keyup', this.onKeyup);
|
||||
|
||||
// Unselect class applier when focus is out of the panel
|
||||
this.onFocusIn = () => this.$el.contains(document.activeElement) ||
|
||||
setTimeout(() => this.unselectClassApplier(), 15);
|
||||
window.addEventListener('focusin', this.onFocusIn);
|
||||
},
|
||||
destroyed() {
|
||||
// Unregister listeners
|
||||
editorSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
||||
window.removeEventListener('keyup', this.onKeyup);
|
||||
window.removeEventListener('focusin', this.onFocusIn);
|
||||
this.state = 'destroyed';
|
||||
this.debouncedHighlightOccurrences();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.find-replace {
|
||||
padding: 0 35px 0 25px;
|
||||
}
|
||||
|
||||
.find-replace__row {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.find-replace__button {
|
||||
font-size: 15px;
|
||||
padding: 0 8px;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.find-replace__button--find-option {
|
||||
padding: 0;
|
||||
width: 28px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
text-transform: none;
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace__button--on {
|
||||
color: rgba(0, 0, 0, 0.67);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.67);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace__text-input {
|
||||
border: 1px solid transparent;
|
||||
padding: 2px 5px;
|
||||
height: 32px;
|
||||
|
||||
&:focus {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace__close-button {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
padding: 2px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace__find-stats {
|
||||
text-align: right;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.find-replace-highlighting {
|
||||
background-color: $highlighting-color;
|
||||
color: $editor-color-light !important;
|
||||
|
||||
.app--dark & {
|
||||
background-color: $dark-highlighting-color;
|
||||
}
|
||||
}
|
||||
|
||||
.find-replace-selection {
|
||||
background-color: $selection-highlighting-color;
|
||||
}
|
||||
</style>
|
@ -1,37 +1,57 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout" :class="{'layout--revision': revisionContent}">
|
||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{width: styles.layoutOverflow ? '100%' : constants.explorerWidth + 'px'}">
|
||||
<explorer></explorer>
|
||||
</div>
|
||||
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{ height: constants.navigationBarHeight + 'px' }">
|
||||
<div class="layout__panel flex flex--column" tour-step-anchor="welcome,end" :style="{width: styles.innerWidth + 'px'}">
|
||||
<div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{height: constants.navigationBarHeight + 'px'}">
|
||||
<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 + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||
<div class="layout__panel flex flex--row" :style="{height: styles.innerHeight + 'px'}">
|
||||
<div class="layout__panel layout__panel--editor" :class="editTheme" v-show="styles.showEditor" :style="{width: (styles.editorWidth + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
||||
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||
<div class="gutter__background" v-if="styles.editorGutterWidth" :style="{width: styles.editorGutterWidth + 'px'}"></div>
|
||||
</div>
|
||||
<editor></editor>
|
||||
<editor-in-page-buttons v-if="editorShowInPageButtons"></editor-in-page-buttons>
|
||||
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||
<sticky-comment v-if="styles.editorGutterWidth && stickyComment === 'top'"></sticky-comment>
|
||||
<current-discussion v-if="styles.editorGutterWidth"></current-discussion>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{width: constants.buttonBarWidth + 'px'}">
|
||||
<button-bar></button-bar>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{ width: styles.previewWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||
<div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{width: (styles.previewWidth + styles.previewGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
|
||||
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||
<div class="gutter__background" v-if="styles.previewGutterWidth" :style="{width: styles.previewGutterWidth + 'px'}"></div>
|
||||
</div>
|
||||
<preview></preview>
|
||||
<preview-in-page-buttons></preview-in-page-buttons>
|
||||
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||
<sticky-comment v-if="styles.previewGutterWidth && stickyComment === 'top'"></sticky-comment>
|
||||
<current-discussion v-if="styles.previewGutterWidth"></current-discussion>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--find-replace" v-if="showFindReplace">
|
||||
<find-replace></find-replace>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{ height: constants.statusBarHeight + 'px' }">
|
||||
<div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{height: constants.statusBarHeight + 'px'}">
|
||||
<status-bar></status-bar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{ width: constants.sideBarWidth + 'px' }">
|
||||
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{width: styles.layoutOverflow ? '100%' : constants.sideBarWidth + 'px'}">
|
||||
<side-bar></side-bar>
|
||||
</div>
|
||||
</div>
|
||||
<tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import NavigationBar from './NavigationBar';
|
||||
import ButtonBar from './ButtonBar';
|
||||
import StatusBar from './StatusBar';
|
||||
@ -39,7 +59,15 @@ import Explorer from './Explorer';
|
||||
import SideBar from './SideBar';
|
||||
import Editor from './Editor';
|
||||
import Preview from './Preview';
|
||||
import Tour from './Tour';
|
||||
import EditorInPageButtons from './EditorInPageButtons';
|
||||
import PreviewInPageButtons from './PreviewInPageButtons';
|
||||
import StickyComment from './gutters/StickyComment';
|
||||
import CurrentDiscussion from './gutters/CurrentDiscussion';
|
||||
import FindReplace from './FindReplace';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import markdownConversionSvc from '../services/markdownConversionSvc';
|
||||
import store from '../store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -50,24 +78,56 @@ export default {
|
||||
SideBar,
|
||||
Editor,
|
||||
Preview,
|
||||
Tour,
|
||||
EditorInPageButtons,
|
||||
PreviewInPageButtons,
|
||||
StickyComment,
|
||||
CurrentDiscussion,
|
||||
FindReplace,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
...mapState('discussion', [
|
||||
'stickyComment',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'constants',
|
||||
'styles',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'layoutSettings',
|
||||
]),
|
||||
...mapGetters('theme', [
|
||||
'currEditTheme',
|
||||
]),
|
||||
editTheme() {
|
||||
return `edit-theme--${this.currEditTheme || 'default'}`;
|
||||
},
|
||||
showFindReplace() {
|
||||
return !!store.state.findReplace.type;
|
||||
},
|
||||
editorShowInPageButtons() {
|
||||
return store.getters['data/computedSettings'].editor.showInPageButtons;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('layout', [
|
||||
...mapActions('layout', [
|
||||
'updateBodySize',
|
||||
]),
|
||||
saveSelection: () => editorSvc.saveSelection(true),
|
||||
},
|
||||
created() {
|
||||
markdownConversionSvc.init(); // Needs to be inited before mount
|
||||
this.updateBodySize();
|
||||
window.addEventListener('resize', this.updateBodySize);
|
||||
window.addEventListener('keyup', this.saveSelection);
|
||||
window.addEventListener('mouseup', this.saveSelection);
|
||||
window.addEventListener('focusin', this.saveSelection);
|
||||
window.addEventListener('contextmenu', this.saveSelection);
|
||||
},
|
||||
mounted() {
|
||||
@ -75,18 +135,28 @@ export default {
|
||||
const previewElt = this.$el.querySelector('.preview__inner-2');
|
||||
const tocElt = this.$el.querySelector('.toc__inner');
|
||||
editorSvc.init(editorElt, previewElt, tocElt);
|
||||
|
||||
// Focus on the editor every time reader mode is disabled
|
||||
const focus = () => {
|
||||
if (this.styles.showEditor) {
|
||||
editorSvc.clEditor.focus();
|
||||
}
|
||||
};
|
||||
setTimeout(focus, 100);
|
||||
this.$watch(() => this.styles.showEditor, focus);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.updateStyle);
|
||||
window.removeEventListener('keyup', this.saveSelection);
|
||||
window.removeEventListener('mouseup', this.saveSelection);
|
||||
window.removeEventListener('focusin', this.saveSelection);
|
||||
window.removeEventListener('contextmenu', this.saveSelection);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.layout {
|
||||
position: absolute;
|
||||
@ -99,10 +169,11 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layout__panel--navigation-bar {
|
||||
background-color: #2c2c2c;
|
||||
background-color: $navbar-bg;
|
||||
}
|
||||
|
||||
.layout__panel--status-bar {
|
||||
@ -110,16 +181,65 @@ export default {
|
||||
}
|
||||
|
||||
.layout__panel--editor {
|
||||
background-color: #fff;
|
||||
background-color: $editor-background-light;
|
||||
|
||||
.app--dark & {
|
||||
background-color: $editor-background-dark;
|
||||
}
|
||||
|
||||
.gutter__background,
|
||||
.comment-list__current-discussion,
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $editor-background-light, 6.7%);
|
||||
|
||||
.app--dark & {
|
||||
background-color: mix(#fff, $editor-background-dark, 6.7%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$preview-background-light: #f3f3f3;
|
||||
$preview-background-dark: #444;
|
||||
|
||||
.layout__panel--preview,
|
||||
.layout__panel--button-bar {
|
||||
background-color: $preview-background-light;
|
||||
|
||||
.app--dark & {
|
||||
background-color: $preview-background-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__panel--button-bar,
|
||||
.layout__panel--preview {
|
||||
background-color: #f3f3f3;
|
||||
.gutter__background,
|
||||
.comment-list__current-discussion,
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $preview-background-light, 6.7%);
|
||||
}
|
||||
}
|
||||
|
||||
.layout__panel--explorer,
|
||||
.layout__panel--side-bar {
|
||||
background-color: #dadada;
|
||||
background-color: #ddd;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #383c4a;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__panel--find-replace {
|
||||
background-color: #e6e6e6;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 300px;
|
||||
height: auto;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #4d5160;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,172 +1,294 @@
|
||||
<template>
|
||||
<div class="modal" @keyup.esc="onEscape">
|
||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
||||
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
||||
<google-drive-sync-modal v-else-if="config.type === 'googleDriveSync'"></google-drive-sync-modal>
|
||||
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
||||
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
||||
<dropbox-sync-modal v-else-if="config.type === 'dropboxSync'"></dropbox-sync-modal>
|
||||
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
||||
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
|
||||
<github-sync-modal v-else-if="config.type === 'githubSync'"></github-sync-modal>
|
||||
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
||||
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
||||
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
||||
<wordpress-publish-modal v-else-if="config.type === 'wordpressPublish'"></wordpress-publish-modal>
|
||||
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
|
||||
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
||||
<div v-else class="modal__inner-1">
|
||||
<div class="modal__inner-2">
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
<div class="modal__button-bar">
|
||||
<button v-if="config.rejectText" class="button" @click="config.reject()">{{config.rejectText}}</button>
|
||||
<button v-if="config.resolveText" class="button" @click="config.resolve()">{{config.resolveText}}</button>
|
||||
</div>
|
||||
<div class="modal" v-if="config" @keydown.esc.stop="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
|
||||
<!-- <div class="modal__sponsor-banner" v-if="!isSponsor">
|
||||
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/mafgwo/stackedit/">open source</a>, please consider
|
||||
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
|
||||
</div> -->
|
||||
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
|
||||
<modal-inner v-else aria-label="Dialog">
|
||||
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
|
||||
<button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
|
||||
<button v-for="(item, idx) in (simpleModal.resolveArray || [])" class="button button--resolve" @click="config.resolve(item.value)">{{item.text}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import simpleModals from '../data/simpleModals';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import giteeHelper from '../services/providers/helpers/giteeHelper';
|
||||
import store from '../store';
|
||||
|
||||
import ModalInner from './modals/common/ModalInner';
|
||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||
import SettingsModal from './modals/SettingsModal';
|
||||
import TemplatesModal from './modals/TemplatesModal';
|
||||
import AboutModal from './modals/AboutModal';
|
||||
import HtmlExportModal from './modals/HtmlExportModal';
|
||||
import PdfExportModal from './modals/PdfExportModal';
|
||||
import PandocExportModal from './modals/PandocExportModal';
|
||||
import LinkModal from './modals/LinkModal';
|
||||
import ImageModal from './modals/ImageModal';
|
||||
import GooglePhotoModal from './modals/GooglePhotoModal';
|
||||
import SyncManagementModal from './modals/SyncManagementModal';
|
||||
import PublishManagementModal from './modals/PublishManagementModal';
|
||||
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
|
||||
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
|
||||
import DropboxAccountModal from './modals/DropboxAccountModal';
|
||||
import DropboxSyncModal from './modals/DropboxSyncModal';
|
||||
import DropboxPublishModal from './modals/DropboxPublishModal';
|
||||
import GithubAccountModal from './modals/GithubAccountModal';
|
||||
import GithubSyncModal from './modals/GithubSyncModal';
|
||||
import GithubPublishModal from './modals/GithubPublishModal';
|
||||
import GistSyncModal from './modals/GistSyncModal';
|
||||
import GistPublishModal from './modals/GistPublishModal';
|
||||
import WordpressPublishModal from './modals/WordpressPublishModal';
|
||||
import BloggerPublishModal from './modals/BloggerPublishModal';
|
||||
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
|
||||
import ZendeskAccountModal from './modals/ZendeskAccountModal';
|
||||
import ZendeskPublishModal from './modals/ZendeskPublishModal';
|
||||
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
||||
import AccountManagementModal from './modals/AccountManagementModal';
|
||||
import BadgeManagementModal from './modals/BadgeManagementModal';
|
||||
import SponsorModal from './modals/SponsorModal';
|
||||
import CommitMessageModal from './modals/CommitMessageModal';
|
||||
import WorkspaceImgPathModal from './modals/WorkspaceImgPathModal';
|
||||
import ChatGptModal from './modals/ChatGptModal';
|
||||
|
||||
// Providers
|
||||
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
||||
import GoogleDriveAccountModal from './modals/providers/GoogleDriveAccountModal';
|
||||
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
||||
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
|
||||
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
||||
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
|
||||
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
|
||||
import DropboxPublishModal from './modals/providers/DropboxPublishModal';
|
||||
import GithubAccountModal from './modals/providers/GithubAccountModal';
|
||||
import GithubOpenModal from './modals/providers/GithubOpenModal';
|
||||
import GithubSaveModal from './modals/providers/GithubSaveModal';
|
||||
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
|
||||
import GithubPublishModal from './modals/providers/GithubPublishModal';
|
||||
import GithubImgStorageModal from './modals/providers/GithubImgStorageModal';
|
||||
import GistSyncModal from './modals/providers/GistSyncModal';
|
||||
import GistPublishModal from './modals/providers/GistPublishModal';
|
||||
import GiteeAccountModal from './modals/providers/GiteeAccountModal';
|
||||
import GiteeOpenModal from './modals/providers/GiteeOpenModal';
|
||||
import GiteeSaveModal from './modals/providers/GiteeSaveModal';
|
||||
import GiteeWorkspaceModal from './modals/providers/GiteeWorkspaceModal';
|
||||
import GiteePublishModal from './modals/providers/GiteePublishModal';
|
||||
import GiteeGistSyncModal from './modals/providers/GiteeGistSyncModal';
|
||||
import GiteeGistPublishModal from './modals/providers/GiteeGistPublishModal';
|
||||
import GitlabAccountModal from './modals/providers/GitlabAccountModal';
|
||||
import GitlabOpenModal from './modals/providers/GitlabOpenModal';
|
||||
import GitlabPublishModal from './modals/providers/GitlabPublishModal';
|
||||
import GitlabSaveModal from './modals/providers/GitlabSaveModal';
|
||||
import GitlabWorkspaceModal from './modals/providers/GitlabWorkspaceModal';
|
||||
import GiteaAccountModal from './modals/providers/GiteaAccountModal';
|
||||
import GiteaOpenModal from './modals/providers/GiteaOpenModal';
|
||||
import GiteaPublishModal from './modals/providers/GiteaPublishModal';
|
||||
import GiteaSaveModal from './modals/providers/GiteaSaveModal';
|
||||
import GiteaWorkspaceModal from './modals/providers/GiteaWorkspaceModal';
|
||||
import GiteaImgStorageModal from './modals/providers/GiteaImgStorageModal';
|
||||
import WordpressPublishModal from './modals/providers/WordpressPublishModal';
|
||||
import BloggerPublishModal from './modals/providers/BloggerPublishModal';
|
||||
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
|
||||
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
|
||||
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
|
||||
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
|
||||
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';
|
||||
import SmmsAccountModal from './modals/providers/SmmsAccountModal';
|
||||
import CustomAccountModal from './modals/providers/CustomAccountModal';
|
||||
|
||||
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield, input[type=checkbox]')
|
||||
// Filter enabled and visible element
|
||||
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
FilePropertiesModal,
|
||||
SettingsModal,
|
||||
TemplatesModal,
|
||||
AboutModal,
|
||||
HtmlExportModal,
|
||||
PdfExportModal,
|
||||
PandocExportModal,
|
||||
LinkModal,
|
||||
ImageModal,
|
||||
GooglePhotoModal,
|
||||
SyncManagementModal,
|
||||
PublishManagementModal,
|
||||
GoogleDriveSyncModal,
|
||||
WorkspaceManagementModal,
|
||||
AccountManagementModal,
|
||||
BadgeManagementModal,
|
||||
SponsorModal,
|
||||
CommitMessageModal,
|
||||
WorkspaceImgPathModal,
|
||||
ChatGptModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
GoogleDriveAccountModal,
|
||||
GoogleDriveSaveModal,
|
||||
GoogleDriveWorkspaceModal,
|
||||
GoogleDrivePublishModal,
|
||||
DropboxAccountModal,
|
||||
DropboxSyncModal,
|
||||
DropboxSaveModal,
|
||||
DropboxPublishModal,
|
||||
GithubAccountModal,
|
||||
GithubSyncModal,
|
||||
GithubOpenModal,
|
||||
GithubSaveModal,
|
||||
GithubWorkspaceModal,
|
||||
GithubPublishModal,
|
||||
GithubImgStorageModal,
|
||||
GistSyncModal,
|
||||
GistPublishModal,
|
||||
GiteeAccountModal,
|
||||
GiteeOpenModal,
|
||||
GiteeSaveModal,
|
||||
GiteeWorkspaceModal,
|
||||
GiteePublishModal,
|
||||
GiteeGistSyncModal,
|
||||
GiteeGistPublishModal,
|
||||
GitlabAccountModal,
|
||||
GitlabOpenModal,
|
||||
GitlabPublishModal,
|
||||
GitlabSaveModal,
|
||||
GitlabWorkspaceModal,
|
||||
GiteaAccountModal,
|
||||
GiteaOpenModal,
|
||||
GiteaPublishModal,
|
||||
GiteaSaveModal,
|
||||
GiteaWorkspaceModal,
|
||||
GiteaImgStorageModal,
|
||||
WordpressPublishModal,
|
||||
BloggerPublishModal,
|
||||
BloggerPagePublishModal,
|
||||
ZendeskAccountModal,
|
||||
ZendeskPublishModal,
|
||||
CouchdbWorkspaceModal,
|
||||
CouchdbCredentialsModal,
|
||||
SmmsAccountModal,
|
||||
CustomAccountModal,
|
||||
},
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
methods: {
|
||||
onEscape() {
|
||||
this.config.reject();
|
||||
editorEngineSvc.clEditor.focus();
|
||||
},
|
||||
onFocusInOut(evt) {
|
||||
const isFocusIn = evt.type === 'focusin';
|
||||
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
|
||||
// Focus effect
|
||||
if (evt.target.parentNode.classList.contains('form-entry__field') &&
|
||||
evt.target.parentNode.parentNode.classList.contains('form-entry')) {
|
||||
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'isSponsor',
|
||||
]),
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
currentModalComponent() {
|
||||
if (this.config.type) {
|
||||
let componentName = this.config.type[0].toUpperCase();
|
||||
componentName += this.config.type.slice(1);
|
||||
componentName += 'Modal';
|
||||
if (this.$options.components[componentName]) {
|
||||
return componentName;
|
||||
}
|
||||
}
|
||||
if (isFocusIn && this.config) {
|
||||
const modalInner = this.$el.querySelector('.modal__inner-2');
|
||||
let target = evt.target;
|
||||
while (target) {
|
||||
if (target === modalInner) {
|
||||
return;
|
||||
}
|
||||
target = target.parentNode;
|
||||
return null;
|
||||
},
|
||||
simpleModal() {
|
||||
return simpleModals[this.config.type] || {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async sponsor() {
|
||||
try {
|
||||
if (!store.getters['workspace/sponsorToken']) {
|
||||
// User has to sign in
|
||||
await store.dispatch('modal/open', 'signInForSponsorship');
|
||||
await giteeHelper.signin();
|
||||
await syncSvc.afterSignIn();
|
||||
syncSvc.requestSync();
|
||||
}
|
||||
if (!store.getters.isSponsor) {
|
||||
await store.dispatch('modal/open', 'sponsor');
|
||||
}
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
onEscape() {
|
||||
this.config.reject();
|
||||
editorSvc.clEditor.focus();
|
||||
},
|
||||
onTab(evt) {
|
||||
const tabbables = getTabbables(this.$el);
|
||||
const firstTabbable = tabbables[0];
|
||||
const lastTabbable = tabbables[tabbables.length - 1];
|
||||
if (evt.shiftKey && firstTabbable === evt.target) {
|
||||
evt.preventDefault();
|
||||
lastTabbable.focus();
|
||||
} else if (!evt.shiftKey && lastTabbable === evt.target) {
|
||||
evt.preventDefault();
|
||||
firstTabbable.focus();
|
||||
}
|
||||
},
|
||||
onFocusInOut(evt) {
|
||||
const { parentNode } = evt.target;
|
||||
if (parentNode && parentNode.parentNode) {
|
||||
// Focus effect
|
||||
if (parentNode.classList.contains('form-entry__field')
|
||||
&& parentNode.parentNode.classList.contains('form-entry')) {
|
||||
parentNode.parentNode.classList.toggle(
|
||||
'form-entry--focused',
|
||||
evt.type === 'focusin',
|
||||
);
|
||||
}
|
||||
this.config.reject();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('focusin', this.onFocusInOut);
|
||||
window.addEventListener('focusout', this.onFocusInOut);
|
||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
||||
|| this.$el.querySelector('.textfield')
|
||||
|| this.$el.querySelector('.button');
|
||||
if (eltToFocus) {
|
||||
eltToFocus.focus();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('focusin', this.onFocusInOut);
|
||||
window.removeEventListener('focusout', this.onFocusInOut);
|
||||
this.$watch(
|
||||
() => this.config,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
const tabbables = getTabbables(this.$el);
|
||||
if (tabbables[0]) {
|
||||
tabbables[0].focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(180, 180, 180, 0.75);
|
||||
background-color: rgba(160, 160, 160, 0.5);
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__sponsor-banner {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
color: darken($error-color, 10%);
|
||||
background-color: transparentize(lighten($error-color, 33%), 0.075);
|
||||
font-size: 0.9em;
|
||||
line-height: 1.33;
|
||||
text-align: center;
|
||||
padding: 0.25em 1em;
|
||||
}
|
||||
|
||||
.modal__inner-1 {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
max-width: 500px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.modal__inner-2 {
|
||||
margin: 50px 10px 100px;
|
||||
background-color: #fff;
|
||||
padding: 40px 50px 30px;
|
||||
margin: 40px 10px 100px;
|
||||
background-color: #f8f8f8;
|
||||
padding: 50px 50px 40px;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.app--dark & {
|
||||
background-color: #383c4a;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -174,7 +296,7 @@ export default {
|
||||
left: 0;
|
||||
height: $border-radius-base;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(to left, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
|
||||
background-image: linear-gradient(to left, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
@ -184,19 +306,20 @@ export default {
|
||||
left: 0;
|
||||
height: $border-radius-base;
|
||||
width: 100%;
|
||||
background-image: linear-gradient(to right, #ffe600, #ffe600 25%, #bbd500 25%, #bbd500 50%, #ff8a00 50%, #ff8a00 75%, #75b7fd 75%);
|
||||
background-image: linear-gradient(to right, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
|
||||
}
|
||||
}
|
||||
|
||||
.modal__content :first-child {
|
||||
.modal__content > :first-child,
|
||||
.modal__content > .modal__image:first-child + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal__image {
|
||||
float: left;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 1.5em 1.5em 0.5em 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 1.5em 1.2em 0.5em 0;
|
||||
|
||||
& + *::after {
|
||||
content: '';
|
||||
@ -205,24 +328,46 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.modal__title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.4;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.modal__sub-title {
|
||||
opacity: 0.6;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal__error {
|
||||
color: #de2c00;
|
||||
}
|
||||
|
||||
.modal__tip {
|
||||
background-color: transparentize(#ffd600, 0.85);
|
||||
.modal__info {
|
||||
background-color: $info-bg;
|
||||
border-radius: $border-radius-base;
|
||||
margin: 1.2em 0;
|
||||
padding: 0.75em 1.25em;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.6;
|
||||
|
||||
pre {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.modal__info--multiline {
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
.modal__button-bar {
|
||||
margin-top: 1.75rem;
|
||||
text-align: right;
|
||||
|
||||
.button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-entry {
|
||||
@ -232,21 +377,35 @@ export default {
|
||||
.form-entry__label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: #a0a0a0;
|
||||
color: #808080;
|
||||
|
||||
.form-entry--focused & {
|
||||
color: darken($link-color, 10%);
|
||||
}
|
||||
|
||||
.form-entry--error & {
|
||||
color: darken($error-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__label-info {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-entry__field {
|
||||
border: 1px solid #d8d8d8;
|
||||
border: 1px solid #b0b0b0;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.form-entry--focused & {
|
||||
border-color: $link-color;
|
||||
box-shadow: 0 0 0 2.5px transparentize($link-color, 0.67);
|
||||
}
|
||||
|
||||
.form-entry--error & {
|
||||
border-color: $error-color;
|
||||
box-shadow: 0 0 0 2.5px transparentize($error-color, 0.67);
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,8 +441,49 @@ export default {
|
||||
|
||||
.form-entry__info {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
opacity: 0.67;
|
||||
line-height: 1.4;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-bottom: 1px solid $hr-color;
|
||||
margin: 1em 0 2em;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__tab {
|
||||
width: 50%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.tabs__tab > a {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
padding: 0.67em 0.33em;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-top-left-radius: $border-radius-base;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
color: $link-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs__tab--active > a {
|
||||
border-bottom: 2px solid $link-color;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,85 +1,84 @@
|
||||
<template>
|
||||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent, 'navigation-bar--light': light}">
|
||||
<!-- Explorer -->
|
||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||
<icon-folder></icon-folder>
|
||||
</button>
|
||||
<button class="navigation-bar__button navigation-bar__button--close button" v-if="light" @click="close()" v-title="'关闭StackEdit'"><icon-check-circle></icon-check-circle></button>
|
||||
<button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'切换资源管理器'"><icon-folder></icon-folder></button>
|
||||
</div>
|
||||
<!-- Side bar -->
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
|
||||
<icon-provider provider-id="stackedit"></icon-provider>
|
||||
</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>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
||||
<!-- Spinner -->
|
||||
<div class="navigation-bar__spinner">
|
||||
<div v-if="!offline && showSpinner" class="spinner"></div>
|
||||
<icon-sync-off v-if="offline"></icon-sync-off>
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
|
||||
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
|
||||
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
|
||||
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle(false)" @keydown.esc.stop="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
|
||||
<!-- Sync/Publish -->
|
||||
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync">
|
||||
<icon-sync></icon-sync>
|
||||
</button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish">
|
||||
<icon-upload></icon-upload>
|
||||
</button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'同步位置'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'立即同步'"><icon-sync></icon-sync></button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank" v-title="'发布位置'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish" v-title="'立即发布'"><icon-upload></icon-upload></button>
|
||||
</div>
|
||||
<!-- Revision -->
|
||||
<div class="flex flex--row" v-if="revisionContent">
|
||||
<button class="navigation-bar__button navigation-bar__button--revision navigation-bar__button--restore button" @click="restoreRevision">恢复</button>
|
||||
<button class="navigation-bar__button navigation-bar__button--revision button" @click="setRevisionContent()" v-title="'关闭修订'"><icon-close></icon-close></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
||||
<icon-format-bold></icon-format-bold>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('italic')">
|
||||
<icon-format-italic></icon-format-italic>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')">
|
||||
<icon-format-strikethrough></icon-format-strikethrough>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('heading')">
|
||||
<icon-format-size></icon-format-size>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('ulist')">
|
||||
<icon-format-list-bulleted></icon-format-list-bulleted>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('olist')">
|
||||
<icon-format-list-numbers></icon-format-list-numbers>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('table')">
|
||||
<icon-table></icon-table>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('quote')">
|
||||
<icon-format-quote-close></icon-format-quote-close>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('code')">
|
||||
<icon-code-tags></icon-code-tags>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('link')">
|
||||
<icon-link-variant></icon-link-variant>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('image')">
|
||||
<icon-file-image></icon-file-image>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('hr')">
|
||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||
</button>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons">
|
||||
<button class="navigation-bar__button button" @click="undo" v-title="'回退'" :disabled="!canUndo"><icon-undo></icon-undo></button>
|
||||
<button class="navigation-bar__button button" @click="redo" v-title="'重做'" :disabled="!canRedo"><icon-redo></icon-redo></button>
|
||||
<div v-for="button in pagedownButtons" :key="button.method">
|
||||
<button class="navigation-bar__button button" v-if="button.method" @click="pagedownClick(button.method)" v-title="button.titleWithShortcut">
|
||||
<component :is="button.iconClass"></component>
|
||||
</button>
|
||||
<div class="navigation-bar__spacer" v-else></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import utils from '../services/utils';
|
||||
import pagedownButtons from '../data/pagedownButtons';
|
||||
import store from '../store';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
|
||||
// According to mousetrap
|
||||
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
||||
|
||||
const getShortcut = (method) => {
|
||||
let result = '';
|
||||
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
|
||||
if (`${shortcut.method || shortcut}` === method) {
|
||||
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
|
||||
if (key === 'mod') {
|
||||
return mod;
|
||||
}
|
||||
// Capitalize
|
||||
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
|
||||
}).join('+');
|
||||
}
|
||||
return result;
|
||||
});
|
||||
return result && ` – ${result}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
@ -90,6 +89,7 @@ export default {
|
||||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
'offline',
|
||||
]),
|
||||
...mapState('queue', [
|
||||
@ -97,6 +97,13 @@ export default {
|
||||
'isPublishRequested',
|
||||
'currentLocation',
|
||||
]),
|
||||
...mapState('layout', [
|
||||
'canUndo',
|
||||
'canRedo',
|
||||
]),
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
@ -106,12 +113,20 @@ export default {
|
||||
...mapGetters('publishLocation', {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
pagedownButtons() {
|
||||
const buttonShowObj = store.getters['data/computedSettings'].editor.headButtons;
|
||||
return pagedownButtons.filter(it => buttonShowObj[it.method]).map(button => ({
|
||||
...button,
|
||||
titleWithShortcut: `${button.title}${getShortcut(button.method)}`,
|
||||
iconClass: `icon-${button.icon}`,
|
||||
}));
|
||||
},
|
||||
isSyncPossible() {
|
||||
return this.$store.getters['data/loginToken'] ||
|
||||
this.$store.getters['syncLocation/current'].length;
|
||||
return store.getters['workspace/syncToken'] ||
|
||||
store.getters['syncLocation/current'].length;
|
||||
},
|
||||
showSpinner() {
|
||||
return !this.$store.state.queue.isEmpty;
|
||||
return !store.state.queue.isEmpty;
|
||||
},
|
||||
titleWidth() {
|
||||
if (!this.mounted) {
|
||||
@ -119,9 +134,7 @@ export default {
|
||||
}
|
||||
this.titleFakeElt.textContent = this.title;
|
||||
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
|
||||
return width < this.styles.titleMaxWidth
|
||||
? width
|
||||
: this.styles.titleMaxWidth;
|
||||
return Math.min(width, this.styles.titleMaxWidth);
|
||||
},
|
||||
titleScrolling() {
|
||||
const result = this.titleHover && !this.titleFocus;
|
||||
@ -141,15 +154,34 @@ export default {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
editCancelTrigger() {
|
||||
const current = store.getters['file/current'];
|
||||
return utils.serializeObject([
|
||||
current.id,
|
||||
current.name,
|
||||
]);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('content', [
|
||||
'setRevisionContent',
|
||||
]),
|
||||
...mapActions('content', [
|
||||
'restoreRevision',
|
||||
]),
|
||||
...mapActions('data', [
|
||||
'toggleExplorer',
|
||||
'toggleSideBar',
|
||||
]),
|
||||
undo() {
|
||||
return editorSvc.clEditor.undoMgr.undo();
|
||||
},
|
||||
redo() {
|
||||
return editorSvc.clEditor.undoMgr.redo();
|
||||
},
|
||||
requestSync() {
|
||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||
syncSvc.requestSync();
|
||||
syncSvc.requestSync(true);
|
||||
}
|
||||
},
|
||||
requestPublish() {
|
||||
@ -157,23 +189,35 @@ export default {
|
||||
publishSvc.requestPublish();
|
||||
}
|
||||
},
|
||||
switchTheme() {
|
||||
store.dispatch('data/switchThemeSetting');
|
||||
},
|
||||
pagedownClick(name) {
|
||||
if (this.$store.getters['content/current'].id &&
|
||||
this.$store.getters['layout/styles'].showEditor
|
||||
) {
|
||||
if (store.getters['content/isCurrentEditable']) {
|
||||
const text = editorSvc.clEditor.getContent();
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
if (text !== editorSvc.clEditor.getContent()) {
|
||||
badgeSvc.addBadge('formatButtons');
|
||||
}
|
||||
}
|
||||
},
|
||||
editTitle(toggle) {
|
||||
async editTitle(toggle) {
|
||||
this.titleFocus = toggle;
|
||||
if (toggle) {
|
||||
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
|
||||
} else {
|
||||
const title = this.title.trim();
|
||||
if (title) {
|
||||
this.$store.dispatch('file/patchCurrent', { name: title.slice(0, 250) });
|
||||
} else {
|
||||
this.title = this.$store.getters['file/current'].name;
|
||||
this.title = store.getters['file/current'].name;
|
||||
if (title && this.title !== title) {
|
||||
try {
|
||||
await workspaceSvc.storeItem({
|
||||
...store.getters['file/current'],
|
||||
name: title,
|
||||
});
|
||||
badgeSvc.addBadge('editCurrentFileName');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -183,13 +227,19 @@ export default {
|
||||
}
|
||||
this.titleInputElt.blur();
|
||||
},
|
||||
close() {
|
||||
tempFileSvc.close();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.watch(
|
||||
() => this.$store.getters['file/current'].name,
|
||||
(name) => {
|
||||
this.title = name;
|
||||
}, { immediate: true });
|
||||
this.$watch(
|
||||
() => this.editCancelTrigger,
|
||||
() => {
|
||||
this.title = '';
|
||||
this.editTitle(false);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
},
|
||||
mounted() {
|
||||
this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
|
||||
@ -200,7 +250,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.navigation-bar {
|
||||
position: absolute;
|
||||
@ -218,22 +268,26 @@ export default {
|
||||
float: left;
|
||||
|
||||
&.navigation-bar__inner--button {
|
||||
margin-right: 15px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__inner--right {
|
||||
float: right;
|
||||
|
||||
/* prevent from seeing wrapped pagedownButtons */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.navigation-bar__inner--button {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.navigation-bar__inner--edit-buttons {
|
||||
.navigation-bar__inner--edit-pagedownButtons {
|
||||
margin-left: 15px;
|
||||
|
||||
.navigation-bar__button {
|
||||
.navigation-bar__button,
|
||||
.navigation-bar__spacer {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
@ -242,18 +296,36 @@ export default {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
width: 36px;
|
||||
.navigation-bar__button,
|
||||
.navigation-bar__spacer {
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
padding: 0 4px;
|
||||
|
||||
/* prevent from seeing wrapped buttons */
|
||||
/* prevent from seeing wrapped pagedownButtons */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
width: 34px;
|
||||
padding: 0 7px;
|
||||
transition: opacity 0.25s;
|
||||
|
||||
.navigation-bar__inner--button & {
|
||||
padding: 0 4px;
|
||||
width: 38px;
|
||||
|
||||
&.navigation-bar__button--theme {
|
||||
width: 34px;
|
||||
padding: 0 7px;
|
||||
opacity: 0.85;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.navigation-bar__button--stackedit {
|
||||
opacity: 0.85;
|
||||
|
||||
@ -266,8 +338,30 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--revision {
|
||||
width: 38px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--restore {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.navigation-bar__title {
|
||||
margin: 0 4px;
|
||||
font-size: 21px;
|
||||
|
||||
.layout--revision & {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title,
|
||||
@ -275,7 +369,6 @@ export default {
|
||||
display: inline-block;
|
||||
color: $navbar-color;
|
||||
background-color: transparent;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.navigation-bar__button--sync,
|
||||
@ -342,9 +435,7 @@ export default {
|
||||
}
|
||||
|
||||
.navigation-bar__title--input,
|
||||
.navigation-bar__inner--edit-buttons,
|
||||
.navigation-bar__inner--button,
|
||||
.navigation-bar__spinner {
|
||||
.navigation-bar__inner--edit-pagedownButtons {
|
||||
display: none;
|
||||
|
||||
.navigation-bar--editor & {
|
||||
@ -355,32 +446,50 @@ export default {
|
||||
.navigation-bar__button {
|
||||
display: none;
|
||||
|
||||
.navigation-bar__inner--button &,
|
||||
.navigation-bar--editor & {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--revision {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.navigation-bar__button--close {
|
||||
color: lighten($link-color, 15%);
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: lighten($link-color, 25%);
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title--input {
|
||||
cursor: pointer;
|
||||
|
||||
&.navigation-bar__title--focus {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.navigation-bar--light & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
$r: 9px;
|
||||
$r: 10px;
|
||||
$d: $r * 2;
|
||||
$b: $d/10;
|
||||
$t: 3000ms;
|
||||
|
||||
.navigation-bar__spinner {
|
||||
width: 22px;
|
||||
margin: 8px 0 0 8px;
|
||||
color: #b2b2b2;
|
||||
width: 24px;
|
||||
margin: 7px 0 0 8px;
|
||||
|
||||
.icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: transparentize($error-color, 0.5);
|
||||
}
|
||||
}
|
||||
@ -390,7 +499,7 @@ $t: 3000ms;
|
||||
height: $d;
|
||||
display: block;
|
||||
position: relative;
|
||||
border: $b solid currentColor;
|
||||
border: $b solid transparentize($navbar-color, 0.5);
|
||||
border-radius: 50%;
|
||||
margin: 2px;
|
||||
|
||||
@ -400,20 +509,20 @@ $t: 3000ms;
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: $b;
|
||||
background-color: currentColor;
|
||||
background-color: $navbar-color;
|
||||
border-radius: $b * 0.5;
|
||||
transform-origin: 50% 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
height: $r * 0.35;
|
||||
height: $r * 0.4;
|
||||
left: $r - $b * 1.5;
|
||||
top: 50%;
|
||||
animation: spin $t linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: $r * 0.5;
|
||||
height: $r * 0.6;
|
||||
left: $r - $b * 1.5;
|
||||
top: 50%;
|
||||
animation: spin $t/4 linear infinite;
|
||||
|
@ -2,12 +2,19 @@
|
||||
<div class="notification">
|
||||
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
||||
<div class="notification__icon flex flex--column flex--center">
|
||||
<icon-information v-if="item.type === 'info'"></icon-information>
|
||||
<icon-alert v-else-if="item.type === 'error'"></icon-alert>
|
||||
<icon-alert v-if="item.type === 'error'"></icon-alert>
|
||||
<icon-check-circle v-else-if="item.type === 'badge'"></icon-check-circle>
|
||||
<icon-information v-else></icon-information>
|
||||
</div>
|
||||
<div class="notification__content">
|
||||
{{item.content}}
|
||||
</div>
|
||||
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.reject">
|
||||
否
|
||||
</button>
|
||||
<button class="notification__button button" v-if="item.type === 'confirm'" @click="item.resolve">
|
||||
是
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,7 +30,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.notification {
|
||||
position: absolute;
|
||||
@ -49,4 +56,17 @@ export default {
|
||||
margin-right: 12px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.notification__button {
|
||||
color: $navbar-color;
|
||||
padding: 8px;
|
||||
flex: none;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $navbar-hover-color;
|
||||
background-color: $navbar-hover-background;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,13 +1,17 @@
|
||||
<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>
|
||||
<preview-new-discussion-button v-if="!isCurrentTemp"></preview-new-discussion-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!styles.showEditor" class="preview__button-bar">
|
||||
<div class="preview__button" @click="toggleEditor(true)">
|
||||
<div v-if="!styles.showEditor" class="preview__corner">
|
||||
<button class="preview__button button" @click="toggleEditor(true)" v-title="'编辑文件'">
|
||||
<icon-pen></icon-pen>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -15,16 +19,34 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import CommentList from './gutters/CommentList';
|
||||
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
|
||||
import store from '../store';
|
||||
|
||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CommentList,
|
||||
PreviewNewDiscussionButton,
|
||||
},
|
||||
data: () => ({
|
||||
previewTop: true,
|
||||
}),
|
||||
computed: mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
computed: {
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('theme', [
|
||||
'currPreviewTheme',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
previewTheme() {
|
||||
return `preview-theme--${this.currPreviewTheme || 'default'}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
'toggleEditor',
|
||||
@ -46,11 +68,51 @@ export default {
|
||||
this.previewTop = evt.target.scrollTop < 10;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const previewElt = this.$el.querySelector('.preview__inner-2');
|
||||
const onDiscussionEvt = cb => (evt) => {
|
||||
let elt = evt.target;
|
||||
while (elt && elt !== previewElt) {
|
||||
if (elt.discussionId) {
|
||||
cb(elt.discussionId);
|
||||
return;
|
||||
}
|
||||
elt = elt.parentNode;
|
||||
}
|
||||
};
|
||||
|
||||
const classToggler = toggle => (discussionId) => {
|
||||
previewElt.getElementsByClassName(`discussion-preview-highlighting--${discussionId}`)
|
||||
.cl_each(elt => elt.classList.toggle('discussion-preview-highlighting--hover', toggle));
|
||||
document.getElementsByClassName(`comment--discussion-${discussionId}`)
|
||||
.cl_each(elt => elt.classList.toggle('comment--hover', toggle));
|
||||
};
|
||||
|
||||
previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
|
||||
previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
|
||||
previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
|
||||
store.commit('discussion/setCurrentDiscussionId', discussionId);
|
||||
}));
|
||||
|
||||
this.$watch(
|
||||
() => store.state.discussion.currentDiscussionId,
|
||||
(discussionId, oldDiscussionId) => {
|
||||
if (oldDiscussionId) {
|
||||
previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`)
|
||||
.cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
|
||||
}
|
||||
if (discussionId) {
|
||||
previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
|
||||
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.preview,
|
||||
.preview__inner-1 {
|
||||
@ -71,23 +133,49 @@ export default {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.preview__button-bar {
|
||||
$corner-size: 110px;
|
||||
|
||||
.preview__corner {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 26px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-top: $corner-size solid rgba(0, 0, 0, 0.075);
|
||||
border-left: $corner-size solid transparent;
|
||||
pointer-events: none;
|
||||
|
||||
.app--dark & {
|
||||
border-top-color: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview__button {
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 5px;
|
||||
border-radius: $border-radius-base;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
color: rgba(0, 0, 0, 0.33);
|
||||
background-color: transparent;
|
||||
|
||||
.app--dark & {
|
||||
color: rgba(255, 255, 255, 0.33);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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>
|
@ -1,25 +1,30 @@
|
||||
<template>
|
||||
<div class="side-bar flex flex--column">
|
||||
<div class="side-title flex flex--row">
|
||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')">
|
||||
<icon-arrow-left></icon-arrow-left>
|
||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'主菜单'">
|
||||
<icon-dots-horizontal></icon-dots-horizontal>
|
||||
</button>
|
||||
<div class="side-title__title">
|
||||
{{panelName}}
|
||||
</div>
|
||||
<button class="side-title__button button" @click="toggleSideBar(false)">
|
||||
<button class="side-title__button button" @click="toggleSideBar(false)" v-title="'关闭侧边栏'">
|
||||
<icon-close></icon-close>
|
||||
</button>
|
||||
</div>
|
||||
<div class="side-bar__inner">
|
||||
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||
<workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
|
||||
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||
<more-menu v-else-if="panel === 'more'"></more-menu>
|
||||
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
|
||||
<workspace-backup-menu v-else-if="panel === 'workspaceBackups'"></workspace-backup-menu>
|
||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||
</div>
|
||||
<edit-theme-menu v-else-if="panel === 'editTheme'"></edit-theme-menu>
|
||||
<preview-theme-menu v-else-if="panel === 'previewTheme'"></preview-theme-menu>
|
||||
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
|
||||
<toc>
|
||||
</toc>
|
||||
@ -31,41 +36,56 @@
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Toc from './Toc';
|
||||
import MenuEntry from './menus/MenuEntry';
|
||||
import MainMenu from './menus/MainMenu';
|
||||
import WorkspacesMenu from './menus/WorkspacesMenu';
|
||||
import SyncMenu from './menus/SyncMenu';
|
||||
import PublishMenu from './menus/PublishMenu';
|
||||
import ExportMenu from './menus/ExportMenu';
|
||||
import MoreMenu from './menus/MoreMenu';
|
||||
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';
|
||||
|
||||
const panelNames = {
|
||||
menu: 'Menu',
|
||||
help: 'Markdown cheat sheet',
|
||||
toc: 'Table of contents',
|
||||
sync: 'Synchronize',
|
||||
publish: 'Publish',
|
||||
export: 'Export to disk',
|
||||
more: 'More',
|
||||
menu: '菜单',
|
||||
workspaces: '文档空间',
|
||||
help: 'Markdown 帮助',
|
||||
toc: '目录',
|
||||
sync: '同步',
|
||||
publish: '发布',
|
||||
history: '文件历史',
|
||||
importExport: '导入/导出',
|
||||
workspaceBackups: '文档空间备份',
|
||||
editTheme: '编辑区主题',
|
||||
previewTheme: '预览区主题',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Toc,
|
||||
MenuEntry,
|
||||
MainMenu,
|
||||
WorkspacesMenu,
|
||||
SyncMenu,
|
||||
PublishMenu,
|
||||
ExportMenu,
|
||||
MoreMenu,
|
||||
HistoryMenu,
|
||||
ImportExportMenu,
|
||||
WorkspaceBackupMenu,
|
||||
EditThemeMenu,
|
||||
PreviewThemeMenu,
|
||||
},
|
||||
data: () => ({
|
||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||
}),
|
||||
computed: {
|
||||
panel() {
|
||||
return this.$store.getters['data/localSettings'].sideBarPanel;
|
||||
if (store.state.light) {
|
||||
return null; // No menu in light mode
|
||||
}
|
||||
const result = store.getters['data/layoutSettings'].sideBarPanel;
|
||||
return panelNames[result] ? result : 'menu';
|
||||
},
|
||||
panelName() {
|
||||
return panelNames[this.panel];
|
||||
@ -83,7 +103,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.side-bar {
|
||||
overflow: hidden;
|
||||
@ -92,6 +112,7 @@ export default {
|
||||
hr {
|
||||
margin: 10px 40px;
|
||||
display: none;
|
||||
border-top: 1px solid $hr-color;
|
||||
}
|
||||
|
||||
* + hr {
|
||||
@ -101,6 +122,11 @@ export default {
|
||||
hr + hr {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.textfield {
|
||||
font-size: 14px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar__inner {
|
||||
@ -113,6 +139,12 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar__panel--hidden {
|
||||
@ -120,11 +152,11 @@ export default {
|
||||
}
|
||||
|
||||
.side-bar__panel--menu {
|
||||
padding: 10px 10px 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.side-bar__panel--help {
|
||||
padding: 0 10px 40px 20px;
|
||||
padding: 0 10px 0 20px;
|
||||
|
||||
pre {
|
||||
font-size: 0.9em;
|
||||
@ -143,15 +175,17 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.side-bar__warning {
|
||||
.side-bar__info {
|
||||
padding: 10px;
|
||||
margin: 0 -10px;
|
||||
color: darken($error-color, 10);
|
||||
background-color: transparentize($error-color, 0.925);
|
||||
margin: -10px -10px 10px;
|
||||
background-color: $info-bg;
|
||||
font-size: 0.95em;
|
||||
|
||||
p {
|
||||
margin: 10px;
|
||||
line-height: 1.4;
|
||||
margin: 10px 15px;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.67;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|