Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2ae94df34 | ||
|
|
f799613b1a | ||
|
|
3f057a01d8 | ||
|
|
3ad5136735 | ||
|
|
e201ce6f83 | ||
|
|
383a3a7d4c | ||
|
|
8261f1de1b | ||
|
|
5b38e6fa56 | ||
|
|
717f34bc85 | ||
|
|
47fb2a90a9 | ||
|
|
85a7221895 | ||
|
|
9618a89528 | ||
|
|
14f0af2754 | ||
|
|
ebe248670d | ||
|
|
511960c4a9 | ||
|
|
31fd1f93ce | ||
|
|
6625b69170 | ||
|
|
9e8533fbb8 | ||
|
|
9c9cbb7dcb | ||
|
|
079a13e161 | ||
|
|
69c1e587d0 | ||
|
|
3996252531 | ||
|
|
4fddda65e6 | ||
|
|
5916344092 | ||
|
|
b96722dd69 | ||
|
|
263ccab311 | ||
|
|
3571af82c7 | ||
|
|
c60520c0ff | ||
|
|
b473431eae | ||
|
|
cbf434f741 | ||
|
|
04c401207f | ||
|
|
7291e6aac6 | ||
|
|
a7aab96f0e | ||
|
|
f500749644 | ||
|
|
47e59bc54c | ||
|
|
8902e25021 | ||
|
|
33093e1eb4 | ||
|
|
d36178c44f | ||
|
|
15b09ccc75 | ||
|
|
dffa6c87a0 | ||
|
|
c4a1caee09 | ||
|
|
1d9f07b86d | ||
|
|
a794a95bb8 | ||
|
|
40a56f6057 | ||
|
|
82353f7b64 | ||
|
|
82a22da90a | ||
|
|
380e40ea05 | ||
|
|
2bedb23341 | ||
|
|
1110b76364 | ||
|
|
d19e632f80 | ||
|
|
4e040b5f7a | ||
|
|
7a2a0934c2 | ||
|
|
d3eb7b223c | ||
|
|
f74eec954f | ||
|
|
864c4e7aa6 | ||
|
|
5667906caf | ||
|
|
2fe7c524e7 | ||
|
|
5cc83526ad | ||
|
|
76b5e99a08 | ||
|
|
7d5505d421 | ||
|
|
d97a11f8b5 | ||
|
|
0590cec684 | ||
|
|
b4cdc4feb9 | ||
|
|
986fd95524 | ||
|
|
f51fec5fb9 | ||
|
|
8198ce2af3 | ||
|
|
defffc4c8e | ||
|
|
8f47cbfb0b | ||
|
|
0be91c17d0 | ||
|
|
fae7ab8417 | ||
|
|
df239f2cc0 | ||
|
|
70099dc97f | ||
|
|
c11be0e3ec | ||
|
|
f8b7870180 | ||
|
|
c58a5c62d9 | ||
|
|
ce92444bf2 | ||
|
|
b2709ebffd | ||
|
|
2b20cf9d24 | ||
|
|
f4a499ad0f | ||
|
|
4494b158c0 | ||
|
|
3416e55264 | ||
|
|
9b53e31cd5 | ||
|
|
f49b93b042 | ||
|
|
a005930c14 | ||
|
|
9dea1ee6ed | ||
|
|
1ea0999d17 | ||
|
|
2e6a466ba5 | ||
|
|
fbcd430dc6 | ||
|
|
2bd910db47 | ||
|
|
8158c9a483 | ||
|
|
4622fbe757 | ||
|
|
97a635c2de | ||
|
|
e8c67c75fd | ||
|
|
6151ecb4c1 | ||
|
|
1d8c192c07 | ||
|
|
014911b7a9 | ||
|
|
7b2d7b8aa5 | ||
|
|
caceed2d4c | ||
|
|
476ec6c2fd | ||
|
|
5374edfa01 | ||
|
|
6a0cac9dfb | ||
|
|
b525a6a211 | ||
|
|
c295761560 | ||
|
|
6154164b4b | ||
|
|
05ce2d3537 | ||
|
|
544f24bcba | ||
|
|
1c354ed976 | ||
|
|
9b9bf1bfbe | ||
|
|
29d5f02d10 | ||
|
|
9bc2fbc141 | ||
|
|
cac009b773 | ||
|
|
bf1639cac7 | ||
|
|
5f28e145c4 | ||
|
|
8199ab964e | ||
|
|
14c050e34b | ||
|
|
b96411d1f9 | ||
|
|
87c469a6c7 | ||
|
|
c29aef03f0 | ||
|
|
9eaf94de7a | ||
|
|
f5e7526fae | ||
|
|
579e4925c8 | ||
|
|
4a4c558d9d | ||
|
|
9bb34850e2 | ||
|
|
c6e37532bc | ||
|
|
221f962beb | ||
|
|
17566279e2 | ||
|
|
4e7488414d | ||
|
|
c206817431 |
7
.github/ISSUE_TEMPLATE/feedback_request.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Feedback
|
||||
about: Tell more on what you think
|
||||
title: 'Feedback: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
3
.github/workflows/release-beta.yml
vendored
@@ -25,3 +25,6 @@ jobs:
|
||||
- name: Publish
|
||||
run: npx vsce publish -p ${{ secrets.VSCE_PAT }} --baseImagesUrl https://raw.githubusercontent.com/estruyf/vscode-front-matter/dev
|
||||
|
||||
- name: Publish to open-vsx.org
|
||||
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -26,3 +26,5 @@ jobs:
|
||||
- name: Publish
|
||||
run: npx vsce publish -p ${{ secrets.VSCE_PAT }}
|
||||
|
||||
- name: Publish to open-vsx.org
|
||||
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_PAT }}
|
||||
99
CHANGELOG.md
@@ -1,5 +1,104 @@
|
||||
# Change Log
|
||||
|
||||
## [5.5.0] - 2021-11-15
|
||||
|
||||
As from this version onwards, the extension will be published to [open-vsx.org](https://open-vsx.org/).
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#173](https://github.com/estruyf/vscode-front-matter/issues/173): Allow to specify your own sorting for the content dashboard
|
||||
- [#174](https://github.com/estruyf/vscode-front-matter/issues/174): Added option to exclude sub-directories from page/markdown content retrieval
|
||||
|
||||
## [5.4.0] - 2021-11-05
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#166](https://github.com/estruyf/vscode-front-matter/issues/166): Added preview button to the panel base view
|
||||
- [#167](https://github.com/estruyf/vscode-front-matter/issues/167): Allow to set the preview path per content type
|
||||
|
||||
## [5.3.1] - 2021-10-29
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#163](https://github.com/estruyf/vscode-front-matter/issues/163): Setting workspace state instead of global state for the media view
|
||||
|
||||
## [5.3.0] - 2021-10-28 - [Release Notes](https://beta.frontmatter.codes/updates/v5.3.0)
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#158](https://github.com/estruyf/vscode-front-matter/issues/158): Add support for non-boolean draft/publish status fields
|
||||
- [#159](https://github.com/estruyf/vscode-front-matter/issues/159): Enhancements to SEO checks: Slug check, keyword details, more article information
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Value check when generating slug from title
|
||||
- Fix for date time formatting with `DD` and `YYYY` tokens
|
||||
- Fix in tag space replacing when object is passed
|
||||
|
||||
## [5.2.0] - 2021-10-19
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#151](https://github.com/estruyf/vscode-front-matter/issues/151): Detect which site-generator or framework is used
|
||||
- [#152](https://github.com/estruyf/vscode-front-matter/issues/152): Automatically set setting based on the used site-generator or framework
|
||||
- [#154](https://github.com/estruyf/vscode-front-matter/issues/154): Bulk script support added
|
||||
- [#155](https://github.com/estruyf/vscode-front-matter/issues/155): Fallback image added for the images shown in the editor panel
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#153](https://github.com/estruyf/vscode-front-matter/issues/153): Support old date formatting for date-fns
|
||||
- [#156](https://github.com/estruyf/vscode-front-matter/issues/156): Fix for uploading media files into a new folder
|
||||
|
||||
## [5.1.1] - 2021-10-14
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#149](https://github.com/estruyf/vscode-front-matter/issues/149): Fix panel rendering when incorrect type for keywords is provided
|
||||
|
||||
## [5.1.0] - 2021-10-13
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#141](https://github.com/estruyf/vscode-front-matter/issues/141): Allow content creation for page bundles or single files
|
||||
- [#145](https://github.com/estruyf/vscode-front-matter/issues/145): Moved folder registration settings to `frontmatter.json` file
|
||||
- [#147](https://github.com/estruyf/vscode-front-matter/issues/147): Error boundary added for metadata fields
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Rendered more hooks than during the previous render in `FileList`
|
||||
- [#142](https://github.com/estruyf/vscode-front-matter/issues/142): Fix for unknown tags where it throws an error
|
||||
- [#143](https://github.com/estruyf/vscode-front-matter/issues/143): Fix for duplicate values in the file list
|
||||
- [#144](https://github.com/estruyf/vscode-front-matter/issues/144): Fix for `toISOString` does not exist on object
|
||||
- [#146](https://github.com/estruyf/vscode-front-matter/issues/146): Date parsing logic added with fallbacks
|
||||
|
||||
## [5.0.0] - 2021-10-07 - [Release Notes](https://beta.frontmatter.codes/updates/v5.0.0)
|
||||
|
||||
### ✨ New features
|
||||
|
||||
- [#113](https://github.com/estruyf/vscode-front-matter/issues/113): Integrating a local DB for media metadata (caption, alt)
|
||||
- [#132](https://github.com/estruyf/vscode-front-matter/issues/132): Major changes to the media dashboard which allows you to navigate through all folders
|
||||
|
||||
### 🎨 Enhancements
|
||||
|
||||
- [#110](https://github.com/estruyf/vscode-front-matter/issues/110): Add support for workspaces with multiple folders
|
||||
- [#117](https://github.com/estruyf/vscode-front-matter/issues/117): Allow to specify a singleline of text in the metadata fields
|
||||
- [#119](https://github.com/estruyf/vscode-front-matter/issues/119): Multi-select support for choice fields
|
||||
- [#121](https://github.com/estruyf/vscode-front-matter/issues/121): Choice fields support ID/title objects as well as a regular string
|
||||
- [#122](https://github.com/estruyf/vscode-front-matter/issues/122): Update the filenames of your media
|
||||
- [#124](https://github.com/estruyf/vscode-front-matter/issues/124): Add new `isPreviewImage` property to the content type field to specify custom preview images
|
||||
- [#126](https://github.com/estruyf/vscode-front-matter/issues/126): Create new content from the available content types
|
||||
- [#127](https://github.com/estruyf/vscode-front-matter/issues/127): Title bar action added to open the dashboard
|
||||
- [#128](https://github.com/estruyf/vscode-front-matter/issues/128): Support for multi-select on image fields added
|
||||
- [#131](https://github.com/estruyf/vscode-front-matter/issues/131): Folder creation support added on media dashboard
|
||||
- [#134](https://github.com/estruyf/vscode-front-matter/issues/134): On startup, the extension checks if local settings can be promoted
|
||||
- [#135](https://github.com/estruyf/vscode-front-matter/issues/135): `Hidden` property added for field configuration
|
||||
- [#137](https://github.com/estruyf/vscode-front-matter/issues/137): Ask to move the `.templates` folder to the new `.frontmatter` folder
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- [#120](https://github.com/estruyf/vscode-front-matter/issues/120): Choice and number field not updating when set manually in front matter of the file
|
||||
- [#133](https://github.com/estruyf/vscode-front-matter/issues/133): Fix for overriding default content type settings
|
||||
|
||||
## [4.0.1] - 2021-09-24
|
||||
|
||||
- [#114](https://github.com/estruyf/vscode-front-matter/issues/114): Fix for categories/tags provided as string in YAML
|
||||
|
||||
@@ -48,6 +48,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
@@ -96,18 +100,28 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🖤 Sponsors 👇 🤘
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/timschps" title="Tim Schaeps">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
|
||||
</a>
|
||||
<a href="https://github.com/flikteoh" title="FlikTeoh">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## 🖤 Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
|
||||
<img src="assets/sponsors/powered-by-vercel.png" />
|
||||
<img src="assets/sponsors/powered-by-vercel.png" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
28
README.md
@@ -46,6 +46,10 @@ Our main extension features are:
|
||||
|
||||
> If you see something missing in your article creation flow, please feel free to reach out.
|
||||
|
||||
**Version 5**
|
||||
|
||||
The new media dashboard redesign got introduced + support for setting metadata on media files [v5.0.0 release notes](https://frontmatter.codes/updates/v5.0.0).
|
||||
|
||||
**Version 4**
|
||||
|
||||
Support for Team level settings, content-types, and image support. Get to know more at: [v4.0.0 release notes](https://frontmatter.codes/updates/v4_0_0).
|
||||
@@ -94,18 +98,28 @@ If you have the courage to test out the beta features, we made available a beta
|
||||
|
||||
## 👉 Contributors 🤘
|
||||
|
||||
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://github.com/estruyf/vscode-front-matter/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=estruyf/vscode-front-matter" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 🖤 Sponsors 👇 🤘
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/timschps" title="Tim Schaeps">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/13098307" />
|
||||
</a>
|
||||
<a href="https://github.com/flikteoh" title="FlikTeoh">
|
||||
<img height="64px" style="border-radius:50%" src="https://avatars.githubusercontent.com/u/1472065" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
## 🖤 Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://vercel.com/?utm_source=vscode-frontmatter&utm_campaign=oss">
|
||||
<img src="assets/sponsors/powered-by-vercel.png" />
|
||||
<img src="assets/sponsors/powered-by-vercel.png" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,40 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{enable-background:new ;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st3{fill:none;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<path class="st1" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z"/>
|
||||
<path class="st1" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8v0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
|
||||
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
|
||||
c0-0.6-0.3-1-0.8-1l0,0V6.8z"/>
|
||||
<path class="st1" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2s-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
|
||||
s0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
|
||||
c-0.2,0-0.4,0.2-0.5,0.6s-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6s0.4-0.2,0.5-0.6
|
||||
C14.5,8.4,14.6,7.9,14.6,7.2z"/>
|
||||
<path class="st1" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9
|
||||
s-0.1-1-0.1-1.3V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9S19,6.8,18.9,6.6c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2
|
||||
h-1.8V11.4z"/>
|
||||
<path class="st1" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z"/>
|
||||
</g>
|
||||
<rect class="st2" width="28" height="28"/>
|
||||
<g class="st0">
|
||||
<path class="st1" d="M2.9,17h0.9l0.6,3c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1v-0.1L5,21.3l0.1-0.8
|
||||
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2v-1v-0.9l0,0c0,0,0,0,0-0.1v0.2c0,0.2,0,0.3-0.1,0.5
|
||||
s0,0.2-0.1,0.3l-0.1,0.7v0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2l-0.3,5.2h-1L2.9,17z"/>
|
||||
<path class="st1" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6
|
||||
c0,0.5-0.1,0.9-0.2,1.4c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1L10.3,22.2L10.3,22.2z"/>
|
||||
<path class="st1" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z"/>
|
||||
<path class="st1" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z"/>
|
||||
<path class="st1" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z"/>
|
||||
<path class="st1" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6
|
||||
l1.4,3.7h-1l-1.4-3.7v3.7h-1L22.3,17L22.3,17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6
|
||||
s-0.1-0.3-0.2-0.4s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2v2.5H23.3z"/>
|
||||
</g>
|
||||
<rect x="-33.5" y="14" class="st3" width="8.6" height="14"/>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#ffffff;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#ffffff"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#ffffff"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#01aeb7"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#01aeb7" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#01aeb7"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#01aeb7"/></svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#AD0670;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#AD0670"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,44 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 28 28" style="color:#ad0670" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st1 {
|
||||
fill: none;
|
||||
stroke: #ad0670;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
<g class="st0">
|
||||
<path fill="currentcolor" d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z" />
|
||||
<path fill="currentcolor" d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8l0,0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
|
||||
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
|
||||
c0-0.6-0.3-1-0.8-1H8V6.8z" />
|
||||
<path fill="currentcolor" d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2c-0.7,0-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
|
||||
c0-1.5,0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
|
||||
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
|
||||
C14.5,8.4,14.6,7.9,14.6,7.2z" />
|
||||
<path fill="currentcolor" d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9s-0.1-1-0.1-1.3
|
||||
V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9s-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2H17.2z" />
|
||||
<path fill="currentcolor" d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z" />
|
||||
</g>
|
||||
|
||||
<rect class="st1" width="28" height="28" />
|
||||
|
||||
<g class="st0">
|
||||
<path fill="currentcolor" d="M2.9,17h0.9L4.4,20c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1L5,21.3l0.1-0.8
|
||||
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2l0-1l0-0.9c0,0,0,0,0,0c0,0,0,0,0-0.1l0,0.2
|
||||
c0,0.2,0,0.3-0.1,0.5s0,0.2-0.1,0.3l-0.1,0.7l0,0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2
|
||||
l-0.3,5.2h-1L2.9,17z" />
|
||||
<path fill="currentcolor" d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6c0,0.5-0.1,0.9-0.2,1.4
|
||||
c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1H10.3z" />
|
||||
<path fill="currentcolor" d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z" />
|
||||
<path fill="currentcolor" d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z" />
|
||||
<path fill="currentcolor" d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z" />
|
||||
<path fill="currentcolor" d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6l1.4,3.7h-1
|
||||
l-1.4-3.7v3.7h-1V17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6s-0.1-0.3-0.2-0.4
|
||||
s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2V20.3z" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#AD0670;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#AD0670"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#AD0670"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
assets/frontmatter-short-min.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
16
assets/frontmatter-short-min.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#02AEB7;stroke-width:50;stroke-miterlimit:10;}
|
||||
.st1{fill:#02AEB7;}
|
||||
</style>
|
||||
<rect x="25" y="25" class="st0" width="1200" height="1200"/>
|
||||
<path class="st1" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path class="st1" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.7 KiB |
1
assets/frontmatter-teal-min.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#02aeb7;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#02aeb7"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#02aeb7"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
@@ -1,39 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 28 28" style="enable-background:new 0 0 28 28;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{enable-background:new ;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st2{fill:none;}
|
||||
</style>
|
||||
<g class="st0">
|
||||
<path d="M4,11.4H2.2V2.9h3.2v2H4v1.2h1.3V8H4V11.4z"/>
|
||||
<path d="M10.9,11.4H9l-0.9-3c0-0.1,0-0.1,0-0.2C8,8.1,8,8,7.9,7.8l0,0.6v3H6.1V2.9H8c0.8,0,1.4,0.2,1.9,0.6
|
||||
c0.5,0.5,0.8,1.3,0.8,2.2c0,1-0.4,1.7-1.1,2.1L10.9,11.4z M8,6.8h0.1c0.2,0,0.4-0.1,0.5-0.3C8.7,6.3,8.8,6,8.8,5.7
|
||||
c0-0.6-0.3-1-0.8-1H8V6.8z"/>
|
||||
<path d="M16.5,7.2c0,1.3-0.2,2.4-0.7,3.2c-0.5,0.8-1.1,1.2-1.8,1.2c-0.7,0-1.2-0.3-1.7-0.9c-0.6-0.8-0.9-2-0.9-3.5
|
||||
c0-1.5,0.3-2.7,0.9-3.5c0.5-0.6,1-0.9,1.7-0.9c0.8,0,1.4,0.4,1.9,1.2C16.2,4.7,16.5,5.8,16.5,7.2z M14.6,7.2c0-1.5-0.2-2.3-0.7-2.3
|
||||
c-0.2,0-0.4,0.2-0.5,0.6c-0.1,0.4-0.2,0.9-0.2,1.7c0,0.7,0.1,1.3,0.2,1.7c0.1,0.4,0.3,0.6,0.5,0.6c0.2,0,0.4-0.2,0.5-0.6
|
||||
C14.5,8.4,14.6,7.9,14.6,7.2z"/>
|
||||
<path d="M17.2,11.4V2.9H19l0.9,3C20,6,20,6.2,20.1,6.5c0.1,0.2,0.1,0.5,0.2,0.8L20.5,8c-0.1-0.7-0.1-1.4-0.2-1.9s-0.1-1-0.1-1.3
|
||||
V2.9H22v8.5h-1.7l-0.9-3.1c-0.1-0.3-0.2-0.6-0.3-0.9s-0.1-0.6-0.2-0.8c0,0.6,0.1,1.1,0.1,1.6c0,0.4,0,0.8,0,1.2v2.2H17.2z"/>
|
||||
<path d="M25.3,11.4h-1.8V4.9h-1v-2h3.9v2h-1.1V11.4z"/>
|
||||
</g>
|
||||
<rect class="st1" width="28" height="28"/>
|
||||
<g class="st0">
|
||||
<path d="M2.9,17h0.9L4.4,20c0.1,0.4,0.2,0.8,0.2,1.2c0.1,0.4,0.1,0.8,0.2,1.2c0-0.1,0-0.1,0-0.1c0,0,0-0.1,0-0.1L5,21.3l0.1-0.8
|
||||
L5.2,20l0.6-3h0.9l0.7,7.5h-1l-0.2-2.6c0-0.1,0-0.2,0-0.3s0-0.2,0-0.2l0-1l0-0.9c0,0,0,0,0,0c0,0,0,0,0-0.1l0,0.2
|
||||
c0,0.2,0,0.3-0.1,0.5s0,0.2-0.1,0.3l-0.1,0.7l0,0.3l-0.6,3.3H4.5l-0.6-2.8c-0.1-0.4-0.2-0.8-0.2-1.1c-0.1-0.4-0.1-0.8-0.2-1.2
|
||||
l-0.3,5.2h-1L2.9,17z"/>
|
||||
<path d="M9.3,17h0.8l1.6,7.5h-1L10.4,23H8.9l-0.3,1.5h-1L9.3,17z M10.3,22.2L10,21c-0.1-0.8-0.3-1.7-0.4-2.6c0,0.5-0.1,0.9-0.2,1.4
|
||||
c-0.1,0.5-0.2,1-0.3,1.5l-0.2,1H10.3z"/>
|
||||
<path d="M11.5,17h3.3v0.9h-1.1v6.7h-1v-6.7h-1.2V17z"/>
|
||||
<path d="M14.8,17h3.3v0.9H17v6.7h-1v-6.7h-1.2V17z"/>
|
||||
<path d="M18.7,17h2.7v0.9h-1.7v2.4h1.5v0.9h-1.5v2.6h1.7v0.9h-2.7V17z"/>
|
||||
<path d="M22.3,17h1.3c0.6,0,1,0.1,1.2,0.4c0.3,0.3,0.5,0.9,0.5,1.6c0,0.5-0.1,1-0.3,1.3c-0.2,0.3-0.4,0.5-0.8,0.6l1.4,3.7h-1
|
||||
l-1.4-3.7v3.7h-1V17z M23.3,20.3c0.4,0,0.7-0.1,0.8-0.3c0.2-0.2,0.2-0.5,0.2-0.9c0-0.2,0-0.4-0.1-0.6s-0.1-0.3-0.2-0.4
|
||||
s-0.2-0.2-0.3-0.2c-0.1,0-0.3-0.1-0.4-0.1h-0.2V20.3z"/>
|
||||
</g>
|
||||
<rect x="-33.5" y="14" class="st2" width="8.6" height="14"/>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1249.98 1249.98"><rect x="25" y="25" width="1199.98" height="1199.98" style="fill:none;stroke:#000000;stroke-miterlimit:10;stroke-width:50px"/><path d="M171.89,489.56H95.38V127.21H230.6V212.4H171.89v52.8h54.68v81.91H171.89Z" transform="translate(24 24)" style="fill:#000000"/><path d="M461.79,489.56H379l-37.8-129.08c-.37-2.18-1-5.08-1.93-8.68s-2.05-7.9-3.39-12.91l.55,23.94V489.56H260.33V127.21h78.34q51.75,0,77.43,26.05,32.65,33.32,32.66,94.81,0,65.71-43.85,90.82ZM336.84,295H342q13.21,0,22-12.91t8.81-32.86q0-40.59-33.21-40.6h-2.75Z" transform="translate(24 24)" style="fill:#000000"/><path d="M691.68,309.56q0,82.85-29.54,134.71-29.35,51.63-76.51,51.63-41.82,0-71.74-39.66Q476.29,406,476.28,305.57q0-96.23,39.26-147.15,29.18-37.78,69.18-37.79,49,0,78,51.17T691.68,309.56Zm-79.44.7q0-98.32-27.16-98.33-13.58,0-21.65,25.81-7.89,23.94-7.89,70.41,0,45.77,7.43,71t20.65,25.23q13.57,0,20.91-24.88Q612.24,354.62,612.24,310.26Z" transform="translate(24 24)" style="fill:#000000"/><path d="M724.34,489.56V127.21h73l38.35,127.2q3.1,11.27,7.06,25.81t8.72,33.56l7.88,31.92Q855.17,298.52,853,265t-2.2-56.33V127.21h73V489.56h-73l-38.53-133.3q-6.06-21.35-10.92-40t-8.53-35.56q2.38,38.26,3.49,66.65t1.1,49.76v92.46Z" transform="translate(24 24)" style="fill:#000000"/><path d="M1062.31,489.56H985.8V214H943.6V127.21h162.56V214h-43.85Z" transform="translate(24 24)" style="fill:#000000"/><path d="M122.7,730.59h35.82l27.36,133.72q5,25.05,9.16,50.2t7.55,52.74q.39-3.6.6-5.62a25.33,25.33,0,0,1,.4-2.87l5.84-37.56,5.23-35.66L219.29,862l24.35-131.39h36.22l28.57,327.72h-40l-7-111.22q-.41-8.49-.71-14.64c-.2-4.11-.3-7.5-.3-10.19l-1.81-43.94-1-40.33c0-.28,0-.88-.1-1.8s-.17-2.16-.3-3.72l-1,6.58q-1.61,11.69-2.91,20.38t-2.32,14.65L245.65,904l-2,11.25-26.16,143.06H189.3L164.75,934.78q-5-24.4-8.95-49.56t-7.14-52.75l-12.08,225.84H97.14Z" transform="translate(24 24)" style="fill:#000000"/><path d="M395.56,730.59h32.6l66.6,327.72H453.31l-11.67-63.89H380.06l-11.87,63.89H327.94Zm40,229.66L426.35,908q-9.27-53.28-15.1-113.77Q408.43,823.78,404,854t-10.46,64.2l-7.65,42Z" transform="translate(24 24)" style="fill:#000000"/><path d="M496.17,730.59H632.4v38.63H585.51v289.09h-41V769.22H496.17Z" transform="translate(24 24)" style="fill:#000000"/><path d="M639,730.59H775.26v38.63H728.38v289.09h-41V769.22H639Z" transform="translate(24 24)" style="fill:#000000"/><path d="M806.65,730.59H917.93V768H848.5V871.74h61.58V909.1H848.5V1021h69.43v37.35H806.65Z" transform="translate(24 24)" style="fill:#000000"/><path d="M964.61,730.59h55.13q34.21,0,50.91,17.19,21.13,22.29,21.13,68.14,0,35.24-11.16,56.56t-31.9,26.43l57.15,159.4h-42.46l-57-160.46v160.46H964.61Zm41.85,145.18q24.35,0,34.41-11.88t10.06-40.12a138.46,138.46,0,0,0-2.11-26.11q-2.11-10.81-6.64-17.61a27.08,27.08,0,0,0-11.67-10,41.58,41.58,0,0,0-17-3.18h-7Z" transform="translate(24 24)" style="fill:#000000"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#F3EFF5">
|
||||
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#C5C5C5">
|
||||
<path d="M9 9H4v1h5V9z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 277 B After Width: | Height: | Size: 277 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentcolor">
|
||||
<svg width="32px" height="32px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="#424242">
|
||||
<path d="M9 9H4v1h5V9z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3l1-1h7l1 1v7l-1 1h-2v2l-1 1H3l-1-1V6l1-1h2V3zm1 2h4l1 1v4h2V3H6v2zm4 1H3v7h7V6z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 277 B |
1
assets/icons/frontmatter-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#C5C5C5"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#C5C5C5" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#C5C5C5"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#C5C5C5"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
assets/icons/frontmatter-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M4,11.4H2.2V2.9H5.4v2H4V6.1H5.3V8H4Z" transform="translate(1 1)" fill="#424242"/><path d="M10.9,11.4H9l-.9-3V8.2C8,8.1,8,8,7.9,7.8v3.6H6.1V2.9H8a2.88,2.88,0,0,1,1.9.6,3.11,3.11,0,0,1,.8,2.2A2.25,2.25,0,0,1,9.6,7.8ZM8,6.8h.1a.55.55,0,0,0,.5-.3,1.88,1.88,0,0,0,.2-.8c0-.6-.3-1-.8-1H8Z" transform="translate(1 1)" fill="#424242"/><path d="M16.5,7.2a6.08,6.08,0,0,1-.7,3.2A2.14,2.14,0,0,1,14,11.6a2.09,2.09,0,0,1-1.7-.9,5.84,5.84,0,0,1-.9-3.5,5.84,5.84,0,0,1,.9-3.5A2.09,2.09,0,0,1,14,2.8,2.16,2.16,0,0,1,15.9,4,8.24,8.24,0,0,1,16.5,7.2Zm-1.9,0c0-1.5-.2-2.3-.7-2.3-.2,0-.4.2-.5.6a6.53,6.53,0,0,0-.2,1.7,7.18,7.18,0,0,0,.2,1.7c.1.4.3.6.5.6s.4-.2.5-.6A7.93,7.93,0,0,0,14.6,7.2Z" transform="translate(1 1)" fill="#424242"/><path d="M17.2,11.4V2.9H19l.9,3c.1.1.1.3.2.6s.1.5.2.8l.2.7c-.1-.7-.1-1.4-.2-1.9a6.64,6.64,0,0,1-.1-1.3V2.9H22v8.5H20.3l-.9-3.1-.3-.9c-.1-.3-.1-.6-.2-.8,0,.6.1,1.1.1,1.6v3.4H17.2Z" transform="translate(1 1)" fill="#424242"/><path d="M25.3,11.4H23.5V4.9h-1v-2h3.9v2H25.3Z" transform="translate(1 1)" fill="#424242"/><rect x="1" y="1" width="28" height="28" fill="none" stroke="#424242" stroke-miterlimit="10" stroke-width="2"/><path d="M2.9,17h.9l.6,3a5,5,0,0,1,.2,1.2c.1.4.1.8.2,1.2v-.2l.2-.9.1-.8.1-.5.6-3h.9l.7,7.5h-1l-.2-2.6V19.5h0v.1a.9.9,0,0,1-.1.5c-.1.2,0,.2-.1.3l-.1.7v.3l-.6,3.3H4.5l-.6-2.8a5.16,5.16,0,0,1-.2-1.1c-.1-.4-.1-.8-.2-1.2l-.3,5.2h-1Z" transform="translate(1 1)" fill="#424242"/><path d="M9.3,17h.8l1.6,7.5h-1L10.4,23H8.9l-.3,1.5h-1Zm1,5.2L10,21c-.1-.8-.3-1.7-.4-2.6a6.75,6.75,0,0,1-.2,1.4l-.3,1.5-.2,1h1.4Z" transform="translate(1 1)" fill="#424242"/><path d="M11.5,17h3.3v.9H13.7v6.7h-1V17.9H11.5Z" transform="translate(1 1)" fill="#424242"/><path d="M14.8,17h3.3v.9H17v6.7H16V17.9H14.8Z" transform="translate(1 1)" fill="#424242"/><path d="M18.7,17h2.7v.9H19.7v2.4h1.5v.9H19.7v2.6h1.7v.9H18.7Z" transform="translate(1 1)" fill="#424242"/><path d="M22.3,17h1.3c.6,0,1,.1,1.2.4a2.35,2.35,0,0,1,.5,1.6,2.5,2.5,0,0,1-.3,1.3,1.24,1.24,0,0,1-.8.6l1.4,3.7h-1l-1.4-3.7v3.7h-1V17Zm1,3.3c.4,0,.7-.1.8-.3s.2-.5.2-.9a1.27,1.27,0,0,0-.1-.6c-.1-.2-.1-.3-.2-.4s-.2-.2-.3-.2-.3-.1-.4-.1h-.2v2.5Z" transform="translate(1 1)" fill="#424242"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
12
assets/icons/frontmatter-short-dark.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
|
||||
<rect x="25" y="25" fill="none" stroke="#ffffff" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
|
||||
<path fill="#ffffff" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#ffffff" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
12
assets/icons/frontmatter-short-light.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1250 1250" style="enable-background:new 0 0 1250 1250;" xml:space="preserve">
|
||||
<rect x="25" y="25" fill="none" stroke="#424242" stroke-width="50" stroke-miterlimit="10" width="1200" height="1200"/>
|
||||
<path fill="#424242" d="M316,1082.3H119.4V151.2h347.5v218.9H316v135.7h140.5v210.5H316V1082.3z"/>
|
||||
<path fill="#424242" d="M602.2,151.2H704l77.7,379.9c9.5,47.4,18.1,95,26,142.6c7.9,47.6,15,97.6,21.4,149.8c0.7-6.8,1.3-12.1,1.7-16
|
||||
c0.2-2.7,0.6-5.5,1.1-8.2l16.6-106.7l14.9-101.3l13.2-66.9l69.2-373.3h102.9l81.2,931.1h-113.6l-19.9-316c-0.8-16.1-1.4-29.9-2-41.6
|
||||
c-0.6-11.7-0.9-21.3-0.9-29L988.3,571l-2.8-114.6c0-0.8,0-2.5-0.3-5.1s-0.5-6.1-0.9-10.6l-2.8,18.7c-3,22.1-5.8,41.4-8.3,57.9
|
||||
c-2.5,16.5-4.7,30.3-6.6,41.6l-15.1,84.9l-5.7,32l-74.3,406.4h-80.1l-69.7-351c-9.5-46.2-17.9-93.1-25.4-140.8
|
||||
c-7.5-47.7-14.2-97.6-20.3-149.9l-34.3,641.6H529.6L602.2,151.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -356,8 +356,18 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.table__cell__seo_details {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table__cell__validation {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table__cell__validation div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.table__cell__validation .valid {
|
||||
@@ -368,6 +378,15 @@
|
||||
color: #E6AF2E;
|
||||
}
|
||||
|
||||
.table__cell__validation div span + span {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.seo__status__note {
|
||||
font-size: 10px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
.field__toggle {
|
||||
position: relative;
|
||||
@@ -437,6 +456,26 @@ input:checked + .field__toggle__slider:before {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__error {
|
||||
color: var(--vscode-errorForeground);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata_field__error button {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__error button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__input, .metadata_field__input:focus,
|
||||
.metadata_field__textarea, .metadata_field__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -451,17 +490,92 @@ input:checked + .field__toggle__slider:before {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.metadata_field__choice {
|
||||
.metadata_field__choice__toggle {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder) !important;
|
||||
outline: none !important;
|
||||
width: 100%;
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
color: var(--vscode-input-foreground);
|
||||
background-color: var(--vscode-input-background);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle:hover,
|
||||
.metadata_field__choice__toggle:focus,
|
||||
.metadata_field__choice__toggle:active,
|
||||
.metadata_field__choice__toggle:disabled {
|
||||
background-color: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice::placeholde {
|
||||
color: var(--vscode-input-placeholderForeground);
|
||||
.metadata_field__choice__toggle span {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__toggle svg.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-left: .25rem;
|
||||
|
||||
position: absolute;
|
||||
right: .25rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list {
|
||||
width: 90%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
|
||||
color: var(--vscode-dropdown-foreground);
|
||||
background-color: var(--vscode-dropdown-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list.open {
|
||||
border: 1px solid rgba(0, 0, 0, .9);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li {
|
||||
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li:active {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-selected="true"] {
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__choice_list li[aria-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata_field__choice_list__item {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button {
|
||||
margin-top: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__choice__button_icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__datetime {
|
||||
@@ -481,12 +595,56 @@ input:checked + .field__toggle__slider:before {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.metadata_field__datetime > button:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.metadata_field__multiple_images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button {
|
||||
background-color: transparent;
|
||||
border: 2px dashed var(--vscode-button-background);
|
||||
padding: 1.5rem;
|
||||
filter: brightness(85%);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button:hover {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button svg {
|
||||
color: var(--vscode-foreground);
|
||||
display: block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__button span {
|
||||
color: var(--vscode-foreground);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__preview {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metadata_field__preview_image__remove {
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
|
||||
230
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-front-matter-beta",
|
||||
"version": "4.0.1",
|
||||
"version": "5.5.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -236,6 +236,60 @@
|
||||
"integrity": "sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/fast-element": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.6.0.tgz",
|
||||
"integrity": "sha512-ePTcBuCA99n7He0BYLIzFr5YOHYPSiBLJeDbDsyyQ5JUs4oXaZD0v54Pq0GAtSZuagnCGTDqcxEcBPUhTqLmQw==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/fast-foundation": {
|
||||
"version": "1.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-1.24.8.tgz",
|
||||
"integrity": "sha512-n4O9jPh8BBliF/Yl9FAVhrSoopsRCnva2L432s/fHwLelY9WUeswjO3DidVBFbzXD5u/gzC4LGWJScNe/ZGU4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/fast-element": "^1.4.0",
|
||||
"@microsoft/fast-web-utilities": "^4.8.0",
|
||||
"@microsoft/tsdoc-config": "^0.13.4",
|
||||
"tabbable": "^5.2.0",
|
||||
"tslib": "^1.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@microsoft/fast-web-utilities": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-4.8.1.tgz",
|
||||
"integrity": "sha512-P3xeyUwQ9nPkFrgAdmkOzaXxIq8YqMU5K+LXcoHgJddJCBCKfGWW9OZQOTigLddItTyVyfO8qsJpDQb1TskKHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exenv-es6": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@microsoft/tsdoc": {
|
||||
"version": "0.12.24",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.24.tgz",
|
||||
"integrity": "sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==",
|
||||
"dev": true
|
||||
},
|
||||
"@microsoft/tsdoc-config": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.9.tgz",
|
||||
"integrity": "sha512-VqqZn+rT9f6XujFPFR2aN9XKF/fuir/IzKVzoxI0vXIzxysp4ee6S2jCakmlGFHEasibifFTsJr7IYmRPxfzYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/tsdoc": "0.12.24",
|
||||
"ajv": "~6.12.6",
|
||||
"jju": "~1.4.0",
|
||||
"resolve": "~1.19.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -268,6 +322,96 @@
|
||||
"integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==",
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.3.tgz",
|
||||
"integrity": "sha512-jwlpsk2/u1cofvfYsjmqcnx50JJtf/T6HTgdW+ih8+rqWC5ABEZf4IiB/H+KAyjJ3wVzCOugMq5irL83XDCfqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/core": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.3.tgz",
|
||||
"integrity": "sha512-obm3SjgCk8A7nB37b2AU1eq1q7gMoJRrGMv9VRIyfcG0Wlz/5lJ9O3ohUk+YZaaVfZMxXn6hFtsBiOWmlv7IIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.3.tgz",
|
||||
"integrity": "sha512-eYppBVqvhs5cvm33snW2sxfcw6G20/74RbBn+E4WDo15hozis89kU7ZCJDOPkXuag3v1h9igns/kM6PNBb41dw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.3.tgz",
|
||||
"integrity": "sha512-63MlYYRni3fs5Bh8XBAfVZ+ctDdWg0fapSTP1ydIC37fKvbE+5zhyUqwrEKBIiclEApg1VKX7bkKxVdu/vsFdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/react": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.3.tgz",
|
||||
"integrity": "sha512-fdfmD9XNpGDwdkeLyd+iq+kqtNeghpH3wiez2rD81ZBvrn70uKaO2/yYDE71AXC6fUOwQuJmdfAuqBcNJkYIEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/browser": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.3.tgz",
|
||||
"integrity": "sha512-yyOFIhqlprPM0g4f35Icear3eZk2mwyYcGEzljJfY2iU6pJwj1lzia5PfSwiCW7jFGMmlBJNhOAIpfhlliZi8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/hub": "6.13.3",
|
||||
"@sentry/minimal": "6.13.3",
|
||||
"@sentry/types": "6.13.3",
|
||||
"@sentry/utils": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.3.tgz",
|
||||
"integrity": "sha512-Vrz5CdhaTRSvCQjSyIFIaV9PodjAVFkzJkTRxyY7P77RcegMsRSsG1yzlvCtA99zG9+e6MfoJOgbOCwuZids5A==",
|
||||
"dev": true
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.3.tgz",
|
||||
"integrity": "sha512-zYFuFH3MaYtBZTeJ4Yajg7pDf0pM3MWs3+9k5my9Fd+eqNcl7dYQYJbT9gyC0HXK1QI4CAMNNlHNl4YXhF91ag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/types": "6.13.3",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@tailwindcss/forms": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.3.tgz",
|
||||
@@ -496,6 +640,16 @@
|
||||
"integrity": "sha512-LlO6K7nzrIWDCZN1Zi6J6ibxrpMibSAct+zNjAwpkNkwup6cJLx5diYvsOJODMPWOuQlBO21qkxtdkSRzW6+Jw==",
|
||||
"dev": true
|
||||
},
|
||||
"@vscode/webview-ui-toolkit": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-0.8.1.tgz",
|
||||
"integrity": "sha512-SOgeaZ+/6yFDXLsgH+JvKcs4E0ThvuohNhkr9mvnggjl+OfFxc+Yqkyf5B4B5kuu3EppOCzEMiUwPC8APuLEGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@microsoft/fast-element": "^1.2.0",
|
||||
"@microsoft/fast-foundation": "^1.24.7"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
|
||||
@@ -2174,6 +2328,12 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"exenv-es6": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.0.0.tgz",
|
||||
"integrity": "sha512-fcG/TX8Ruv9Ma6PBaiNsUrHRJzVzuFMP6LtPn/9iqR+nr9mcLeEOGzXQGLC5CVQSXGE98HtzW2mTZkrCA3XrDg==",
|
||||
"dev": true
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
@@ -2873,6 +3033,15 @@
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
@@ -3085,6 +3254,15 @@
|
||||
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
|
||||
"dev": true
|
||||
},
|
||||
"image-size": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
|
||||
"integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"queue": "6.0.2"
|
||||
}
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
|
||||
@@ -3404,6 +3582,12 @@
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"dev": true
|
||||
},
|
||||
"jju": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
|
||||
"integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3537,6 +3721,12 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.topath": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
|
||||
@@ -4106,6 +4296,23 @@
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node-json-db": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-json-db/-/node-json-db-1.3.0.tgz",
|
||||
"integrity": "sha512-3IK9KuqfKdK12zFKtzmnD6Y5J9qL0TY0gnnZ5XKpzdhCP019zOxPxGCaH6cmIqiho2ymFgcTMKQeJvYkbBFraQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mkdirp": "~1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@@ -4791,6 +4998,15 @@
|
||||
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
|
||||
"dev": true
|
||||
},
|
||||
"queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -5640,6 +5856,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tabbable": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.2.1.tgz",
|
||||
"integrity": "sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ==",
|
||||
"dev": true
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.7.tgz",
|
||||
@@ -6164,6 +6386,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"url-join-ts": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/url-join-ts/-/url-join-ts-1.0.5.tgz",
|
||||
"integrity": "sha512-u+5gi7JyOwhj58ZKwkmkzFGHuepTpmwjqfUDGVjsJJstsCz63CJAINixgJaDcMbmuyWPJIxbtBpIfaDgOQ9KMQ==",
|
||||
"dev": true
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
||||
207
package.json
@@ -3,7 +3,7 @@
|
||||
"displayName": "Front Matter",
|
||||
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
|
||||
"icon": "assets/frontmatter-teal-128x128.png",
|
||||
"version": "4.0.1",
|
||||
"version": "5.5.0",
|
||||
"preview": false,
|
||||
"publisher": "eliostruyf",
|
||||
"galleryBanner": {
|
||||
@@ -97,6 +97,43 @@
|
||||
"markdownDescription": "Specify if you want to automatically update the modified date of your article/page. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.autoupdatedate)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.draftField": {
|
||||
"type": "object",
|
||||
"markdownDescription": "Define the draft field you want to use to manage your content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.draftField)",
|
||||
"default": {
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"boolean",
|
||||
"choice"
|
||||
],
|
||||
"description": ""
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the field to use"
|
||||
},
|
||||
"choices": {
|
||||
"type": "array",
|
||||
"description": "List of choices for the field",
|
||||
"items": {
|
||||
"type": [
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.fmHighlight": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -117,6 +154,11 @@
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path of the folder"
|
||||
},
|
||||
"excludeSubdir": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Exclude sub-directories"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -133,6 +175,48 @@
|
||||
"markdownDescription": "Specify the folder name where all your assets are located. For instance in Hugo this is the `static` folder. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.publicfolder)",
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.content.sorting": {
|
||||
"type": "object",
|
||||
"default": [],
|
||||
"markdownDescription": "Define the sorting options for your dashboard content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.sorting)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Name of the sorting label"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the metadata field to sort by"
|
||||
},
|
||||
"order": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"description": "Order of the sorting"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"date"
|
||||
],
|
||||
"description": "Type of the field value"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"title",
|
||||
"name",
|
||||
"order"
|
||||
]
|
||||
},
|
||||
"scope": "Content"
|
||||
},
|
||||
"frontMatter.custom.scripts": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
@@ -151,6 +235,22 @@
|
||||
"nodeBin": {
|
||||
"type": "string",
|
||||
"description": "Path to the node executable. This is required when using NVM, so that there is no confusion of which node version to use."
|
||||
},
|
||||
"bulk": {
|
||||
"type": "boolean",
|
||||
"description": "Run the script for all content files"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"editor",
|
||||
"notification"
|
||||
],
|
||||
"description": "Define where you want to output your script output. Default is a notification, but you can specify to show it in an editor panel."
|
||||
},
|
||||
"outputType": {
|
||||
"type": "string",
|
||||
"description": "The type of output for the editor panel. Can be used to change it to 'markdown' for example"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -180,6 +280,11 @@
|
||||
"markdownDescription": "Specify if you want to open the dashboard when you start VS Code. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.openonstart)",
|
||||
"scope": "Dashboard"
|
||||
},
|
||||
"frontMatter.framework.id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the ID of your static site generator or framework you are using for your website. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.framework.id)"
|
||||
},
|
||||
"frontMatter.panel.freeform": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -198,6 +303,12 @@
|
||||
"markdownDescription": "Specify the path you want to add after the host and before your slug. This can be used for instance to include the year/month like: `yyyy/MM`. The date will be generated based on the article its date field value. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.preview.pathname)",
|
||||
"scope": "Site preview"
|
||||
},
|
||||
"frontMatter.site.baseURL": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Specify the base URL of your site, this will be used for SEO checks. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.site.baseURL)",
|
||||
"scope": "Site"
|
||||
},
|
||||
"frontMatter.taxonomy.alignFilename": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@@ -252,7 +363,8 @@
|
||||
"image",
|
||||
"choice",
|
||||
"tags",
|
||||
"categories"
|
||||
"categories",
|
||||
"draft"
|
||||
],
|
||||
"description": "Define the type of field"
|
||||
},
|
||||
@@ -268,8 +380,44 @@
|
||||
"type": "array",
|
||||
"description": "Define your choices",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"description": "The choice ID"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The choice title"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"single": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Is a single line field"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do you allow to select multiple values?"
|
||||
},
|
||||
"isPreviewImage": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if the image field can be used as preview. Be aware, you can only have one preview image per content type."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Do you want to hide the field from the metadata section?"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -278,6 +426,19 @@
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pageBundle": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Specify if you want to create a folder when creating new content."
|
||||
},
|
||||
"previewPath": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
],
|
||||
"default": null,
|
||||
"description": "Defines a custom preview path for the content type."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -289,6 +450,7 @@
|
||||
"default": [
|
||||
{
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
@@ -306,7 +468,7 @@
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"title": "Article preview",
|
||||
"title": "Content preview",
|
||||
"name": "preview",
|
||||
"type": "image"
|
||||
},
|
||||
@@ -393,6 +555,12 @@
|
||||
"markdownDescription": "Specifies the optimal description length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seodescriptionlength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoSlugLength": {
|
||||
"type": "number",
|
||||
"default": 75,
|
||||
"markdownDescription": "Specifies the optimal slug length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.seoSlugLength)",
|
||||
"scope": "Taxonomy"
|
||||
},
|
||||
"frontMatter.taxonomy.seoTitleLength": {
|
||||
"type": "number",
|
||||
"default": 60,
|
||||
@@ -419,7 +587,7 @@
|
||||
},
|
||||
"frontMatter.templates.folder": {
|
||||
"type": "string",
|
||||
"default": ".templates",
|
||||
"default": ".frontmatter/templates",
|
||||
"markdownDescription": "Specify the folder to use for your article templates. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.templates.folder)",
|
||||
"scope": "Templates"
|
||||
},
|
||||
@@ -475,7 +643,7 @@
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.generateSlug",
|
||||
"title": "Generate slug based on article title",
|
||||
"title": "Generate slug based on content title",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
@@ -490,7 +658,7 @@
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.insertImage",
|
||||
"title": "Insert image into article",
|
||||
"title": "Insert image into your content",
|
||||
"category": "Front matter",
|
||||
"icon": "$(device-camera)"
|
||||
},
|
||||
@@ -501,17 +669,18 @@
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.createContent",
|
||||
"title": "New article from template",
|
||||
"title": "Create new content from defined content type or template",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"title": "Open dashboard",
|
||||
"category": "Front matter"
|
||||
"category": "Front matter",
|
||||
"icon": "$(preview)"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.preview",
|
||||
"title": "Preview article",
|
||||
"title": "Preview content",
|
||||
"category": "Front matter"
|
||||
},
|
||||
{
|
||||
@@ -533,9 +702,14 @@
|
||||
"menus": {
|
||||
"editor/title": [
|
||||
{
|
||||
"when": "resourceLangId == markdown",
|
||||
"command": "frontMatter.insertImage",
|
||||
"group": "navigation"
|
||||
"group": "navigation@-99",
|
||||
"when": "resourceLangId == markdown"
|
||||
},
|
||||
{
|
||||
"command": "frontMatter.dashboard",
|
||||
"group": "navigation@-98",
|
||||
"when": "frontMatter:enabled == true"
|
||||
}
|
||||
],
|
||||
"explorer/context": [
|
||||
@@ -616,6 +790,8 @@
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
"@types/glob": "7.1.3",
|
||||
"@types/js-yaml": "3.12.1",
|
||||
@@ -627,6 +803,7 @@
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/vscode": "1.51.0",
|
||||
"@vscode/codicons": "0.0.20",
|
||||
"@vscode/webview-ui-toolkit": "^0.8.1",
|
||||
"autoprefixer": "^10.3.2",
|
||||
"css-loader": "5.2.7",
|
||||
"date-fns": "2.23.0",
|
||||
@@ -636,8 +813,11 @@
|
||||
"gray-matter": "4.0.2",
|
||||
"html-loader": "1.3.2",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"image-size": "^1.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash.uniqby": "4.7.0",
|
||||
"mdast-util-from-markdown": "1.0.0",
|
||||
"node-json-db": "^1.3.0",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-loader": "4.3.0",
|
||||
"react": "17.0.1",
|
||||
@@ -651,6 +831,7 @@
|
||||
"ts-loader": "8.0.3",
|
||||
"tslint": "6.1.3",
|
||||
"typescript": "4.0.2",
|
||||
"url-join-ts": "^1.0.5",
|
||||
"wc-react": "github:estruyf/wc-react",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-cli": "3.3.12"
|
||||
@@ -658,4 +839,4 @@
|
||||
"dependencies": {
|
||||
"@docsearch/js": "^3.0.0-alpha.40"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX } from './../constants/settings';
|
||||
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { TaxonomyType } from "../models";
|
||||
import { CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from "../constants/settings";
|
||||
import { format } from "date-fns";
|
||||
import { ArticleHelper, Settings, SlugHelper } from '../helpers';
|
||||
import matter = require('gray-matter');
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { extname, basename } from 'path';
|
||||
import { extname, basename, parse, dirname } from 'path';
|
||||
import { COMMAND_NAME, DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
|
||||
|
||||
export class Article {
|
||||
@@ -172,7 +173,7 @@ export class Article {
|
||||
|
||||
let newFileName = `${slugName}${ext}`;
|
||||
if (filePrefix && typeof filePrefix === "string") {
|
||||
newFileName = `${format(new Date(), filePrefix)}-${newFileName}`;
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(filePrefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
const newPath = editor.document.uri.fsPath.replace(fileName, newFileName);
|
||||
@@ -183,7 +184,7 @@ export class Article {
|
||||
await vscode.workspace.fs.rename(editor.document.uri, vscode.Uri.file(newPath), {
|
||||
overwrite: false
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
Notifications.error(`Failed to rename file: ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
@@ -191,6 +192,31 @@ export class Article {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the slug from the front matter
|
||||
*/
|
||||
public static getSlug() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!file.endsWith(`.md`) && !file.endsWith(`.mdx`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedFile = parse(file);
|
||||
|
||||
if (parsedFile.name.toLowerCase() !== "index") {
|
||||
return parsedFile.name;
|
||||
}
|
||||
|
||||
const folderName = basename(dirname(file));
|
||||
return folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the page its draft mode
|
||||
*/
|
||||
@@ -240,13 +266,13 @@ export class Article {
|
||||
/**
|
||||
* Format the date to the defined format
|
||||
*/
|
||||
public static formatDate(dateValue: Date) {
|
||||
public static formatDate(dateValue: Date): string {
|
||||
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
|
||||
|
||||
if (dateFormat && typeof dateFormat === "string") {
|
||||
return format(dateValue, dateFormat);
|
||||
return format(dateValue, DateHelper.formatUpdate(dateFormat) as string);
|
||||
} else {
|
||||
return dateValue.toISOString();
|
||||
return typeof dateValue.toISOString === 'function' ? dateValue.toISOString() : dateValue?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
31
src/commands/Content.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { commands, QuickPickItem, window } from 'vscode';
|
||||
import { COMMAND_NAME } from '../constants';
|
||||
|
||||
export class Content {
|
||||
|
||||
public static async create() {
|
||||
|
||||
const options: QuickPickItem[] = [{
|
||||
label: "Create content by content type",
|
||||
description: "Select if you want to create new content by the available content type(s)"
|
||||
}, {
|
||||
label: "Create content by template",
|
||||
description: "Select if you want to create new content by the available template(s)"
|
||||
} as QuickPickItem];
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
placeHolder: `Select how you want to create your new content`,
|
||||
canPickMany: false
|
||||
});
|
||||
|
||||
if (selectedOption) {
|
||||
if (selectedOption.label === options[0].label) {
|
||||
commands.executeCommand(COMMAND_NAME.createByContentType);
|
||||
} else {
|
||||
commands.executeCommand(COMMAND_NAME.createByTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET } from './../constants/settings';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING } from '../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { basename, dirname, extname, join } from "path";
|
||||
import { existsSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { basename, dirname, extname, join, parse } from "path";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
|
||||
import { Settings as SettingsHelper } from '../helpers';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { DraftField, Framework, SortingSetting, TaxonomyType } from '../models';
|
||||
import { Folders } from './Folders';
|
||||
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
|
||||
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
|
||||
import { Page } from '../dashboardWebView/models/Page';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
import { COMMAND_NAME, EXTENSION_STATE_PAGES_VIEW } from '../constants/Extension';
|
||||
import { Template } from './Template';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { Settings } from '../dashboardWebView/models/Settings';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { parseJSON } from 'date-fns';
|
||||
import { ViewType } from '../dashboardWebView/state';
|
||||
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
|
||||
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
|
||||
import { decodeBase64Image } from '../helpers/decodeBase64Image';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
|
||||
import { MediaLibrary } from '../helpers/MediaLibrary';
|
||||
import imageSize from 'image-size';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { FrameworkDetector } from '../helpers/FrameworkDetector';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class Dashboard {
|
||||
private static webview: WebviewPanel | null = null;
|
||||
@@ -31,12 +33,13 @@ export class Dashboard {
|
||||
private static media: MediaInfo[] = [];
|
||||
private static timers: { [folder: string]: any } = {};
|
||||
private static _viewData: DashboardData | undefined;
|
||||
private static mediaLib: MediaLibrary;
|
||||
|
||||
public static get viewData(): DashboardData | undefined {
|
||||
return Dashboard._viewData;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Init the dashboard
|
||||
*/
|
||||
public static async init() {
|
||||
@@ -50,6 +53,8 @@ export class Dashboard {
|
||||
* Open or reveal the dashboard
|
||||
*/
|
||||
public static async open(data?: DashboardData) {
|
||||
this.mediaLib = MediaLibrary.getInstance();
|
||||
|
||||
Dashboard._viewData = data;
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
@@ -94,8 +99,8 @@ export class Dashboard {
|
||||
Dashboard.isDisposed = false;
|
||||
|
||||
Dashboard.webview.iconPath = {
|
||||
dark: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter-dark.svg')),
|
||||
light: Uri.file(join(extensionUri.fsPath, 'assets/frontmatter.svg'))
|
||||
dark: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-dark.svg')),
|
||||
light: Uri.file(join(extensionUri.fsPath, 'assets/icons/frontmatter-short-light.svg'))
|
||||
};
|
||||
|
||||
Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri);
|
||||
@@ -136,6 +141,12 @@ export class Dashboard {
|
||||
case DashboardMessage.createContent:
|
||||
await commands.executeCommand(COMMAND_NAME.createContent);
|
||||
break;
|
||||
case DashboardMessage.createByContentType:
|
||||
await commands.executeCommand(COMMAND_NAME.createByContentType);
|
||||
break;
|
||||
case DashboardMessage.createByTemplate:
|
||||
await commands.executeCommand(COMMAND_NAME.createByTemplate);
|
||||
break;
|
||||
case DashboardMessage.updateSetting:
|
||||
Dashboard.updateSetting(msg.data);
|
||||
break;
|
||||
@@ -151,16 +162,16 @@ export class Dashboard {
|
||||
}
|
||||
break;
|
||||
case DashboardMessage.setPageViewType:
|
||||
Extension.getInstance().setState(EXTENSION_STATE_PAGES_VIEW, msg.data);
|
||||
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
|
||||
break;
|
||||
case DashboardMessage.getMedia:
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder)
|
||||
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder);
|
||||
break;
|
||||
case DashboardMessage.copyToClipboard:
|
||||
env.clipboard.writeText(msg.data);
|
||||
break;
|
||||
case DashboardMessage.refreshMedia:
|
||||
Dashboard.media = [];
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, msg?.data?.folder);
|
||||
break;
|
||||
case DashboardMessage.uploadMedia:
|
||||
@@ -172,9 +183,35 @@ export class Dashboard {
|
||||
case DashboardMessage.insertPreviewImage:
|
||||
Dashboard.insertImage(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.updateMediaMetadata:
|
||||
Dashboard.updateMediaMetadata(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.createMediaFolder:
|
||||
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setFramework:
|
||||
Dashboard.setFramework(msg?.data);
|
||||
break;
|
||||
case DashboardMessage.setState:
|
||||
if (msg?.data?.key && msg?.data?.value) {
|
||||
Extension.getInstance().setState(msg?.data?.key, msg?.data?.value, "workspace");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset media array
|
||||
*/
|
||||
public static resetMedia() {
|
||||
Dashboard.media = [];
|
||||
}
|
||||
|
||||
public static switchFolder(folderPath: string) {
|
||||
Dashboard.resetMedia();
|
||||
Dashboard.getMedia(0, folderPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an image into the front matter or contents
|
||||
@@ -193,16 +230,33 @@ export class Dashboard {
|
||||
const panel = ExplorerView.getInstance(extensionUri);
|
||||
|
||||
if (data?.position) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const editor = window.activeTextEditor;
|
||||
const line = data.position.line;
|
||||
const character = data.position.character;
|
||||
if (line) {
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
let imgPath = data.image;
|
||||
const filePath = data.file;
|
||||
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
|
||||
|
||||
const imgDir = dirname(absImgPath);
|
||||
const fileDir = dirname(filePath);
|
||||
|
||||
if (imgDir === fileDir) {
|
||||
imgPath = join('/', basename(imgPath));
|
||||
|
||||
// Snippets are already parsed, so update the URL of the image
|
||||
if (data.snippet) {
|
||||
data.snippet = data.snippet.replace(data.image, imgPath);
|
||||
}
|
||||
}
|
||||
|
||||
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || ``));
|
||||
}
|
||||
panel.getMediaSelection();
|
||||
} else {
|
||||
panel.getMediaSelection();
|
||||
panel.updateMetadata({field: data.fieldName, value: data.image});
|
||||
panel.updateMetadata({field: data.fieldName, value: data.image });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,25 +267,53 @@ export class Dashboard {
|
||||
private static async getSettings() {
|
||||
const ext = Extension.getInstance();
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isInitialized = await Template.isInitialized();
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.settings,
|
||||
data: {
|
||||
beta: ext.isBetaVersion(),
|
||||
wsFolder: wsFolder ? wsFolder.fsPath : '',
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS),
|
||||
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
|
||||
folders: Folders.get(),
|
||||
initialized: await Template.isInitialized(),
|
||||
initialized: isInitialized,
|
||||
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
|
||||
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(EXTENSION_STATE_PAGES_VIEW),
|
||||
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
|
||||
customSorting: SettingsHelper.get<SortingSetting[]>(SETTINGS_CONTENT_SORTING),
|
||||
contentFolders: Folders.get(),
|
||||
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
|
||||
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
dashboardState: {
|
||||
sorting: await ext.getState<ViewType | undefined>(ExtensionState.Dashboard.Sorting, "workspace")
|
||||
}
|
||||
} as Settings
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current site-generator or framework + related settings
|
||||
* @param frameworkId
|
||||
*/
|
||||
private static setFramework(frameworkId: string | null) {
|
||||
SettingsHelper.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
|
||||
|
||||
if (frameworkId) {
|
||||
const allFrameworks = FrameworkDetector.getAll();
|
||||
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
|
||||
if (framework) {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
|
||||
} else {
|
||||
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a setting from the dashboard
|
||||
*/
|
||||
@@ -243,50 +325,84 @@ export class Dashboard {
|
||||
/**
|
||||
* Retrieve all media files
|
||||
*/
|
||||
private static async getMedia(page: number = 0, folder: string = '') {
|
||||
private static async getMedia(page: number = 0, requestedFolder: string = '') {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const contentFolders = Folders.get();
|
||||
const viewData = Dashboard.viewData;
|
||||
let selectedFolder = requestedFolder;
|
||||
|
||||
if (Dashboard.media.length === 0) {
|
||||
const contentFolder = Folders.get();
|
||||
let allMedia: MediaInfo[] = [];
|
||||
// If the static folder is not set, retreive the last opened location
|
||||
if (!selectedFolder) {
|
||||
const stateValue = await Extension.getInstance().getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
|
||||
|
||||
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
|
||||
// Support for page bundles
|
||||
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
|
||||
const folderPath = parse(viewData.data.filePath).dir;
|
||||
selectedFolder = folderPath;
|
||||
} else if (stateValue && existsSync(stateValue)) {
|
||||
selectedFolder = stateValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go to the home folder
|
||||
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
|
||||
selectedFolder = '';
|
||||
}
|
||||
|
||||
let relSelectedFolderPath = selectedFolder;
|
||||
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
|
||||
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
|
||||
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
|
||||
}
|
||||
|
||||
if (relSelectedFolderPath.startsWith('/')) {
|
||||
relSelectedFolderPath = relSelectedFolderPath.substring(1);
|
||||
}
|
||||
|
||||
let allMedia: MediaInfo[] = [];
|
||||
|
||||
if (relSelectedFolderPath) {
|
||||
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
|
||||
const media = Dashboard.filterMedia(files);
|
||||
allMedia = [...media];
|
||||
} else {
|
||||
if (staticFolder) {
|
||||
const files = await workspace.findFiles(`${staticFolder || ""}/**/*`);
|
||||
const folderSearch = join(staticFolder || "", '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...media];
|
||||
}
|
||||
|
||||
if (contentFolder && wsFolder) {
|
||||
for (let i = 0; i < contentFolder.length; i++) {
|
||||
const folder = contentFolder[i];
|
||||
const relFolderPath = folder.path.substring(wsFolder.fsPath.length + 1);
|
||||
const files = await workspace.findFiles(`${relFolderPath}/**/*`);
|
||||
if (contentFolders && wsFolder) {
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const contentFolder = contentFolders[i];
|
||||
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
|
||||
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
|
||||
const files = await workspace.findFiles(folderSearch);
|
||||
const media = Dashboard.filterMedia(files);
|
||||
|
||||
allMedia = [...allMedia, ...media];
|
||||
}
|
||||
}
|
||||
|
||||
allMedia = allMedia.sort((a, b) => {
|
||||
if (b.fsPath < a.fsPath) {
|
||||
return -1;
|
||||
}
|
||||
if (b.fsPath > a.fsPath) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
Dashboard.media = Object.assign([], allMedia);
|
||||
}
|
||||
|
||||
// Filter the media
|
||||
allMedia = allMedia.sort((a, b) => {
|
||||
if (b.fsPath < a.fsPath) {
|
||||
return -1;
|
||||
}
|
||||
if (b.fsPath > a.fsPath) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
Dashboard.media = Object.assign([], allMedia);
|
||||
|
||||
let files: MediaInfo[] = Dashboard.media;
|
||||
if (folder) {
|
||||
files = files.filter(f => f.fsPath.includes(folder));
|
||||
}
|
||||
|
||||
// Retrieve the total after filtering and before the slicing happens
|
||||
const total = files.length;
|
||||
@@ -295,32 +411,53 @@ export class Dashboard {
|
||||
files = files.slice(page * 16, ((page + 1) * 16));
|
||||
files = files.map((file) => {
|
||||
try {
|
||||
const metadata = Dashboard.mediaLib.get(file.fsPath);
|
||||
|
||||
return {
|
||||
...file,
|
||||
stats: statSync(file.fsPath)
|
||||
stats: statSync(file.fsPath),
|
||||
dimensions: imageSize(file.fsPath),
|
||||
...metadata
|
||||
};
|
||||
} catch (e) {
|
||||
return {...file, stats: undefined};
|
||||
}
|
||||
}).filter(f => f.stats !== undefined);
|
||||
});
|
||||
files = files.filter(f => f.stats !== undefined);
|
||||
|
||||
const folders = [...new Set(Dashboard.media.map((file) => {
|
||||
let relFolderPath = wsFolder ? file.fsPath.substring(wsFolder.fsPath.length + 1) : file.fsPath;
|
||||
if (staticFolder && relFolderPath.startsWith(staticFolder)) {
|
||||
relFolderPath = relFolderPath.substring(staticFolder.length);
|
||||
// Retrieve all the folders
|
||||
let allContentFolders: string[] = [];
|
||||
let allFolders: string[] = [];
|
||||
|
||||
if (selectedFolder) {
|
||||
if (existsSync(selectedFolder)) {
|
||||
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
|
||||
}
|
||||
if (relFolderPath?.startsWith('/')) {
|
||||
relFolderPath = relFolderPath.substring(1);
|
||||
} else {
|
||||
for (const contentFolder of contentFolders) {
|
||||
const contentPath = contentFolder.path;
|
||||
if (contentPath && existsSync(contentPath)) {
|
||||
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
|
||||
allContentFolders = [...allContentFolders, ...subFolders];
|
||||
}
|
||||
}
|
||||
return dirname(relFolderPath);
|
||||
}))];
|
||||
|
||||
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
|
||||
if (staticPath && existsSync(staticPath)) {
|
||||
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last opened folder
|
||||
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
|
||||
|
||||
Dashboard.postWebviewMessage({
|
||||
command: DashboardCommand.media,
|
||||
data: {
|
||||
media: files,
|
||||
total: total,
|
||||
folders
|
||||
folders: [...allContentFolders, ...allFolders],
|
||||
selectedFolder
|
||||
} as MediaPaths
|
||||
});
|
||||
}
|
||||
@@ -333,7 +470,7 @@ export class Dashboard {
|
||||
|
||||
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
|
||||
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Page[] = [];
|
||||
@@ -353,8 +490,8 @@ export class Dashboard {
|
||||
fmModified: file.mtime,
|
||||
fmFilePath: file.filePath,
|
||||
fmFileName: file.fileName,
|
||||
fmDraft: article?.data.draft ? "Draft" : "Published",
|
||||
fmYear: article?.data[dateField] ? parseJSON(article?.data[dateField]).getFullYear() : null,
|
||||
fmDraft: ContentType.getDraftStatus(article?.data),
|
||||
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
slug: article?.data.slug,
|
||||
@@ -362,23 +499,38 @@ export class Dashboard {
|
||||
draft: article?.data.draft,
|
||||
description: article?.data[descriptionField] || "",
|
||||
};
|
||||
|
||||
if (article?.data.preview && wsFolder) {
|
||||
const staticPath = join(wsFolder.fsPath, staticFolder || "", article?.data.preview);
|
||||
const contentFolderPath = join(dirname(file.filePath), article?.data.preview);
|
||||
|
||||
let previewUri = null;
|
||||
if (existsSync(staticPath)) {
|
||||
previewUri = Uri.file(staticPath);
|
||||
} else if (existsSync(contentFolderPath)) {
|
||||
previewUri = Uri.file(contentFolderPath);
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
|
||||
|
||||
if (article?.data[previewField] && wsFolder) {
|
||||
let fieldValue = article?.data[previewField];
|
||||
if (fieldValue && Array.isArray(fieldValue)) {
|
||||
if (fieldValue.length > 0) {
|
||||
fieldValue = fieldValue[0];
|
||||
} else {
|
||||
fieldValue = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (previewUri) {
|
||||
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
|
||||
page.preview = preview?.toString() || "";
|
||||
} else {
|
||||
page.preview = "";
|
||||
// Revalidate as the array could have been empty
|
||||
if (fieldValue) {
|
||||
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
|
||||
const contentFolderPath = join(dirname(file.filePath), fieldValue);
|
||||
|
||||
let previewUri = null;
|
||||
if (existsSync(staticPath)) {
|
||||
previewUri = Uri.file(staticPath);
|
||||
} else if (existsSync(contentFolderPath)) {
|
||||
previewUri = Uri.file(contentFolderPath);
|
||||
}
|
||||
|
||||
if (previewUri) {
|
||||
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
|
||||
page[previewField] = preview?.toString() || "";
|
||||
} else {
|
||||
page[previewField] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,9 +571,13 @@ export class Dashboard {
|
||||
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
|
||||
if (fileName && contents) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
const wsPath = wsFolder ? wsFolder.fsPath : "";
|
||||
let absFolderPath = join(wsPath, staticFolder || "", folder || "");
|
||||
let absFolderPath = join(wsPath, staticFolder || "");
|
||||
|
||||
if (folder) {
|
||||
absFolderPath = folder;
|
||||
}
|
||||
|
||||
if (!existsSync(absFolderPath)) {
|
||||
absFolderPath = join(wsPath, folder || "");
|
||||
@@ -437,9 +593,9 @@ export class Dashboard {
|
||||
|
||||
if (imgData) {
|
||||
writeFileSync(staticPath, imgData.data);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${staticFolder}/${folder}`);
|
||||
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
|
||||
|
||||
const folderPath = `${staticFolder}/${folder}`;
|
||||
const folderPath = `${folder}`;
|
||||
if (Dashboard.timers[folderPath]) {
|
||||
clearTimeout(Dashboard.timers[folderPath]);
|
||||
delete Dashboard.timers[folderPath];
|
||||
@@ -475,6 +631,18 @@ export class Dashboard {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the metadata of the selected file
|
||||
*/
|
||||
private static async updateMediaMetadata({ file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; }) {
|
||||
Dashboard.mediaLib.set(file, metadata);
|
||||
|
||||
// Check if filename needs to be updated
|
||||
Dashboard.mediaLib.updateFilename(file, filename);
|
||||
|
||||
Dashboard.getMedia(page || 0, folder || "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Post data to the dashboard
|
||||
* @param msg
|
||||
@@ -498,19 +666,21 @@ export class Dashboard {
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const version = Extension.getInstance().getVersion();
|
||||
const ext = Extension.getInstance();
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="width:100%;height:100%;margin:0;padding:0;">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Front Matter Dashboard</title>
|
||||
</head>
|
||||
<body style="width:100%;height:100%;margin:0;padding:0;overflow:hidden" class="bg-gray-100 text-vulcan-500 dark:bg-vulcan-500 dark:text-whisper-500">
|
||||
<div id="app" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" style="width:100%;height:100%;margin:0;padding:0;" ${version.usedVersion ? "" : `data-showWelcome="true"`}></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`dashboard-${version.installedVersion}`}" alt="Daily usage" />
|
||||
|
||||
|
||||
@@ -1,43 +1,82 @@
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS } from './../constants/settings';
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import { SETTINGS_CONTENT_PAGE_FOLDERS, SETTINGS_CONTENT_STATIC_FOLDER } from './../constants';
|
||||
import { commands, Uri, workspace, window } from "vscode";
|
||||
import { basename, join } from "path";
|
||||
import { ContentFolder, FileInfo, FolderInfo } from "../models";
|
||||
import uniqBy = require("lodash.uniqby");
|
||||
import { Template } from "./Template";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
import { CONTEXT } from "../constants/context";
|
||||
import { Settings } from "../helpers";
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { format } from 'date-fns';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { parseWinPath } from '../helpers/parseWinPath';
|
||||
|
||||
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
|
||||
|
||||
export class Folders {
|
||||
|
||||
/**
|
||||
* Add a media folder
|
||||
* @returns
|
||||
*/
|
||||
public static async addMediaFolder(data?: {selectedFolder?: string}) {
|
||||
let wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
let startPath = "";
|
||||
|
||||
if (data?.selectedFolder) {
|
||||
startPath = data.selectedFolder.replace(parseWinPath(wsFolder?.fsPath || ""), "");
|
||||
} else if (staticFolder) {
|
||||
startPath = `/${staticFolder}`;
|
||||
}
|
||||
|
||||
if (startPath && !startPath.endsWith("/")) {
|
||||
startPath += "/";
|
||||
}
|
||||
|
||||
const folderName = await window.showInputBox({
|
||||
prompt: `Which name would you like to give to your folder (use "/" to create multi-level folders)?`,
|
||||
value: startPath,
|
||||
ignoreFocusOut: true,
|
||||
placeHolder: `${format(new Date(), `yyyy/MM`)}`
|
||||
});
|
||||
|
||||
if (!folderName) {
|
||||
Notifications.warning(`No folder name was specified.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = folderName.split("/").filter(f => f);
|
||||
let parentFolders: string[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
const folderPath = join(parseWinPath(wsFolder?.fsPath || ""), parentFolders.join("/"), folder);
|
||||
|
||||
parentFolders.push(folder);
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
mkdirSync(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (Dashboard.isOpen) {
|
||||
Dashboard.switchFolder(folderName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content in a registered folder
|
||||
* @returns
|
||||
*/
|
||||
public static async create() {
|
||||
const folders = Folders.get();
|
||||
|
||||
if (!folders || folders.length === 0) {
|
||||
Notifications.warning(`There are no known content locations defined in this project.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedFolder: string | undefined;
|
||||
if (folders.length > 1) {
|
||||
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
|
||||
placeHolder: `Select where you want to create your content`
|
||||
});
|
||||
} else {
|
||||
selectedFolder = folders[0].title;
|
||||
}
|
||||
|
||||
const selectedFolder = await Questions.SelectContentFolder();
|
||||
if (!selectedFolder) {
|
||||
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = Folders.get();
|
||||
const location = folders.find(f => f.title === selectedFolder);
|
||||
if (location) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(location.path));
|
||||
@@ -115,9 +154,33 @@ export class Folders {
|
||||
*/
|
||||
public static getWorkspaceFolder(): Uri | undefined {
|
||||
const folders = workspace.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
|
||||
if (folders && folders.length === 1) {
|
||||
return folders[0].uri;
|
||||
} else if (folders && folders.length > 1) {
|
||||
let projectFolder = undefined;
|
||||
|
||||
for (const folder of folders) {
|
||||
if (!projectFolder && existsSync(join(folder.uri.fsPath, Settings.globalFile))) {
|
||||
projectFolder = folder.uri;
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectFolder) {
|
||||
window.showWorkspaceFolderPick({
|
||||
placeHolder: `Please select the main workspace folder for Front Matter to use.`
|
||||
}).then(selectedFolder => {
|
||||
if (selectedFolder) {
|
||||
Settings.createGlobalFile(selectedFolder.uri);
|
||||
// Full reload to make sure the whole extension is reloaded correctly
|
||||
commands.executeCommand(`workbench.action.reloadWindow`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return projectFolder;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -127,7 +190,6 @@ export class Folders {
|
||||
public static getProjectFolderName(): string {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
// const projectFolder = wsFolder?.fsPath.split('\\').join('/').split('/').pop();
|
||||
return basename(wsFolder.fsPath);
|
||||
}
|
||||
return "";
|
||||
@@ -148,8 +210,8 @@ export class Folders {
|
||||
if (projectStart) {
|
||||
projectStart = projectStart.replace(/\\/g, '/');
|
||||
projectStart = projectStart.startsWith('/') ? projectStart.substr(1) : projectStart;
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, '**/*.md'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, '**/*.mdx'));
|
||||
const mdFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.md'));
|
||||
const mdxFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', '*.mdx'));
|
||||
let files = [...mdFiles, ...mdxFiles];
|
||||
if (files) {
|
||||
let fileStats: FileInfo[] = [];
|
||||
@@ -201,7 +263,7 @@ export class Folders {
|
||||
const folders: ContentFolder[] = Settings.get(SETTINGS_CONTENT_PAGE_FOLDERS) as ContentFolder[];
|
||||
|
||||
return folders.map(folder => ({
|
||||
title: folder.title,
|
||||
...folder,
|
||||
path: Folders.absWsFolder(folder, wsFolder)
|
||||
}));
|
||||
}
|
||||
@@ -212,7 +274,13 @@ export class Folders {
|
||||
*/
|
||||
private static async update(folders: ContentFolder[]) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folders.map(folder => ({ title: folder.title, path: Folders.relWsFolder(folder, wsFolder) })));
|
||||
|
||||
let folderDetails = folders.map(folder => ({
|
||||
...folder,
|
||||
path: Folders.relWsFolder(folder, wsFolder)
|
||||
}));
|
||||
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +291,7 @@ export class Folders {
|
||||
*/
|
||||
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, wsFolder?.fsPath || "");
|
||||
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ""));
|
||||
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
@@ -236,7 +304,7 @@ export class Folders {
|
||||
*/
|
||||
private static relWsFolder(folder: ContentFolder, wsFolder?: Uri) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = folder.path.replace(wsFolder?.fsPath || "", WORKSPACE_PLACEHOLDER);
|
||||
let absPath = folder.path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
|
||||
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME } from './../constants/settings';
|
||||
import { SETTING_PREVIEW_HOST, SETTING_PREVIEW_PATHNAME, CONTEXT } from './../constants';
|
||||
import { ArticleHelper } from './../helpers/ArticleHelper';
|
||||
import { join } from "path";
|
||||
import { commands, env, Uri, ViewColumn, window } from "vscode";
|
||||
import { Settings } from '../helpers';
|
||||
import { PreviewSettings } from '../models';
|
||||
import { format } from 'date-fns';
|
||||
import { CONTEXT } from '../constants/context';
|
||||
import { DateHelper } from '../helpers/DateHelper';
|
||||
import { Article } from '.';
|
||||
import { urlJoin } from 'url-join-ts';
|
||||
|
||||
|
||||
export class Preview {
|
||||
@@ -32,12 +34,25 @@ export class Preview {
|
||||
const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
|
||||
let slug = article?.data ? article.data.slug : "";
|
||||
|
||||
if (settings.pathname) {
|
||||
let pathname = settings.pathname;
|
||||
if (article?.data) {
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType && contentType.previewPath) {
|
||||
pathname = contentType.previewPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
slug = Article.getSlug();
|
||||
}
|
||||
|
||||
if (pathname) {
|
||||
const articleDate = ArticleHelper.getDate(article);
|
||||
|
||||
try {
|
||||
slug = join(format(articleDate || new Date(), settings.pathname), slug);
|
||||
slug = join(format(articleDate || new Date(), DateHelper.formatUpdate(pathname) as string), slug);
|
||||
} catch (error) {
|
||||
slug = join(settings.pathname, slug);
|
||||
slug = join(pathname, slug);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,9 +128,9 @@ export class Preview {
|
||||
</head>
|
||||
<body>
|
||||
<div class="slug">
|
||||
<input type="text" value="${join(localhostUrl.toString(), slug)}" disabled />
|
||||
<input type="text" value="${urlJoin(localhostUrl.toString(), slug || '')}" disabled />
|
||||
</div>
|
||||
<iframe src="${join(localhostUrl.toString(), slug)}" >
|
||||
<iframe src="${urlJoin(localhostUrl.toString(), slug || '')}" >
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -132,4 +147,4 @@ export class Preview {
|
||||
pathname
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { workspace, Uri } from "vscode";
|
||||
import { CONFIG_KEY, SETTING_TEMPLATES_FOLDER } from "../constants";
|
||||
import { join } from "path";
|
||||
import * as fs from "fs";
|
||||
import { Notifications } from "../helpers/Notifications";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants/settings';
|
||||
import { SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { ArticleHelper, SeoHelper, Settings } from '../helpers';
|
||||
import { ExplorerView } from '../explorerView/ExplorerView';
|
||||
import { DefaultFields } from '../constants';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
|
||||
export class StatusListener {
|
||||
|
||||
@@ -15,6 +16,11 @@ export class StatusListener {
|
||||
public static async verify(frontMatterSB: vscode.StatusBarItem, collection: vscode.DiagnosticCollection) {
|
||||
const draftMsg = "in draft";
|
||||
const publishMsg = "to publish";
|
||||
|
||||
const draft = ContentType.getDraftField();
|
||||
if (!draft || draft.type !== "boolean") {
|
||||
frontMatterSB.hide();
|
||||
}
|
||||
|
||||
let editor = vscode.window.activeTextEditor;
|
||||
if (editor && ArticleHelper.isMarkdownFile()) {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Questions } from './../helpers/Questions';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { format } from 'date-fns';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { ArticleHelper, Settings } from '../helpers';
|
||||
import { Article } from '.';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { CONTEXT } from '../constants/context';
|
||||
import { CONTEXT } from '../constants';
|
||||
import { Project } from './Project';
|
||||
import { Folders } from './Folders';
|
||||
import { ContentType } from '../helpers/ContentType';
|
||||
import { ContentType as IContentType } from '../models';
|
||||
|
||||
export class Template {
|
||||
|
||||
@@ -67,7 +68,7 @@ export class Template {
|
||||
["yes", "no"],
|
||||
{
|
||||
canPickMany: false,
|
||||
placeHolder: `Do you want to keep the article its contents for the template?`,
|
||||
placeHolder: `Do you want to keep the contents for the template?`,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -94,7 +95,7 @@ export class Template {
|
||||
*/
|
||||
public static async create(folderPath: string) {
|
||||
const folder = Settings.get<string>(SETTING_TEMPLATES_FOLDER);
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
const contentTypes = ContentType.getAll();
|
||||
|
||||
if (!folderPath) {
|
||||
Notifications.warning(`Incorrect project folder path retrieved.`);
|
||||
@@ -113,39 +114,33 @@ export class Template {
|
||||
}
|
||||
|
||||
const selectedTemplate = await vscode.window.showQuickPick(templates.map(t => path.basename(t.fsPath)), {
|
||||
placeHolder: `Select the article template to use`
|
||||
placeHolder: `Select the content template to use`
|
||||
});
|
||||
if (!selectedTemplate) {
|
||||
Notifications.warning(`No template selected.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const titleValue = await vscode.window.showInputBox({
|
||||
prompt: `What would you like to use as a title for the new article?`,
|
||||
placeHolder: `Article title`
|
||||
});
|
||||
const titleValue = await Questions.ContentTitle();
|
||||
if (!titleValue) {
|
||||
Notifications.warning(`You did not specify an article title.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the template read
|
||||
const template = templates.find(t => t.fsPath.endsWith(selectedTemplate));
|
||||
if (!template) {
|
||||
Notifications.warning(`Article template could not be found.`);
|
||||
Notifications.warning(`Content template could not be found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileExt = path.parse(selectedTemplate).ext;
|
||||
const sanitizedName = sanitize(titleValue.toLowerCase().replace(/ /g, "-"));
|
||||
let newFileName = `${sanitizedName}${fileExt}`;
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), prefix)}-${newFileName}`;
|
||||
const templateData = ArticleHelper.getFrontMatterByPath(template.fsPath);
|
||||
let contentType: IContentType | undefined;
|
||||
if (templateData && templateData.data && templateData.data.type) {
|
||||
contentType = contentTypes?.find(t => t.name === templateData.data.type);
|
||||
}
|
||||
|
||||
const newFilePath = path.join(folderPath, newFileName);
|
||||
if (fs.existsSync(newFilePath)) {
|
||||
Notifications.warning(`File already exists, please remove it before creating a new one with the same title.`);
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,7 +160,7 @@ export class Template {
|
||||
fmData.title = titleValue;
|
||||
}
|
||||
if (typeof fmData.slug !== "undefined") {
|
||||
fmData.slug = sanitizedName;
|
||||
fmData.slug = ArticleHelper.sanitize(titleValue);
|
||||
}
|
||||
|
||||
frontMatter = Article.updateDate(frontMatter);
|
||||
@@ -180,7 +175,7 @@ export class Template {
|
||||
vscode.window.showTextDocument(txtDoc);
|
||||
}
|
||||
|
||||
Notifications.info(`Your new article has been created.`);
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,8 @@ export const DEFAULT_CONTENT_TYPE_NAME = 'default';
|
||||
|
||||
export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
"name": "default",
|
||||
"pageBundle": false,
|
||||
"previewPath": null,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Title",
|
||||
@@ -28,7 +30,7 @@ export const DEFAULT_CONTENT_TYPE: ContentType = {
|
||||
{
|
||||
"title": "Is in draft",
|
||||
"name": "draft",
|
||||
"type": "boolean"
|
||||
"type": "draft"
|
||||
},
|
||||
{
|
||||
"title": "Tags",
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
export const DefaultFields = {
|
||||
PublishingDate: `date`,
|
||||
LastModified: `lastmod`,
|
||||
Description: `description`
|
||||
Description: `description`,
|
||||
Slug: `slug`
|
||||
};
|
||||
|
||||
@@ -3,9 +3,6 @@ const extensionName = "frontMatter";
|
||||
export const EXTENSION_ID = 'eliostruyf.vscode-front-matter';
|
||||
export const EXTENSION_BETA_ID = 'eliostruyf.vscode-front-matter-beta';
|
||||
|
||||
export const EXTENSION_STATE_VERSION = 'frontMatter:Version';
|
||||
export const EXTENSION_STATE_PAGES_VIEW = 'frontMatter:Pages:ViewType';
|
||||
|
||||
export const getCommandName = (command: string) => {
|
||||
return `${extensionName}.${command}`;
|
||||
};
|
||||
@@ -25,10 +22,13 @@ export const COMMAND_NAME = {
|
||||
registerFolder: getCommandName("registerFolder"),
|
||||
unregisterFolder: getCommandName("unregisterFolder"),
|
||||
createContent: getCommandName("createContent"),
|
||||
createByContentType: getCommandName("createByContentType"),
|
||||
createByTemplate: getCommandName("createByTemplate"),
|
||||
createTemplate: getCommandName("createTemplate"),
|
||||
collapseSections: getCommandName("collapseSections"),
|
||||
preview: getCommandName("preview"),
|
||||
dashboard: getCommandName("dashboard"),
|
||||
promote: getCommandName("promoteSettings"),
|
||||
insertImage: getCommandName("insertImage"),
|
||||
createFolder: getCommandName("createFolder"),
|
||||
};
|
||||
12
src/constants/ExtensionState.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export const ExtensionState = {
|
||||
PagesView: `frontMatter:Pages:ViewType`,
|
||||
SelectedFolder: `frontMatter:SelectedFolder`,
|
||||
Version: `frontMatter:Version`,
|
||||
SettingPromoted: `frontMatter:Settings:Promoted`,
|
||||
MoveTemplatesFolder: `frontMatter:Templates:Move`,
|
||||
|
||||
Dashboard: {
|
||||
Sorting: `frontMatter:Dashboard:Sorting`,
|
||||
}
|
||||
};
|
||||
21
src/constants/FrameworkDetectors.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const FrameworkDetectors = [
|
||||
{
|
||||
"framework": {"name": "gatsby", "dist": "public", "static": "static", "build": "gatsby build"},
|
||||
"requiredFiles": ["gatsby-config.js"],
|
||||
"requiredDependencies": ["gatsby"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "hugo", "dist": "public", "static": "static", "build": "hugo"},
|
||||
"requiredFiles": ["config.toml", "config.yaml", "config.yml"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "next", "dist": ".next", "static": "public", "build": "next build"},
|
||||
"requiredFiles": ["next.config.js"],
|
||||
"requiredDependencies": ["next"]
|
||||
},
|
||||
{
|
||||
"framework": {"name": "nuxt", "dist": "dist", "static": "static", "build": "nuxt"},
|
||||
"requiredFiles": ["nuxt.config.js"],
|
||||
"requiredDependencies": ["nuxt"]
|
||||
}
|
||||
];
|
||||
@@ -1,4 +1,6 @@
|
||||
export const GITHUB_LINK = "https://github.com/estruyf/vscode-front-matter";
|
||||
export const ISSUE_LINK = "https://github.com/estruyf/vscode-front-matter/issues";
|
||||
export const SPONSOR_LINK = "https://github.com/sponsors/estruyf";
|
||||
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
|
||||
export const REVIEW_LINK = "https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter&ssr=false#review-details";
|
||||
|
||||
export const SENTRY_LINK = "https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293";
|
||||
8
src/constants/LocalStore.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const LocalStore = {
|
||||
rootFolder: ".frontmatter",
|
||||
contentFolder: "content",
|
||||
templatesFolder: "templates",
|
||||
mediaDatabaseFile: "mediaDb.json"
|
||||
}
|
||||
1
src/constants/Navigation.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const HOME_PAGE_NAVIGATION_ID = "FrontMatter:RootFolder";
|
||||
@@ -1,5 +1,6 @@
|
||||
export const CONTEXT = {
|
||||
canInit: "frontMatterCanInit",
|
||||
canOpenPreview: "frontMatterCanOpenPreview",
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard"
|
||||
canOpenDashboard: "frontMatterCanOpenDashboard",
|
||||
isEnabled: "frontMatter:enabled"
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
export * from './ContentType';
|
||||
export * from './DefaultFields';
|
||||
export * from './Extension';
|
||||
export * from './ExtensionState';
|
||||
export * from './Links';
|
||||
export * from './LocalStore';
|
||||
export * from './Navigation';
|
||||
export * from './charMap';
|
||||
export * from './context';
|
||||
export * from './settings';
|
||||
|
||||
@@ -20,6 +20,7 @@ export const SETTING_REMOVE_QUOTES = "taxonomy.noPropertyValueQuotes";
|
||||
export const SETTING_FRONTMATTER_TYPE = "taxonomy.frontMatterType";
|
||||
|
||||
export const SETTING_SEO_TITLE_LENGTH = "taxonomy.seoTitleLength";
|
||||
export const SETTING_SEO_SLUG_LENGTH = "taxonomy.seoSlugLength";
|
||||
export const SETTING_SEO_DESCRIPTION_LENGTH = "taxonomy.seoDescriptionLength";
|
||||
export const SETTING_SEO_CONTENT_MIN_LENGTH = "taxonomy.seoContentLengh";
|
||||
export const SETTING_SEO_DESCRIPTION_FIELD = "taxonomy.seoDescriptionField";
|
||||
@@ -36,12 +37,18 @@ export const SETTING_CUSTOM_SCRIPTS = "custom.scripts";
|
||||
|
||||
export const SETTING_AUTO_UPDATE_DATE = "content.autoUpdateDate";
|
||||
export const SETTINGS_CONTENT_PAGE_FOLDERS = "content.pageFolders";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDERS = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_STATIC_FOLDER = "content.publicFolder";
|
||||
export const SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT = "content.fmHighlight";
|
||||
export const SETTINGS_CONTENT_DRAFT_FIELD = "content.draftField";
|
||||
export const SETTINGS_CONTENT_SORTING = "content.sorting";
|
||||
|
||||
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
export const SETTINGS_FRAMEWORK_ID = "framework.id";
|
||||
|
||||
export const SETTING_SITE_BASEURL = "site.baseURL";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -3,5 +3,5 @@ export enum DashboardCommand {
|
||||
pages = "pages",
|
||||
settings = "settings",
|
||||
media = "media",
|
||||
viewData = "viewData",
|
||||
viewData = "viewData"
|
||||
}
|
||||
@@ -4,6 +4,8 @@ export enum DashboardMessage {
|
||||
openFile = 'openFile',
|
||||
getTheme = 'getTheme',
|
||||
createContent = 'createContent',
|
||||
createByContentType = 'createByContentType',
|
||||
createByTemplate = 'createByTemplate',
|
||||
updateSetting = 'updateSetting',
|
||||
initializeProject = 'initializeProject',
|
||||
reload = 'reload',
|
||||
@@ -14,4 +16,8 @@ export enum DashboardMessage {
|
||||
uploadMedia = 'uploadMedia',
|
||||
deleteMedia = 'deleteMedia',
|
||||
insertPreviewImage = 'insertPreviewImage',
|
||||
updateMediaMetadata = 'updateMediaMetadata',
|
||||
createMediaFolder = 'createMediaFolder',
|
||||
setFramework = 'setFramework',
|
||||
setState = 'setState',
|
||||
}
|
||||
52
src/dashboardWebView/components/ChoiceButton.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { MenuItem, MenuItems } from './Menu';
|
||||
|
||||
export interface IChoiceButtonProps {
|
||||
title: string;
|
||||
choices: {
|
||||
title: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ChoiceButton: React.FunctionComponent<IChoiceButtonProps> = ({onClick, disabled, choices, title}: React.PropsWithChildren<IChoiceButtonProps>) => {
|
||||
return (
|
||||
<span className="relative z-50 inline-flex shadow-sm rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<Menu as="span" className="-ml-px relative block">
|
||||
<Menu.Button
|
||||
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-700 hover:bg-teal-800 focus:outline-none disabled:bg-gray-500"
|
||||
disabled={disabled}>
|
||||
<span className="sr-only">Open options</span>
|
||||
<ChevronDownIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
<MenuItems widthClass={`w-56`}>
|
||||
<div className="py-1">
|
||||
{choices.map((choice) => (
|
||||
<MenuItem
|
||||
key={choice.title}
|
||||
title={choice.title}
|
||||
value={null}
|
||||
onClick={choice.onClick}
|
||||
disabled={choice.disabled} />
|
||||
))}
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -3,15 +3,20 @@ import { useRecoilValue } from 'recoil';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models/Page';
|
||||
import { ViewSelector, ViewType } from '../../state';
|
||||
import { SettingsSelector, ViewSelector, ViewType } from '../../state';
|
||||
import { DateField } from '../DateField';
|
||||
import { Status } from '../Status';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import useContentType from '../../../hooks/useContentType';
|
||||
|
||||
export interface IItemProps extends Page {}
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, preview }: React.PropsWithChildren<IItemProps>) => {
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren<IItemProps>) => {
|
||||
const view = useRecoilValue(ViewSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const contentType = useContentType(settings, { type });
|
||||
|
||||
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
|
||||
|
||||
const openFile = () => {
|
||||
Messenger.send(DashboardMessage.openFile, fmFilePath);
|
||||
@@ -24,8 +29,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
onClick={openFile}>
|
||||
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
|
||||
{
|
||||
preview ? (
|
||||
<img src={`${preview}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
previewField && pageData[previewField] ? (
|
||||
<img src={`${pageData[previewField]}`} alt={title} className="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
) : (
|
||||
<div className={`flex items-center justify-center bg-whisper-500 dark:bg-vulcan-200 dark:group-hover:bg-vulcan-100`}>
|
||||
<MarkdownIcon className={`h-32 text-vulcan-100 dark:text-whisper-100`} />
|
||||
@@ -36,7 +41,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
|
||||
<div className="p-4 w-full">
|
||||
<div className={`flex justify-between items-center`}>
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
@@ -59,7 +64,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
<DateField value={date} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Status draft={!!draft} />
|
||||
<Status draft={draft} />
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -5,7 +5,7 @@ import useDarkMode from '../../hooks/useDarkMode';
|
||||
import usePages from '../hooks/usePages';
|
||||
import { WelcomeScreen } from './WelcomeScreen';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardViewSelector, ViewDataAtom } from '../state';
|
||||
import { DashboardViewSelector } from '../state';
|
||||
import { Contents } from './Contents/Contents';
|
||||
import { Media } from './Media/Media';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { format, parseJSON } from 'date-fns';
|
||||
import { format } from 'date-fns';
|
||||
import * as React from 'react';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
|
||||
export interface IDateFieldProps {
|
||||
value: Date | string;
|
||||
@@ -10,9 +11,9 @@ export const DateField: React.FunctionComponent<IDateFieldProps> = ({value}: Rea
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const parsedValue = typeof value === 'string' ? parseJSON(value) : value;
|
||||
const dateString = format(parsedValue, 'yyyy-MM-dd');
|
||||
setDateValue(dateString);
|
||||
const parsedValue = typeof value === 'string' ? DateHelper.tryParse(value) : value;
|
||||
const dateString = parsedValue ? format(parsedValue, 'yyyy-MM-dd') : parsedValue;
|
||||
setDateValue(dateString || "");
|
||||
} catch (e) {
|
||||
// Date is invalid
|
||||
}
|
||||
|
||||
100
src/dashboardWebView/components/Header/Breadcrumb.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { CollectionIcon } from '@heroicons/react/outline';
|
||||
import { basename, join } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
|
||||
|
||||
export interface IBreadcrumbProps {}
|
||||
|
||||
export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: React.PropsWithChildren<IBreadcrumbProps>) => {
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
const [ folders, setFolders ] = React.useState<string[]>([]);
|
||||
|
||||
if (!settings?.wsFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const { wsFolder, staticFolder, contentFolders } = settings;
|
||||
|
||||
const isValid = (folderPath: string) => {
|
||||
if (staticFolder) {
|
||||
const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string;
|
||||
const relPath = folderPath.replace(staticPath, '') as string;
|
||||
|
||||
if (relPath.length > 1 && folderPath.startsWith(staticPath)) {
|
||||
return true;
|
||||
} else if (relPath.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1
|
||||
for (let i = 0; i < contentFolders.length; i++) {
|
||||
const folder = contentFolders[i];
|
||||
const contentFolder = parseWinPath(folder.path) as string;
|
||||
const relContentPath = folderPath.replace(contentFolder, '');
|
||||
return relContentPath.length > 1 && folderPath.startsWith(contentFolder);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!selectedFolder) {
|
||||
setFolders([]);
|
||||
} else {
|
||||
const relPath = parseWinPath(selectedFolder.replace(parseWinPath(settings.wsFolder) as string, '')) as string;
|
||||
const folderParts = relPath.split('/').filter(f => f);
|
||||
const allFolders: string[] = [];
|
||||
let previousFolder = parseWinPath(settings.wsFolder) as string;
|
||||
|
||||
for (const part of folderParts) {
|
||||
const folder = join(previousFolder, part);
|
||||
if (isValid(folder)) {
|
||||
allFolders.push(folder);
|
||||
}
|
||||
previousFolder = folder;
|
||||
}
|
||||
|
||||
setFolders(allFolders);
|
||||
}
|
||||
}, [selectedFolder]);
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex py-2" aria-label="Breadcrumb">
|
||||
<ol role="list" className="w-full mx-auto flex space-x-4 px-5">
|
||||
<li className="flex">
|
||||
<div className="flex items-center">
|
||||
<button onClick={() => setSelectedFolder(HOME_PAGE_NAVIGATION_ID)} className="text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500">
|
||||
<CollectionIcon className="flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
<span className="sr-only">Home</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{folders.map((folder) => (
|
||||
<li key={folder} className="flex">
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="flex-shrink-0 h-5 w-5 text-gray-300 dark:text-whisper-900"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<button
|
||||
onClick={() => setSelectedFolder(folder)}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500"
|
||||
>
|
||||
{basename(folder)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { XCircleIcon } from '@heroicons/react/solid';
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { SortingSelector, FolderSelector, TagSelector, CategorySelector, SortingAtom, DEFAULT_SORTING_OPTION, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
|
||||
import { FolderSelector, TagSelector, CategorySelector, SortingAtom, FolderAtom, DEFAULT_FOLDER_STATE, TagAtom, CategoryAtom, DEFAULT_TAG_STATE, DEFAULT_CATEGORY_STATE } from '../../state';
|
||||
|
||||
import { DefaultValue } from 'recoil';
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface IClearFiltersProps {}
|
||||
export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props: React.PropsWithChildren<IClearFiltersProps>) => {
|
||||
const [ show, setShow ] = React.useState(false);
|
||||
|
||||
const sorting = useRecoilValue(SortingSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
@@ -36,19 +35,19 @@ export const ClearFilters: React.FunctionComponent<IClearFiltersProps> = (props:
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (sorting !== DEFAULT_SORTING_OPTION || folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
|
||||
if (folder !== DEFAULT_FOLDER_STATE || tag !== DEFAULT_TAG_STATE || category !== DEFAULT_CATEGORY_STATE) {
|
||||
setShow(true);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
}, [sorting, folder, tag, category]);
|
||||
}, [folder, tag, category]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
<button className="flex items-center hover:text-teal-600" onClick={reset} title={`Clear filters, grouping, and sorting`}>
|
||||
<XCircleIcon className={`inline-block w-5 h-5 mr-1`} /><span>Clear</span>
|
||||
<span className={`sr-only`}> filters, grouping, and sorting</span>
|
||||
<span className={`sr-only`}> filters and grouping</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { FolderAtom } from '../../state';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { FolderAtom, SettingsSelector } from '../../state';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
|
||||
export interface IFoldersProps {
|
||||
folders: string[];
|
||||
}
|
||||
export interface IFoldersProps {}
|
||||
|
||||
const DEFAULT_TYPE = "All types";
|
||||
|
||||
export const Folders: React.FunctionComponent<IFoldersProps> = ({folders}: React.PropsWithChildren<IFoldersProps>) => {
|
||||
export const Folders: React.FunctionComponent<IFoldersProps> = ({}: React.PropsWithChildren<IFoldersProps>) => {
|
||||
const [ crntFolder, setCrntFolder ] = useRecoilState(FolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const contentFolders = settings?.contentFolders || [];
|
||||
|
||||
if (folders.length <= 1) {
|
||||
if (contentFolders.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ export const Folders: React.FunctionComponent<IFoldersProps> = ({folders}: React
|
||||
isCurrent={!crntFolder}
|
||||
onClick={(value) => setCrntFolder(value)} />
|
||||
|
||||
{folders.map((option) => (
|
||||
{contentFolders.map((option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
title={option}
|
||||
value={option}
|
||||
isCurrent={option === crntFolder}
|
||||
key={option.title}
|
||||
title={option.title}
|
||||
value={option.title}
|
||||
isCurrent={option.title === crntFolder}
|
||||
onClick={(value) => setCrntFolder(value)} />
|
||||
))}
|
||||
</MenuItems>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Folders } from './Folders';
|
||||
import { Settings } from '../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Startup } from '../Startup';
|
||||
import { Button } from '../Button';
|
||||
import { Navigation } from '../Navigation';
|
||||
import { Grouping } from '.';
|
||||
import { ViewSwitch } from './ViewSwitch';
|
||||
@@ -17,6 +16,8 @@ import { ClearFilters } from './ClearFilters';
|
||||
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
|
||||
import { PhotographIcon } from '@heroicons/react/outline';
|
||||
import { Pagination } from '../Media/Pagination';
|
||||
import { ChoiceButton } from '../ChoiceButton';
|
||||
import { Breadcrumb } from './Breadcrumb';
|
||||
|
||||
export interface IHeaderProps {
|
||||
settings: Settings | null;
|
||||
@@ -37,6 +38,14 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
Messenger.send(DashboardMessage.createContent);
|
||||
};
|
||||
|
||||
const createByContentType = () => {
|
||||
Messenger.send(DashboardMessage.createByContentType);
|
||||
};
|
||||
|
||||
const createByTemplate = () => {
|
||||
Messenger.send(DashboardMessage.createByTemplate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
|
||||
|
||||
@@ -60,7 +69,19 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
<div className={`flex items-center space-x-4`}>
|
||||
<Startup settings={settings} />
|
||||
|
||||
<Button onClick={createContent} disabled={!settings?.initialized}>Create content</Button>
|
||||
<ChoiceButton
|
||||
title={`Create content`}
|
||||
choices={[{
|
||||
title: `Create by content type`,
|
||||
onClick: createByContentType,
|
||||
disabled: !settings?.initialized
|
||||
}, {
|
||||
title: `Create by template`,
|
||||
onClick: createByTemplate,
|
||||
disabled: !settings?.initialized
|
||||
}]}
|
||||
onClick={createContent}
|
||||
disabled={!settings?.initialized} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +98,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end space-x-4 lg:space-x-6 xl:space-x-8 bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100`}>
|
||||
<ClearFilters />
|
||||
|
||||
<Folders folders={folders || []} />
|
||||
<Folders />
|
||||
|
||||
<Filter label={`Tag`} activeItem={crntTag} items={settings?.tags || []} onClick={(value) => setCrntTag(value)} />
|
||||
|
||||
@@ -93,7 +114,10 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
|
||||
|
||||
{
|
||||
view === "media" && (
|
||||
<Pagination />
|
||||
<>
|
||||
<Pagination />
|
||||
<Breadcrumb />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,64 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { ExtensionState } from '../../../constants';
|
||||
import { SortOrder, SortType } from '../../../models';
|
||||
import { SortOption } from '../../constants/SortOption';
|
||||
import { SearchSelector, SortingAtom } from '../../state';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SortingOption } from '../../models/SortingOption';
|
||||
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
|
||||
import { MenuButton, MenuItem, MenuItems } from '../Menu';
|
||||
|
||||
export interface ISortingProps {}
|
||||
|
||||
export const sortOptions = [
|
||||
{ name: "Last modified", id: SortOption.LastModified },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc },
|
||||
export const sortOptions: SortingOption[] = [
|
||||
{ name: "Last modified", id: SortOption.LastModified, order: SortOrder.desc, type: SortType.string },
|
||||
{ name: "By filename (asc)", id: SortOption.FileNameAsc, order: SortOrder.asc, type: SortType.string },
|
||||
{ name: "By filename (desc)", id: SortOption.FileNameDesc, order: SortOrder.desc, type: SortType.string },
|
||||
];
|
||||
|
||||
export const Sorting: React.FunctionComponent<ISortingProps> = ({}: React.PropsWithChildren<ISortingProps>) => {
|
||||
const [ crntSorting, setCrntSorting ] = useRecoilState(SortingAtom);
|
||||
const searchValue = useRecoilValue(SearchSelector);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const crntSort = sortOptions.find(x => x.id === crntSorting);
|
||||
const updateSorting = (value: SortingOption) => {
|
||||
|
||||
Messenger.send(DashboardMessage.setState, {
|
||||
key: ExtensionState.Dashboard.Sorting,
|
||||
value: value
|
||||
})
|
||||
|
||||
setCrntSorting(value)
|
||||
};
|
||||
|
||||
let allOptions = [...sortOptions];
|
||||
if (settings?.customSorting) {
|
||||
allOptions = [...allOptions, ...settings.customSorting.map((s) => ({
|
||||
title: s.title || s.name,
|
||||
name: s.name,
|
||||
id: `${s.name}-${s.order}`,
|
||||
order: s.order,
|
||||
type: s.type
|
||||
}))];
|
||||
}
|
||||
|
||||
let crntSort = allOptions.find(x => x.id === crntSorting?.id) || sortOptions[0];
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Menu as="div" className="relative z-10 inline-block text-left">
|
||||
<MenuButton label={`Sort by`} title={crntSort?.name || ""} disabled={!!searchValue} />
|
||||
<MenuButton label={`Sort by`} title={crntSort?.title || crntSort?.name || ""} disabled={!!searchValue} />
|
||||
|
||||
<MenuItems>
|
||||
{sortOptions.map((option) => (
|
||||
{allOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.id}
|
||||
title={option.name}
|
||||
value={option.id}
|
||||
isCurrent={option.id === crntSorting}
|
||||
onClick={(value) => setCrntSorting(value)} />
|
||||
title={option.title || option.name}
|
||||
value={option}
|
||||
isCurrent={option.id === crntSorting?.id}
|
||||
onClick={(value) => updateSorting(value)} />
|
||||
))}
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
|
||||
28
src/dashboardWebView/components/Media/FolderCreation.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { FolderAddIcon } from '@heroicons/react/outline';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { SelectedMediaFolderAtom } from '../../state';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
|
||||
export interface IFolderCreationProps {}
|
||||
|
||||
export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (props: React.PropsWithChildren<IFolderCreationProps>) => {
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
|
||||
|
||||
const onFolderCreation = () => {
|
||||
Messenger.send(DashboardMessage.createMediaFolder, {
|
||||
selectedFolder
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create new folder`}
|
||||
onClick={onFolderCreation}>
|
||||
<FolderAddIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={``}>Create new folder</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
29
src/dashboardWebView/components/Media/FolderItem.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { FolderIcon } from '@heroicons/react/solid';
|
||||
import { basename } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { SelectedMediaFolderAtom } from '../../state';
|
||||
|
||||
export interface IFolderItemProps {
|
||||
folder: string;
|
||||
wsFolder?: string;
|
||||
staticFolder?: string;
|
||||
}
|
||||
|
||||
export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder, wsFolder, staticFolder }: React.PropsWithChildren<IFolderItemProps>) => {
|
||||
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
|
||||
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
|
||||
|
||||
return (
|
||||
<li className={`group relative bg-gray-200 dark:bg-vulcan-300 hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
|
||||
<button className={`w-full flex flex-col items-center`} onClick={() => setSelectedFolder(folder)}>
|
||||
<FolderIcon className={`h-auto w-1/2`} />
|
||||
|
||||
<p className="text-sm font-bold pointer-events-none flex items-center">
|
||||
{basename(relFolderPath)}
|
||||
</p>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { XIcon } from '@heroicons/react/outline';
|
||||
import Downshift from 'downshift';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaFoldersSelector, SelectedMediaFolderAtom } from '../../state';
|
||||
|
||||
export interface IFolderSelectionProps {}
|
||||
|
||||
export const FolderSelection: React.FunctionComponent<IFolderSelectionProps> = (props: React.PropsWithChildren<IFolderSelectionProps>) => {
|
||||
const folders = useRecoilValue(MediaFoldersSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ focus, setFocus ] = React.useState(false);
|
||||
|
||||
let allFolders: string[] = Object.assign([], folders);
|
||||
allFolders = allFolders.sort((a: string, b: string) => {
|
||||
if (a.toLowerCase() < b.toLowerCase()) return -1;
|
||||
if (a.toLowerCase() > b.toLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Downshift
|
||||
isOpen={focus}
|
||||
selectedItem={selectedFolder}
|
||||
onOuterClick={() => setFocus(false)}
|
||||
onSelect={(selFolder) => {
|
||||
setSelectedFolder(selFolder);
|
||||
setFocus(false);
|
||||
}}>
|
||||
{
|
||||
({
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
getMenuProps,
|
||||
isOpen,
|
||||
inputValue,
|
||||
getRootProps
|
||||
}) => (
|
||||
<div className={`relative flex items-center`}>
|
||||
<label className={`text-sm text-gray-500 dark:text-whisper-900`}>Filter by: </label>
|
||||
|
||||
<div
|
||||
className={`inline-flex items-center`}
|
||||
{...getRootProps({} as any, {suppressRefError: true})}
|
||||
>
|
||||
<input disabled={!!selectedFolder} onFocus={() => setFocus(true)} className={`ml-2 py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none`} {...getInputProps()} />
|
||||
|
||||
{
|
||||
selectedFolder && (
|
||||
<button title={`Clear`} onClick={() => setSelectedFolder(null)}><XIcon className={`ml-2 h-6 w-6 text-red-500 hover:text-red-800`} /></button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={`${focus ? `block` : `hidden`} top-8 absolute right-0 z-10 mt-2 w-min rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`} {...getMenuProps()}>
|
||||
{isOpen
|
||||
? allFolders
|
||||
.filter((item: string) => !inputValue || item.includes(inputValue))
|
||||
.map((item, index) => (
|
||||
<div
|
||||
className="cursor-pointer text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100"
|
||||
{...getItemProps({ key: item, index, item })}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Downshift>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,31 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { CheckCircleIcon, ClipboardCopyIcon, CodeIcon, PencilIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { MediaInfo } from '../../../models/MediaPaths';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { Metadata } from '../Modals/Metadata';
|
||||
|
||||
export interface IItemProps {
|
||||
media: MediaInfo;
|
||||
}
|
||||
|
||||
export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWithChildren<IItemProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const [ , setLightbox ] = useRecoilState(LightboxAtom);
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
const [ showForm, setShowForm ] = React.useState(false);
|
||||
const [ caption, setCaption ] = React.useState(media.caption);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
const [ filename, setFilename ] = React.useState<string | null>(null);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const parseWinPath = (path: string | undefined) => {
|
||||
return path?.split(`\\`).join(`/`);
|
||||
}
|
||||
const page = useRecoilValue(PageSelector);
|
||||
|
||||
const getFolder = () => {
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
@@ -48,6 +52,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return relPath;
|
||||
};
|
||||
|
||||
const getFileName = () => {
|
||||
return basename(parseWinPath(media.fsPath) || "");
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
const relPath = getRelPath();
|
||||
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
|
||||
@@ -59,18 +67,27 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
image: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
position: viewData?.data?.position || null
|
||||
multiple: viewData?.data?.multiple,
|
||||
value: viewData?.data?.value,
|
||||
position: viewData?.data?.position || null,
|
||||
alt: alt || "",
|
||||
caption: caption || ""
|
||||
});
|
||||
};
|
||||
|
||||
const insertSnippet = () => {
|
||||
const relPath = getRelPath();
|
||||
let snippet = settings?.mediaSnippet.join("\n");
|
||||
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
|
||||
snippet = snippet?.replace("{alt}", alt || "");
|
||||
snippet = snippet?.replace("{caption}", caption || "");
|
||||
|
||||
Messenger.send(DashboardMessage.insertPreviewImage, {
|
||||
image: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
position: viewData?.data?.position || null,
|
||||
snippet: settings?.mediaSnippet.join("\n").replace("{mediaUrl}", parseWinPath(relPath) || "")
|
||||
snippet
|
||||
});
|
||||
};
|
||||
|
||||
@@ -86,20 +103,75 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const calculateSize = () => {
|
||||
let sizeDetails = [];
|
||||
|
||||
if (media?.dimensions) {
|
||||
if (media.dimensions.width && media.dimensions.height) {
|
||||
sizeDetails.push(`${media.dimensions.width}x${media.dimensions.height}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (media?.stats?.size) {
|
||||
const size = media.stats.size / (1024*1024);
|
||||
if (size > 1) {
|
||||
return `${size.toFixed(2)} MB`;
|
||||
sizeDetails.push(`${size.toFixed(2)} MB`);
|
||||
} else {
|
||||
return `${(size * 1024).toFixed(2)} KB`;
|
||||
sizeDetails.push(`${(size * 1024).toFixed(2)} KB`);
|
||||
}
|
||||
}
|
||||
|
||||
return sizeDetails.join(" — ");
|
||||
};
|
||||
|
||||
const openLightbox = () => {
|
||||
setLightbox(media.vsPath || "");
|
||||
};
|
||||
|
||||
const updateMetadata = () => {
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const submitMetadata = () => {
|
||||
Messenger.send(DashboardMessage.updateMediaMetadata, {
|
||||
file: media.fsPath,
|
||||
filename,
|
||||
caption,
|
||||
alt,
|
||||
folder: selectedFolder,
|
||||
page
|
||||
});
|
||||
|
||||
setShowForm(false);
|
||||
|
||||
// Reset the values
|
||||
setAlt(media.alt);
|
||||
setCaption(media.caption);
|
||||
setFilename(getFileName());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (media.alt !== alt) {
|
||||
setAlt(media.alt);
|
||||
}
|
||||
}, [media.alt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (media.caption !== caption) {
|
||||
setCaption(media.caption);
|
||||
}
|
||||
}, [media.caption]);
|
||||
|
||||
useEffect(() => {
|
||||
const name = basename(parseWinPath(media.fsPath) || "");
|
||||
if (name !== filename) {
|
||||
setFilename(getFileName());
|
||||
}
|
||||
}, [media.fsPath]);
|
||||
|
||||
const fileInfo = filename ? basename(filename).split('.') : null;
|
||||
const extension = fileInfo?.pop();
|
||||
const name = fileInfo?.join('.');
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
|
||||
@@ -111,17 +183,24 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
|
||||
</div>
|
||||
</button>
|
||||
<div className={`relative py-4 pl-4 pr-10`}>
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-2`}>
|
||||
<div className={`relative py-4 pl-4 pr-12`}>
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
<button title={`Edit metadata`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={updateMetadata}>
|
||||
<PencilIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Edit metadata</span>
|
||||
</button>
|
||||
|
||||
{
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<button
|
||||
title={`Insert into your article`}
|
||||
title={`Insert into your content`}
|
||||
className={`hover:text-teal-900 focus:outline-none`}
|
||||
onClick={insertToArticle}>
|
||||
<CheckCircleIcon className={`h-5 w-5`} />
|
||||
<span className={`sr-only`}>Insert into your article</span>
|
||||
<span className={`sr-only`}>Insert into your content</span>
|
||||
</button>
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
@@ -156,19 +235,95 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
|
||||
{basename(parseWinPath(media.fsPath) || "")}
|
||||
</p>
|
||||
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
|
||||
<b className={`mr-2`}>Folder:</b> {getFolder()}
|
||||
{
|
||||
media.caption && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-2`}>Caption:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.caption}</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
{
|
||||
media.alt && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-2`}>Alt:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.alt}</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-2`}>Folder:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getFolder()}</span>
|
||||
</p>
|
||||
{
|
||||
media?.stats?.size && (
|
||||
<p className="mt-2 text-sm dark:text-whisper-900 font-medium pointer-events-none flex items-center">
|
||||
<b className={`mr-1`}>Size:</b> {calculateSize()}
|
||||
(media?.stats?.size || media?.dimensions) && (
|
||||
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
|
||||
<b className={`mr-1`}>Size:</b>
|
||||
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{calculateSize()}</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showForm && (
|
||||
<Metadata
|
||||
title={`Set metadata for: ${basename(parseWinPath(media.fsPath) || "")}`}
|
||||
description={`Please specify the metadata you want to set for the file.`}
|
||||
okBtnText={`Save`}
|
||||
cancelBtnText={`Cancel`}
|
||||
dismiss={() => setShowForm(false)}
|
||||
trigger={submitMetadata}
|
||||
isSaveDisabled={!filename}>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Filename
|
||||
</label>
|
||||
<div className="relative mt-1">
|
||||
<input
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={name}
|
||||
onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
|
||||
|
||||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500 sm:text-sm">
|
||||
.{extension}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Caption
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
rows={3}
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={caption || ''}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
|
||||
Alt tag value
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
|
||||
value={alt || ''}
|
||||
onChange={(e) => setAlt(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Metadata>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showAlert && (
|
||||
<Alert
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
import { Header } from '../Header';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
@@ -15,6 +15,8 @@ import { List } from './List';
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useCallback } from 'react';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { FolderItem } from './FolderItem';
|
||||
|
||||
export interface IMediaProps {}
|
||||
|
||||
@@ -22,10 +24,10 @@ export const LIMIT = 16;
|
||||
|
||||
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
|
||||
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
|
||||
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
|
||||
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
|
||||
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
@@ -51,12 +53,14 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
accept: 'image/*'
|
||||
});
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<MediaPaths>>) => {
|
||||
const messageListener = (message: MessageEvent<EventData<MediaPaths | { key: string, value: any }>>) => {
|
||||
if (message.data.command === DashboardCommand.media) {
|
||||
const data: MediaPaths = message.data.data as MediaPaths;
|
||||
setLoading(false);
|
||||
setMedia(message.data.data.media);
|
||||
setTotal(message.data.data.total);
|
||||
setFolders(message.data.data.folders);
|
||||
setMedia(data.media);
|
||||
setTotal(data.total);
|
||||
setFolders(data.folders);
|
||||
setSelectedFolder(data.selectedFolder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,6 +99,32 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
(media.length === 0 && folders.length === 0 && !loading) && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<div className={`max-w-xl text-center`}>
|
||||
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
|
||||
|
||||
<p className={`text-xl font-medium`}>No media files to show. You can drag & drop new files.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
folders && folders.length > 0 && (
|
||||
<div className={`mb-8`}>
|
||||
<List>
|
||||
{
|
||||
folders && folders.map((folder) => (
|
||||
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<List>
|
||||
{
|
||||
media.map((file) => (
|
||||
|
||||
@@ -3,8 +3,8 @@ import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LoadingAtom, MediaTotalSelector, SelectedMediaFolderSelector } from '../../state';
|
||||
import { FolderSelection } from './FolderSelection';
|
||||
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector } from '../../state';
|
||||
import { FolderCreation } from './FolderCreation';
|
||||
import { LIMIT } from './Media';
|
||||
import { PaginationButton } from './PaginationButton';
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const totalMedia = useRecoilValue(MediaTotalSelector);
|
||||
const [ , setLoading ] = useRecoilState(LoadingAtom);
|
||||
const [ page, setPage ] = React.useState<number>(0);
|
||||
const [ page, setPage ] = useRecoilState(PageAtom);
|
||||
|
||||
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FolderSelection />
|
||||
<FolderCreation />
|
||||
|
||||
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
|
||||
<PaginationButton
|
||||
|
||||
@@ -4,16 +4,18 @@ import * as React from 'react';
|
||||
export interface IMenuItemProps {
|
||||
title: string;
|
||||
value: any;
|
||||
isCurrent: boolean;
|
||||
isCurrent?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: any) => void;
|
||||
}
|
||||
|
||||
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
|
||||
export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value, isCurrent, disabled, onClick}: React.PropsWithChildren<IMenuItemProps>) => {
|
||||
return (
|
||||
<Menu.Item>
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={() => onClick(value)}
|
||||
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100`}
|
||||
className={`${!isCurrent ? `text-vulcan-500 dark:text-whisper-500` : `text-gray-500 dark:text-whisper-900`} block px-4 py-2 text-sm font-medium w-full text-left hover:bg-gray-100 hover:text-gray-700 dark:hover:text-whisper-600 dark:hover:bg-vulcan-100 disabled:bg-gray-500`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Menu, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export interface IMenuItemsProps {}
|
||||
export interface IMenuItemsProps {
|
||||
widthClass?: string;
|
||||
}
|
||||
|
||||
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}: React.PropsWithChildren<IMenuItemsProps>) => {
|
||||
export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass, children}: React.PropsWithChildren<IMenuItemsProps>) => {
|
||||
return (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@@ -15,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({children}:
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 z-10 mt-2 w-40 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto">
|
||||
<Menu.Items className={`${widthClass || "w-40"} origin-top-right absolute right-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
92
src/dashboardWebView/components/Modals/Metadata.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { Fragment, useRef } from 'react';
|
||||
|
||||
export interface IMetadataProps {
|
||||
title: string;
|
||||
description: string;
|
||||
okBtnText: string;
|
||||
cancelBtnText: string;
|
||||
isSaveDisabled: boolean;
|
||||
|
||||
dismiss: () => void;
|
||||
trigger: () => void;
|
||||
}
|
||||
|
||||
export const Metadata: React.FunctionComponent<IMetadataProps> = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren<IMetadataProps>) => {
|
||||
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
|
||||
return (
|
||||
<Transition.Root show={true} as={Fragment}>
|
||||
<Dialog className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef} onClose={() => dismiss()}>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30"
|
||||
onClick={() => trigger()}
|
||||
disabled={isSaveDisabled}
|
||||
>
|
||||
{okBtnText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-200 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm"
|
||||
onClick={() => dismiss()}
|
||||
ref={cancelButtonRef}
|
||||
>
|
||||
{cancelBtnText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { TabAtom } from '../state';
|
||||
import { SettingsAtom, TabAtom } from '../state';
|
||||
|
||||
export interface INavigationProps {
|
||||
totalPages: number;
|
||||
@@ -15,19 +15,47 @@ export const tabs = [
|
||||
|
||||
export const Navigation: React.FunctionComponent<INavigationProps> = ({totalPages}: React.PropsWithChildren<INavigationProps>) => {
|
||||
const [ crntTab, setCrntTab ] = useRecoilState(TabAtom);
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
return (
|
||||
<nav className="flex-1 -mb-px flex space-x-6 xl:space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))}
|
||||
{
|
||||
settings?.draftField?.type === "boolean" ? (
|
||||
tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.name}
|
||||
className={`${tab.id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tab.id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tab.id)}
|
||||
>
|
||||
{tab.name}{(tab.id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`${tabs[0].id === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm`}
|
||||
aria-current={tabs[0].id === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(tabs[0].id)}
|
||||
>
|
||||
{tabs[0].name}{(tabs[0].id === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
|
||||
{
|
||||
settings?.draftField?.choices?.map((value, idx) => (
|
||||
<button
|
||||
key={`${value}-${idx}`}
|
||||
className={`${value === crntTab ? `border-teal-900 dark:border-teal-300 text-teal-900 dark:text-teal-300` : `border-transparent text-gray-500 dark:text-whisper-600 hover:text-gray-700 dark:hover:text-whisper-700 hover:border-gray-300 dark:hover:border-whisper-500`} whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm first-letter:uppercase`}
|
||||
aria-current={value === crntTab ? 'page' : undefined}
|
||||
onClick={() => setCrntTab(value)}
|
||||
>
|
||||
{value}{(value === crntTab && totalPages) ? ` (${totalPages})` : ''}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
|
||||
import { REVIEW_LINK, SPONSOR_LINK } from '../../constants';
|
||||
import { VersionInfo } from '../../models';
|
||||
|
||||
export interface ISponsorMsgProps {
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsAtom } from '../state';
|
||||
|
||||
export interface IStatusProps {
|
||||
draft: boolean;
|
||||
draft: boolean | string;
|
||||
}
|
||||
|
||||
export const Status: React.FunctionComponent<IStatusProps> = ({draft}: React.PropsWithChildren<IStatusProps>) => {
|
||||
const settings = useRecoilValue(SettingsAtom);
|
||||
|
||||
if (settings?.draftField && settings.draftField.type === "choice") {
|
||||
if (draft) {
|
||||
return <span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 bg-teal-500`}>{draft}</span>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-block px-2 py-1 leading-none rounded-full font-semibold uppercase tracking-wide text-xs text-whisper-200 dark:text-vulcan-500 ${draft ? "bg-red-500" : "bg-teal-500"}`}>{draft ? "Draft" : "Published"}</span>
|
||||
);
|
||||
|
||||
@@ -4,22 +4,17 @@ import { Status } from '../../models/Status';
|
||||
|
||||
export interface IStepProps {
|
||||
name: string;
|
||||
description: string;
|
||||
description: JSX.Element;
|
||||
status: Status;
|
||||
showLine: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: () => void | undefined;
|
||||
}
|
||||
|
||||
export const Step: React.FunctionComponent<IStepProps> = ({name, description, status, showLine, onClick}: React.PropsWithChildren<IStepProps>) => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
<button className={`relative flex items-start group text-left ${onClick ? "" : "cursor-default"}`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
const renderChildren = () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
status === Status.NotStarted && (
|
||||
<span className="h-9 flex items-center" aria-hidden="true">
|
||||
@@ -52,9 +47,31 @@ export const Step: React.FunctionComponent<IStepProps> = ({name, description, st
|
||||
|
||||
<span className="ml-4 min-w-0 flex flex-col">
|
||||
<span className="text-xs font-semibold tracking-wide uppercase text-vulcan-500 dark:text-whisper-500">{name}</span>
|
||||
<span className="text-sm text-vulcan-400 dark:text-whisper-600" dangerouslySetInnerHTML={{__html: description}} />
|
||||
<div className="text-sm text-vulcan-400 dark:text-whisper-600">{description}</div>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLine ? (
|
||||
<div className={`-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full ${status === Status.Completed ? "bg-teal-600" : "bg-gray-300"}`} aria-hidden="true" />
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
onClick ? (
|
||||
<button className={`relative flex items-start group text-left`} onClick={() => { if (onClick) { onClick(); } }} disabled={!onClick}>
|
||||
{renderChildren()}
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative flex items-start group text-left">
|
||||
{renderChildren()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -4,36 +4,99 @@ import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Settings } from '../../models/Settings';
|
||||
import { Status } from '../../models/Status';
|
||||
import { Step } from './Step';
|
||||
import { useState } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { MenuItem } from '../Menu';
|
||||
import { Framework } from '../../../models';
|
||||
import { ChevronDownIcon } from '@heroicons/react/outline';
|
||||
import { FrameworkDetectors } from '../../../constants/FrameworkDetectors';
|
||||
|
||||
export interface IStepsToGetStartedProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
|
||||
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
|
||||
const setFrameworkAndSendMessage = (framework: string) => {
|
||||
setFramework(framework);
|
||||
Messenger.send(DashboardMessage.setFramework, framework);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: 'Initialize project',
|
||||
description: 'Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.',
|
||||
description: <>Initialize the project with a template folder and sample markdown file. The template folder can be used to define your own templates. <b>Start by clicking on this action</b>.</>,
|
||||
status: settings.initialized ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.initialized ? undefined : () => { Messenger.send(DashboardMessage.initializeProject); }
|
||||
},
|
||||
{
|
||||
name: 'Framework presets',
|
||||
description: (
|
||||
<div>
|
||||
<div>Select your site-generator or framework to prefill some of the recommended settings.</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left mt-4">
|
||||
<div>
|
||||
<Menu.Button className="group flex justify-center text-vulcan-500 hover:text-vulcan-600 dark:text-whisper-500 dark:hover:text-whisper-600 p-2 rounded-md border border-vulcan-400 dark:border-white">
|
||||
{framework ? framework : 'Select your framework'}
|
||||
<ChevronDownIcon
|
||||
className="flex-shrink-0 -mr-1 ml-1 h-5 w-5 text-gray-400 group-hover:text-gray-500 dark:text-whisper-600 dark:group-hover:text-whisper-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Menu.Items className={`w-40 origin-top-left absolute left-0 z-10 mt-2 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
<MenuItem
|
||||
title={`other`}
|
||||
value={`other`}
|
||||
isCurrent={!framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
|
||||
<hr />
|
||||
|
||||
{frameworks.map((f) => (
|
||||
<MenuItem
|
||||
key={f.name}
|
||||
title={f.name}
|
||||
value={f.name}
|
||||
isCurrent={f.name === framework}
|
||||
onClick={(value) => setFrameworkAndSendMessage(value)} />
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
status: settings.crntFramework ? Status.Completed : Status.NotStarted,
|
||||
onClick: undefined
|
||||
},
|
||||
{
|
||||
name: 'Register content folders (manual action)',
|
||||
description: 'Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.',
|
||||
description: <>Register your content folder(s). You can perform this action by right-clicking on the folder in the explorer view, and selecting <b>register folder</b>. Once a folder is set, Front Matter can be used to list all contents and allow you to create content.</>,
|
||||
status: settings.folders && settings.folders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
name: 'Show the dashboard',
|
||||
description: 'Once both actions are completed, click on this action to load the dashboard.',
|
||||
description: <>Once both actions are completed, click on this action to load the dashboard.</>,
|
||||
status: (settings.initialized && settings.folders && settings.folders.length > 0) ? Status.Active : Status.NotStarted,
|
||||
onClick: (settings.initialized && settings.folders && settings.folders.length > 0) ? () => { Messenger.send(DashboardMessage.reload); } : undefined
|
||||
}
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (settings.crntFramework) {
|
||||
setFramework(settings.crntFramework);
|
||||
}
|
||||
}, [settings.crntFramework]);
|
||||
|
||||
return (
|
||||
<nav aria-label="Progress">
|
||||
<ol role="list" className="overflow-hidden">
|
||||
<ol role="list">
|
||||
{steps.map((step, stepIdx) => (
|
||||
<li key={step.name} className={`${stepIdx !== steps.length - 1 ? 'pb-10' : ''} relative`}>
|
||||
<Step name={step.name} description={step.description} status={step.status} showLine={stepIdx !== steps.length - 1} onClick={step.onClick} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HeartIcon, StarIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants/Links';
|
||||
import { GITHUB_LINK, REVIEW_LINK, SPONSOR_LINK } from '../../constants';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { FrontMatterIcon } from '../../panelWebView/components/Icons/FrontMatterIcon';
|
||||
import { GitHubIcon } from '../../panelWebView/components/Icons/GitHubIcon';
|
||||
@@ -33,11 +33,11 @@ export const WelcomeScreen: React.FunctionComponent<IWelcomeScreenProps> = ({set
|
||||
</h1>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
Thank you for taking the time to test out Front Matter!
|
||||
Thank you for using Front Matter!
|
||||
</p>
|
||||
|
||||
<p className="mt-3 text-base text-vulcan-300 dark:text-whisper-700 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions, please don't hesitate to reach out to us on GitHub.
|
||||
We try to aim to make Front Matter as easy to use as possible, but if you have any questions or suggestions. Please don't hesitate to reach out to us on GitHub.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 w-full sm:mx-auto sm:max-w-lg lg:ml-0">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export enum SortOption {
|
||||
LastModified = 1,
|
||||
FileNameAsc,
|
||||
FileNameDesc
|
||||
LastModified = "LastModified",
|
||||
FileNameAsc = "FileNameAsc",
|
||||
FileNameDesc = "FileNameDesc"
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import { SortOption } from '../constants/SortOption';
|
||||
import { Tab } from '../constants/Tab';
|
||||
import { Page } from '../models/Page';
|
||||
import Fuse from 'fuse.js';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SortingSelector, TabSelector, TagSelector } from '../state';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CategorySelector, FolderSelector, SearchSelector, SettingsSelector, SortingAtom, TabSelector, TagSelector } from '../state';
|
||||
import { SortOrder, SortType } from '../../models';
|
||||
import { DateHelper } from '../../helpers/DateHelper';
|
||||
|
||||
const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
keys: [
|
||||
@@ -16,14 +18,49 @@ const fuseOptions: Fuse.IFuseOptions<Page> = {
|
||||
|
||||
export default function usePages(pages: Page[]) {
|
||||
const [ pageItems, setPageItems ] = useState<Page[]>([]);
|
||||
const [ sorting, setSorting ] = useRecoilState(SortingAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const tab = useRecoilValue(TabSelector);
|
||||
const sorting = useRecoilValue(SortingSelector);
|
||||
const folder = useRecoilValue(FolderSelector);
|
||||
const search = useRecoilValue(SearchSelector);
|
||||
const tag = useRecoilValue(TagSelector);
|
||||
const category = useRecoilValue(CategorySelector);
|
||||
|
||||
// Sort field value alphabetically
|
||||
const sortAlphabetically = (property: string) => {
|
||||
return (a: Page, b: Page) => {
|
||||
if (a[property] < b[property]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[property] > b[property]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
};
|
||||
|
||||
// Sort by date
|
||||
const sortByDate = (property: string) => {
|
||||
return (a: Page, b: Page) => {
|
||||
const dateA = DateHelper.tryParse(a[property]);
|
||||
const dateB = DateHelper.tryParse(b[property]);
|
||||
|
||||
return (dateA || new Date(0)).getTime() - (dateB || new Date(0)).getTime();
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const draftField = settings?.draftField;
|
||||
let usedSorting = sorting;
|
||||
|
||||
if (!usedSorting) {
|
||||
const lastSort = settings?.dashboardState.sorting;
|
||||
if (lastSort) {
|
||||
setSorting(lastSort);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if search needs to be performed
|
||||
let searchedPages = pages;
|
||||
if (search) {
|
||||
@@ -34,23 +71,47 @@ export default function usePages(pages: Page[]) {
|
||||
|
||||
// Filter the pages
|
||||
let pagesToShow: Page[] = Object.assign([], searchedPages);
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page.draft);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page.draft);
|
||||
|
||||
if (draftField && draftField.type === 'choice') {
|
||||
if (tab !== Tab.All) {
|
||||
pagesToShow = pagesToShow.filter(page => page.fmDraft === tab);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
const draftFieldName = draftField?.name || "draft";
|
||||
if (tab === Tab.Published) {
|
||||
pagesToShow = searchedPages.filter(page => !page[draftFieldName]);
|
||||
} else if (tab === Tab.Draft) {
|
||||
pagesToShow = searchedPages.filter(page => !!page[draftFieldName]);
|
||||
} else {
|
||||
pagesToShow = searchedPages;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the pages
|
||||
let pagesSorted: Page[] = Object.assign([], pagesToShow);
|
||||
if (!search) {
|
||||
if (sorting === SortOption.FileNameAsc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => a.fmFileName.toLowerCase().localeCompare(b.fmFileName.toLowerCase()));
|
||||
} else if (sorting === SortOption.FileNameDesc) {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmFileName.toLowerCase().localeCompare(a.fmFileName.toLowerCase()));
|
||||
if (sorting && sorting.id === SortOption.FileNameAsc) {
|
||||
pagesSorted = pagesSorted.sort(sortAlphabetically("fmFileName"));
|
||||
} else if (sorting && sorting.id === SortOption.FileNameDesc) {
|
||||
pagesSorted = pagesSorted.sort(sortAlphabetically("fmFileName")).reverse();
|
||||
} else if (sorting && sorting.id === SortOption.LastModified) {
|
||||
pagesSorted = pagesSorted.sort((a, b) => b.fmModified - a.fmModified);
|
||||
} else if (sorting && sorting.id && sorting.name) {
|
||||
const { order, name, type } = sorting;
|
||||
|
||||
if (type === SortType.string) {
|
||||
pagesSorted = pagesSorted.sort(sortAlphabetically(name));
|
||||
} else if (type === SortType.date) {
|
||||
pagesSorted = pagesSorted.sort(sortByDate(name));
|
||||
}
|
||||
|
||||
if (order === SortOrder.desc) {
|
||||
pagesSorted = pagesSorted.reverse();
|
||||
}
|
||||
} else {
|
||||
pagesSorted = pagesToShow.sort((a, b) => b.fmModified - a.fmModified);
|
||||
pagesSorted = pagesSorted.sort((a, b) => b.fmModified - a.fmModified);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +130,7 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
|
||||
setPageItems(pagesSorted);
|
||||
}, [ pages, tab, sorting, folder, search, tag, category ]);
|
||||
}, [ settings?.draftField, pages, tab, sorting, folder, search, tag, category ]);
|
||||
|
||||
return {
|
||||
pageItems
|
||||
|
||||
@@ -2,15 +2,27 @@ import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { Dashboard } from "./components/Dashboard";
|
||||
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { SENTRY_LINK } from "../constants";
|
||||
import './styles.css';
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const welcome = elm?.getAttribute("data-showWelcome");
|
||||
const version = elm?.getAttribute("data-version");
|
||||
const environment = elm?.getAttribute("data-environment");
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || "",
|
||||
environment: environment || ""
|
||||
});
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
setState: (data: T) => void;
|
||||
postMessage: (msg: unknown) => void;
|
||||
};
|
||||
|
||||
const elm = document.querySelector("#app");
|
||||
const welcome = elm?.getAttribute("data-showWelcome");
|
||||
render(<RecoilRoot><Dashboard showWelcome={!!welcome} /></RecoilRoot>, elm);
|
||||
@@ -6,7 +6,7 @@ export interface Page {
|
||||
fmFileName: string;
|
||||
fmModified: number;
|
||||
fmDraft: "Draft" | "Published",
|
||||
fmYear: number | null;
|
||||
fmYear: number | null | undefined;
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ViewType } from '../state';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType, DraftField, Framework, SortingSetting } from '../../models';
|
||||
import { SortingOption } from './SortingOption';
|
||||
|
||||
export interface Settings {
|
||||
beta: boolean;
|
||||
@@ -14,4 +16,15 @@ export interface Settings {
|
||||
versionInfo: VersionInfo;
|
||||
pageViewType: ViewType | undefined;
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: ContentFolder[];
|
||||
crntFramework: string;
|
||||
framework: Framework | null | undefined;
|
||||
draftField: DraftField | null | undefined;
|
||||
customSorting: SortingSetting[] | undefined;
|
||||
dashboardState: DashboardState;
|
||||
}
|
||||
|
||||
export interface DashboardState {
|
||||
sorting: SortingOption | null | undefined;
|
||||
}
|
||||
10
src/dashboardWebView/models/SortingOption.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SortOrder, SortType } from "../../models";
|
||||
import { SortOption } from "../constants/SortOption";
|
||||
|
||||
export interface SortingOption {
|
||||
title?: string;
|
||||
name: string;
|
||||
id: SortOption | string;
|
||||
order: SortOrder,
|
||||
type: SortType;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { atom } from 'recoil';
|
||||
import { SortOption } from '../../constants/SortOption';
|
||||
import { SortingOption } from '../../models/SortingOption';
|
||||
|
||||
export const DEFAULT_SORTING_OPTION = SortOption.LastModified;
|
||||
|
||||
export const SortingAtom = atom<SortOption>({
|
||||
export const SortingAtom = atom<SortingOption | null>({
|
||||
key: 'SortingAtom',
|
||||
default: DEFAULT_SORTING_OPTION
|
||||
default: null
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { atom } from 'recoil';
|
||||
import { Tab } from '../../constants/Tab';
|
||||
|
||||
export const TabAtom = atom<Tab>({
|
||||
export const TabAtom = atom<Tab | string>({
|
||||
key: 'TabAtom',
|
||||
default: Tab.All
|
||||
});
|
||||
9
src/dashboardWebView/state/selectors/PageSelector.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { selector } from 'recoil';
|
||||
import { PageAtom } from '..';
|
||||
|
||||
export const PageSelector = selector({
|
||||
key: 'PageSelector',
|
||||
get: ({get}) => {
|
||||
return get(PageAtom);
|
||||
}
|
||||
});
|
||||
@@ -5,6 +5,7 @@ export * from './GroupingSelector';
|
||||
export * from './LoadingSelector';
|
||||
export * from './MediaFoldersSelector';
|
||||
export * from './MediaTotalSelector';
|
||||
export * from './PageSelector';
|
||||
export * from './SearchSelector';
|
||||
export * from './SelectedMediaFolderSelector';
|
||||
export * from './SettingsSelector';
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { DashboardData } from '../models/DashboardData';
|
||||
import { Template } from '../commands/Template';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS } from '../constants';
|
||||
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL } from '../constants';
|
||||
import * as os from 'os';
|
||||
import { PanelSettings, CustomScript } from '../models/PanelSettings';
|
||||
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv } from "vscode";
|
||||
import { ArticleHelper, Settings } from "../helpers";
|
||||
import { Command } from "../panelWebView/Command";
|
||||
import { CommandToCode } from '../panelWebView/CommandToCode';
|
||||
import { Article } from '../commands';
|
||||
import { TagType } from '../panelWebView/TagType';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { DraftField, TaxonomyType } from '../models';
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown';
|
||||
import { Content } from 'mdast';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
import { COMMAND_NAME } from '../constants/Extension';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Preview } from '../commands/Preview';
|
||||
import { openFileInEditor } from '../helpers/openFileInEditor';
|
||||
import { WebviewHelper } from '@estruyf/vscode';
|
||||
import { Extension } from '../helpers/Extension';
|
||||
import { dirname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { Dashboard } from '../commands/Dashboard';
|
||||
import { ImageHelper } from '../helpers/ImageHelper';
|
||||
import { CustomScript } from '../helpers/CustomScript';
|
||||
import { Link, Parent } from 'mdast-util-from-markdown/lib';
|
||||
|
||||
const FILE_LIMIT = 10;
|
||||
|
||||
@@ -217,7 +216,6 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const filePath = window.activeTextEditor?.document.uri.fsPath;
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
|
||||
const contentTypes = Settings.get<string>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
|
||||
const articleDetails = this.getArticleDetails();
|
||||
@@ -245,28 +243,42 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
const contentType = ArticleHelper.getContentType(updatedMetadata);
|
||||
if (contentType) {
|
||||
const imageFields = contentType.fields.filter((field) => field.type === "image");
|
||||
|
||||
for (const field of imageFields) {
|
||||
if (updatedMetadata[field.name]) {
|
||||
const staticPath = join(wsFolder.fsPath, staticFolder || "", updatedMetadata[field.name]);
|
||||
const contentFolderPath = filePath ? join(dirname(filePath), updatedMetadata[field.name]) : null;
|
||||
const imageData = ImageHelper.allRelToAbs(field, updatedMetadata[field.name])
|
||||
|
||||
let previewUri = null;
|
||||
if (existsSync(staticPath)) {
|
||||
previewUri = Uri.file(staticPath);
|
||||
} else if (contentFolderPath && existsSync(contentFolderPath)) {
|
||||
previewUri = Uri.file(contentFolderPath);
|
||||
}
|
||||
if (imageData) {
|
||||
if (field.multiple && imageData instanceof Array) {
|
||||
const preview = imageData.map(preview => preview && preview.absPath ? ({
|
||||
...preview,
|
||||
webviewUrl: this.panel?.webview.asWebviewUri(preview.absPath).toString()
|
||||
}) : null);
|
||||
|
||||
if (previewUri) {
|
||||
const preview = this.panel?.webview.asWebviewUri(previewUri);
|
||||
updatedMetadata[field.name]= preview?.toString() || "";
|
||||
updatedMetadata[field.name] = preview || [];
|
||||
} else if (!field.multiple && !Array.isArray(imageData) && imageData.absPath) {
|
||||
const preview = this.panel?.webview.asWebviewUri(imageData.absPath);
|
||||
updatedMetadata[field.name] = {
|
||||
...imageData,
|
||||
webviewUrl: preview ? preview.toString() : null
|
||||
};
|
||||
}
|
||||
} else {
|
||||
updatedMetadata[field.name] = "";
|
||||
updatedMetadata[field.name] = field.multiple ? [] : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check slug
|
||||
if (!updatedMetadata[DefaultFields.Slug]) {
|
||||
const slug = Article.getSlug();
|
||||
|
||||
if (slug) {
|
||||
updatedMetadata[DefaultFields.Slug] = slug;
|
||||
}
|
||||
}
|
||||
|
||||
this.postWebviewMessage({ command: Command.metadata, data: {
|
||||
...updatedMetadata
|
||||
@@ -295,7 +307,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
/**
|
||||
* Update the metadata of the article
|
||||
*/
|
||||
public async updateMetadata({field, value}: { field: string, value: string }) {
|
||||
public async updateMetadata({field, value }: { field: string, value: any, fieldData?: { multiple: boolean, value: string[] } }) {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
@@ -312,14 +324,33 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
const dateFields = contentType.fields.filter((field) => field.type === "datetime");
|
||||
const imageFields = contentType.fields.filter((field) => field.type === "image" && field.multiple);
|
||||
|
||||
for (const dateField of dateFields) {
|
||||
if ((field === dateField.name) && value) {
|
||||
article.data[field] = Article.formatDate(new Date(value));
|
||||
} else {
|
||||
} else if (!imageFields.find(f => f.name === field)) {
|
||||
// Only override the field data if it is not an multiselect image field
|
||||
article.data[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
for (const imageField of imageFields) {
|
||||
if (field === imageField.name) {
|
||||
// If value is an array, it means it comes from the explorer view itself (deletion)
|
||||
if (Array.isArray(value)) {
|
||||
article.data[field] = value || [];
|
||||
} else { // Otherwise it is coming from the media dashboard (addition)
|
||||
let fieldValue = article.data[field];
|
||||
if (fieldValue && !Array.isArray(fieldValue)) {
|
||||
fieldValue = [fieldValue];
|
||||
}
|
||||
const crntData = Object.assign([], fieldValue);
|
||||
const allRelPaths = [...(crntData || []), value];
|
||||
article.data[field] = [...new Set(allRelPaths)].filter(f => f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArticleHelper.update(editor, article);
|
||||
this.pushMetadata(article.data);
|
||||
@@ -330,38 +361,12 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* @param msg
|
||||
*/
|
||||
private runCustomScript(msg: { command: string, data: any}) {
|
||||
const scripts: CustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
const scripts: ICustomScript[] | undefined = Settings.get(SETTING_CUSTOM_SCRIPTS);
|
||||
|
||||
if (msg?.data?.title && msg?.data?.script && scripts) {
|
||||
const customScript = scripts.find((s: CustomScript) => s.title === msg.data.title);
|
||||
const customScript = scripts.find((s: ICustomScript) => s.title === msg.data.title);
|
||||
if (customScript?.script && customScript?.title) {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
let articleData = `'${JSON.stringify(article?.data)}'`;
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
}
|
||||
|
||||
exec(`${customScript.nodeBin || "node"} ${path.join(wsPath, msg.data.script)} "${wsPath}" "${editor?.document.uri.fsPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${msg?.data?.title}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.showInformationMessage(`${msg?.data?.title}: ${stdout || "Executed your custom script."}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
CustomScript.run(customScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,6 +390,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
data: {
|
||||
seo: {
|
||||
title: Settings.get(SETTING_SEO_TITLE_LENGTH) as number || -1,
|
||||
slug: Settings.get(SETTING_SEO_SLUG_LENGTH) as number || -1,
|
||||
description: Settings.get(SETTING_SEO_DESCRIPTION_LENGTH) as number || -1,
|
||||
content: Settings.get(SETTING_SEO_CONTENT_MIN_LENGTH) as number || -1,
|
||||
descriptionField: Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description
|
||||
@@ -408,7 +414,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
preview: Preview.getSettings(),
|
||||
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
dashboardViewData: Dashboard.viewData
|
||||
dashboardViewData: Dashboard.viewData,
|
||||
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD)
|
||||
} as PanelSettings
|
||||
});
|
||||
}
|
||||
@@ -480,6 +487,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
* Get article details
|
||||
*/
|
||||
private getArticleDetails() {
|
||||
const baseUrl = Settings.get<string>(SETTING_SITE_BASEURL);
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return null;
|
||||
@@ -496,13 +504,36 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
content = content.replace(/({{(.*?)}})/g, ''); // remove hugo shortcodes
|
||||
|
||||
const mdTree = fromMarkdown(content);
|
||||
const headings = mdTree.children.filter(node => node.type === 'heading').length;
|
||||
const paragraphs = mdTree.children.filter(node => node.type === 'paragraph').length;
|
||||
const elms: Parent[] | Link[] = this.getAllElms(mdTree);
|
||||
|
||||
const headings = elms.filter(node => node.type === 'heading');
|
||||
const paragraphs = elms.filter(node => node.type === 'paragraph').length;
|
||||
const images = elms.filter(node => node.type === 'image').length;
|
||||
const links: string[] = elms.filter(node => node.type === 'link').map(node => (node as Link).url);
|
||||
|
||||
const internalLinks = links.filter(link => !link.startsWith('http') || (baseUrl && link.toLowerCase().includes((baseUrl || "").toLowerCase()))).length;
|
||||
let externalLinks = links.filter(link => link.startsWith('http'));
|
||||
if (baseUrl) {
|
||||
externalLinks = externalLinks.filter(link => !link.toLowerCase().includes(baseUrl.toLowerCase()));
|
||||
}
|
||||
|
||||
const headers = [];
|
||||
for (const header of headings) {
|
||||
const text = header?.children?.filter((node: any) => node.type === 'text').map((node: any) => node.value).join(" ");
|
||||
if (text) {
|
||||
headers.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
const wordCount = this.wordCount(0, mdTree);
|
||||
|
||||
return {
|
||||
headings,
|
||||
headings: headings.length,
|
||||
headingsText: headers,
|
||||
paragraphs,
|
||||
images,
|
||||
internalLinks,
|
||||
externalLinks: externalLinks.length,
|
||||
wordCount,
|
||||
content: article.content
|
||||
};
|
||||
@@ -511,6 +542,21 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getAllElms(node: Content | any, allElms?: any[]): any[] {
|
||||
if (!allElms) {
|
||||
allElms = [];
|
||||
}
|
||||
|
||||
if (node.children?.length > 0) {
|
||||
for (const child of node.children) {
|
||||
allElms.push(Object.assign({}, child));
|
||||
this.getAllElms(child, allElms);
|
||||
}
|
||||
}
|
||||
|
||||
return allElms;
|
||||
}
|
||||
|
||||
private counts(acc: any, node: any) {
|
||||
// add 1 to an initial or existing value
|
||||
acc[node.type] = (acc[node.type] || 0) + 1;
|
||||
@@ -611,13 +657,15 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
|
||||
const nonce = WebviewHelper.getNonce();
|
||||
|
||||
const version = Extension.getInstance().getVersion();
|
||||
const ext = Extension.getInstance();
|
||||
const version = ext.getVersion();
|
||||
const isBeta = ext.isBetaVersion();
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'; script-src 'nonce-${nonce}'; style-src ${webView.cspSource} 'self' 'unsafe-inline'; font-src ${webView.cspSource}; connect-src https://o1022172.ingest.sentry.io">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="${styleResetUri}" rel="stylesheet">
|
||||
<link href="${styleVSCodeUri}" rel="stylesheet">
|
||||
@@ -626,7 +674,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
|
||||
<title>Front Matter</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app" data-environment="${isBeta ? "BETA" : "main"}" data-version="${version.usedVersion}" ></div>
|
||||
|
||||
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContentType } from './helpers/ContentType';
|
||||
import { Dashboard } from './commands/Dashboard';
|
||||
import * as vscode from 'vscode';
|
||||
import { Article, Settings, StatusListener } from './commands';
|
||||
@@ -5,7 +6,7 @@ import { Folders } from './commands/Folders';
|
||||
import { Preview } from './commands/Preview';
|
||||
import { Project } from './commands/Project';
|
||||
import { Template } from './commands/Template';
|
||||
import { COMMAND_NAME } from './constants/Extension';
|
||||
import { COMMAND_NAME } from './constants';
|
||||
import { TaxonomyType } from './models';
|
||||
import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider';
|
||||
import { TagType } from './panelWebView/TagType';
|
||||
@@ -13,6 +14,8 @@ import { ExplorerView } from './explorerView/ExplorerView';
|
||||
import { Extension } from './helpers/Extension';
|
||||
import { DashboardData } from './models/DashboardData';
|
||||
import { Settings as SettingsHelper } from './helpers';
|
||||
import { Content } from './commands/Content';
|
||||
import ContentProvider from './providers/ContentProvider';
|
||||
|
||||
let frontMatterStatusBar: vscode.StatusBarItem;
|
||||
let statusDebouncer: { (fnc: any, time: number): void; };
|
||||
@@ -32,6 +35,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
SettingsHelper.init();
|
||||
extension.migrateSettings();
|
||||
|
||||
SettingsHelper.checkToPromote();
|
||||
|
||||
collection = vscode.languages.createDiagnosticCollection('frontMatter');
|
||||
|
||||
@@ -104,7 +109,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const unregisterFolder = vscode.commands.registerCommand(COMMAND_NAME.unregisterFolder, Folders.unregister);
|
||||
|
||||
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Folders.create);
|
||||
const createFolder = vscode.commands.registerCommand(COMMAND_NAME.createFolder, Folders.addMediaFolder);
|
||||
|
||||
const createByContentType = vscode.commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent);
|
||||
const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create);
|
||||
const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create);
|
||||
|
||||
// Initialize command
|
||||
Template.init();
|
||||
@@ -117,7 +126,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
});
|
||||
|
||||
// Settings promotion command
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, () => { console.log('promote'); SettingsHelper.promote(); }));
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.promote, SettingsHelper.promote ));
|
||||
|
||||
// Collapse all sections in the webview
|
||||
const collapseAll = vscode.commands.registerCommand(COMMAND_NAME.collapseSections, () => {
|
||||
@@ -167,6 +176,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
// Inserting an image in Markdown
|
||||
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
|
||||
|
||||
// Create the editor experience for bulk scripts
|
||||
subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider()));
|
||||
|
||||
// Subscribe all commands
|
||||
subscriptions.push(
|
||||
insertTags,
|
||||
@@ -184,8 +196,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
registerFolder,
|
||||
unregisterFolder,
|
||||
createContent,
|
||||
createByContentType,
|
||||
createByTemplate,
|
||||
projectInit,
|
||||
collapseAll
|
||||
collapseAll,
|
||||
createFolder
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
|
||||
import { ContentType } from './../models/PanelSettings';
|
||||
import * as vscode from 'vscode';
|
||||
import * as matter from "gray-matter";
|
||||
import * as fs from "fs";
|
||||
import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES } from '../constants';
|
||||
import { DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { TomlEngine, getFmLanguage, getFormatOpts } from './TomlEngine';
|
||||
import { Settings } from '.';
|
||||
import { parse } from 'date-fns';
|
||||
import { format, parse } from 'date-fns';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Article } from '../commands';
|
||||
import { basename, join } from 'path';
|
||||
import { EditorHelper } from '@estruyf/vscode';
|
||||
import sanitize from '../helpers/Sanitize';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { ContentType } from '../models';
|
||||
import { DateHelper } from './DateHelper';
|
||||
|
||||
export class ArticleHelper {
|
||||
|
||||
@@ -20,16 +25,16 @@ export class ArticleHelper {
|
||||
*/
|
||||
public static getFrontMatter(editor: vscode.TextEditor) {
|
||||
const fileContents = editor.document.getText();
|
||||
return ArticleHelper.parseFile(fileContents);
|
||||
return ArticleHelper.parseFile(fileContents, editor.document.fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the file's front matter by its path
|
||||
* @param filePath
|
||||
*/
|
||||
public static getFrontMatterByPath(filePath: string) {
|
||||
public static getFrontMatterByPath(filePath: string, surpressNotification: boolean = false) {
|
||||
const file = fs.readFileSync(filePath, { encoding: "utf-8" });
|
||||
return ArticleHelper.parseFile(file);
|
||||
return ArticleHelper.parseFile(file, filePath, surpressNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,11 +91,13 @@ export class ArticleHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return matter.stringify(content, data, ({
|
||||
...TomlEngine,
|
||||
...langOpts,
|
||||
noArrayIndent: !indentArray,
|
||||
skipInvalid: true,
|
||||
noCompatMode: true,
|
||||
lineWidth: 500,
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any));
|
||||
@@ -162,12 +169,63 @@ export class ArticleHelper {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the value
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
public static sanitize(value: string): string {
|
||||
return sanitize(value.toLowerCase().replace(/ /g, "-"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the file or folder for the new content
|
||||
* @param contentType
|
||||
* @param folderPath
|
||||
* @param titleValue
|
||||
* @returns The new file path
|
||||
*/
|
||||
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined {
|
||||
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
|
||||
|
||||
// Name of the file or folder to create
|
||||
const sanitizedName = ArticleHelper.sanitize(titleValue);
|
||||
let newFilePath: string | undefined;
|
||||
|
||||
// Create a folder with the `index.md` file
|
||||
if (contentType?.pageBundle) {
|
||||
const newFolder = join(folderPath, sanitizedName);
|
||||
if (existsSync(newFolder)) {
|
||||
Notifications.error(`A page bundle with the name ${sanitizedName} already exists in ${folderPath}`);
|
||||
return;
|
||||
} else {
|
||||
mkdirSync(newFolder);
|
||||
newFilePath = join(newFolder, `index.md`);
|
||||
}
|
||||
} else {
|
||||
let newFileName = `${sanitizedName}.md`;
|
||||
|
||||
if (prefix && typeof prefix === "string") {
|
||||
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;
|
||||
}
|
||||
|
||||
newFilePath = join(folderPath, newFileName);
|
||||
|
||||
if (existsSync(newFilePath)) {
|
||||
Notifications.warning(`Content with the title already exists. Please specify a new title.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return newFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a markdown file and its front matter
|
||||
* @param fileContents
|
||||
* @returns
|
||||
*/
|
||||
private static parseFile(fileContents: string): matter.GrayMatterFile<string> | null {
|
||||
private static parseFile(fileContents: string, fileName: string, surpressNotification: boolean = false): matter.GrayMatterFile<string> | null {
|
||||
try {
|
||||
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
|
||||
|
||||
@@ -192,7 +250,23 @@ export class ArticleHelper {
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
Notifications.error(`There seems to be an issue parsing your Front Matter. ERROR: ${error.message || error}`);
|
||||
const items = [{
|
||||
title: "Check file",
|
||||
action: async () => {
|
||||
await EditorHelper.showFile(fileName)
|
||||
}
|
||||
}];
|
||||
|
||||
if (!surpressNotification) {
|
||||
Notifications.error(`There seems to be an issue parsing the content its front matter. FileName: ${basename(fileName)}. ERROR: ${error.message || error}`, ...items).then((result: any) => {
|
||||
if (result?.title) {
|
||||
const item = items.find(i => i.title === result.title);
|
||||
if (item) {
|
||||
item.action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
132
src/helpers/ContentType.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { ArticleHelper, Settings } from ".";
|
||||
import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { ContentType as IContentType, DraftField } from '../models';
|
||||
import { Uri, workspace, window } from 'vscode';
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Questions } from "./Questions";
|
||||
import { writeFileSync } from "fs";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
|
||||
|
||||
export class ContentType {
|
||||
|
||||
/**
|
||||
* Retrieve the draft field
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftField() {
|
||||
const draftField = Settings.get<DraftField | null | undefined>(SETTINGS_CONTENT_DRAFT_FIELD);
|
||||
if (draftField) {
|
||||
return draftField;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the field its status
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static getDraftStatus(data: { [field: string]: any }) {
|
||||
const contentType = ArticleHelper.getContentType(data);
|
||||
const draftSetting = ContentType.getDraftField();
|
||||
|
||||
const draftField = contentType.fields.find(f => f.type === "draft");
|
||||
|
||||
let fieldValue = null;
|
||||
|
||||
if (draftField) {
|
||||
fieldValue = data[draftField.name];
|
||||
} else if (draftSetting && data && data[draftSetting.name]) {
|
||||
fieldValue = data[draftSetting.name];
|
||||
}
|
||||
|
||||
if (draftSetting && fieldValue) {
|
||||
if (draftSetting.type === "boolean") {
|
||||
return fieldValue ? "Draft" : "Published";
|
||||
} else {
|
||||
return fieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content based on content types
|
||||
* @returns
|
||||
*/
|
||||
public static async createContent() {
|
||||
const selectedContentType = await Questions.SelectContentType();
|
||||
if (!selectedContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFolder = await Questions.SelectContentFolder();
|
||||
if (!selectedFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
const folders = Folders.get();
|
||||
|
||||
const location = folders.find(f => f.title === selectedFolder);
|
||||
if (contentTypes && location) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(location.path));
|
||||
const contentType = contentTypes.find(ct => ct.name === selectedContentType);
|
||||
if (folderPath && contentType) {
|
||||
ContentType.create(contentType, folderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all content types
|
||||
* @returns
|
||||
*/
|
||||
public static getAll() {
|
||||
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
}
|
||||
|
||||
private static async create(contentType: IContentType, folderPath: string) {
|
||||
|
||||
const titleValue = await Questions.ContentTitle();
|
||||
if (!titleValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
|
||||
if (!newFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any = {};
|
||||
|
||||
for (const field of contentType.fields) {
|
||||
if (field.name === "title") {
|
||||
data[field.name] = titleValue;
|
||||
} else {
|
||||
data[field.name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
data = ArticleHelper.updateDates(Object.assign({}, data));
|
||||
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
|
||||
const content = ArticleHelper.stringifyFrontMatter(``, data);
|
||||
|
||||
writeFileSync(newFilePath, content, { encoding: "utf8" });
|
||||
|
||||
const txtDoc = await workspace.openTextDocument(Uri.parse(newFilePath));
|
||||
if (txtDoc) {
|
||||
window.showTextDocument(txtDoc);
|
||||
}
|
||||
|
||||
Notifications.info(`Your new content has been created.`);
|
||||
}
|
||||
}
|
||||
119
src/helpers/CustomScript.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { CustomScript as ICustomScript } from '../models/PanelSettings';
|
||||
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
|
||||
import { ArticleHelper } from '.';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { exec } from 'child_process';
|
||||
import matter = require('gray-matter');
|
||||
import * as os from 'os';
|
||||
import { join } from 'path';
|
||||
import { Notifications } from './Notifications';
|
||||
import ContentProvider from '../providers/ContentProvider';
|
||||
|
||||
export class CustomScript {
|
||||
|
||||
public static async run(script: ICustomScript): Promise<void> {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
|
||||
if (wsFolder) {
|
||||
const wsPath = wsFolder.fsPath;
|
||||
|
||||
if (script.bulk) {
|
||||
// Run script on all files
|
||||
CustomScript.bulkRun(wsPath, script);
|
||||
} else {
|
||||
// Run script on current file.
|
||||
CustomScript.singleRun(wsPath, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async singleRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
|
||||
if (article) {
|
||||
const output = await CustomScript.runScript(wsPath, article, editor.document.uri.fsPath, script);
|
||||
|
||||
CustomScript.showOutput(output, script);
|
||||
} else {
|
||||
Notifications.warning(`${script.title}: Current article couldn't be retrieved.`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async bulkRun(wsPath: string, script: ICustomScript): Promise<void> {
|
||||
const folders = await Folders.getInfo();
|
||||
|
||||
if (!folders || folders.length === 0) {
|
||||
Notifications.warning(`${script.title}: No files found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let output: string[] = [];
|
||||
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: `Executing: ${script.title}`,
|
||||
cancellable: false
|
||||
}, async (progress, token) => {
|
||||
for await (const folder of folders) {
|
||||
if (folder.lastModified.length > 0) {
|
||||
for await (const file of folder.lastModified) {
|
||||
try {
|
||||
const article = ArticleHelper.getFrontMatterByPath(file.filePath, true);
|
||||
if (article) {
|
||||
const crntOutput = await CustomScript.runScript(wsPath, article, file.filePath, script);
|
||||
if (crntOutput) {
|
||||
output.push(crntOutput);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skipping file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomScript.showOutput(output.join(`\n`), script);
|
||||
});
|
||||
}
|
||||
|
||||
private static async runScript(wsPath: string, article: matter.GrayMatterFile<string> | null, contentPath: string, script: ICustomScript): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let articleData = "";
|
||||
if (os.type() === "Windows_NT") {
|
||||
articleData = `"${JSON.stringify(article?.data).replace(/"/g, `""`)}"`;
|
||||
} else {
|
||||
articleData = JSON.stringify(article?.data).replace(/'/g, "%27");
|
||||
articleData = `'${articleData}'`;
|
||||
}
|
||||
|
||||
exec(`${script.nodeBin || "node"} ${join(wsPath, script.script)} "${wsPath}" "${contentPath}" ${articleData}`, (error, stdout) => {
|
||||
if (error) {
|
||||
Notifications.error(`${script.title}: ${error.message}`);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static showOutput(output: string | null, script: ICustomScript): void {
|
||||
if (output) {
|
||||
if (script.output === "editor") {
|
||||
ContentProvider.show(output, script.title, script.outputType || "text");
|
||||
} else {
|
||||
window.showInformationMessage(`${script.title}: ${output}}`, 'Copy output').then(value => {
|
||||
if (value === 'Copy output') {
|
||||
vscodeEnv.clipboard.writeText(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Notifications.info(`${script.title}: Executed your custom script.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/helpers/DateHelper.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { parse, parseISO, parseJSON } from "date-fns";
|
||||
|
||||
|
||||
export class DateHelper {
|
||||
|
||||
public static formatUpdate(value: string | null | undefined): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.replace(/YYYY/g, 'yyyy');
|
||||
value = value.replace(/DD/g, 'dd');
|
||||
return value;
|
||||
}
|
||||
|
||||
public static tryParse(date: any, format?: string): Date | null {
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (date instanceof Date) {
|
||||
return date;
|
||||
}
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const jsonParsed = DateHelper.tryParseJson(date);
|
||||
if (DateHelper.isValid(jsonParsed)) {
|
||||
return jsonParsed;
|
||||
}
|
||||
|
||||
const isoParsed = DateHelper.tryParseIso(date);
|
||||
if (DateHelper.isValid(isoParsed)) {
|
||||
return isoParsed;
|
||||
}
|
||||
|
||||
if (format) {
|
||||
const formatParsed = DateHelper.tryFormatParse(date, format);
|
||||
if (DateHelper.isValid(formatParsed)) {
|
||||
return formatParsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static isValid(date: any): boolean {
|
||||
return date instanceof Date && !isNaN(date?.getTime());
|
||||
}
|
||||
|
||||
public static tryFormatParse(date: string, format: string): Date | null {
|
||||
try {
|
||||
return parse(date, format, new Date());
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static tryParseJson(date: string): Date | null {
|
||||
try {
|
||||
return parseJSON(date);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static tryParseIso(date: string): Date | null {
|
||||
try {
|
||||
return parseISO(date);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { basename } from "path";
|
||||
import { existsSync, renameSync } from "fs";
|
||||
import { basename, join } from "path";
|
||||
import { extensions, Uri, ExtensionContext, window, workspace, commands } from "vscode";
|
||||
import { Folders, WORKSPACE_PLACEHOLDER } from "../commands/Folders";
|
||||
import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
|
||||
import { EXTENSION_BETA_ID, EXTENSION_ID, EXTENSION_STATE_VERSION } from "../constants/Extension";
|
||||
import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants";
|
||||
import { ContentType } from "../models";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { parseWinPath } from "./parseWinPath";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class Extension {
|
||||
public getVersion(): { usedVersion: string | undefined, installedVersion: string } {
|
||||
const frontMatter = extensions.getExtension(this.isBetaVersion() ? EXTENSION_BETA_ID : EXTENSION_ID)!;
|
||||
let installedVersion = frontMatter.packageJSON.version;
|
||||
const usedVersion = this.ctx.globalState.get<string>(EXTENSION_STATE_VERSION);
|
||||
const usedVersion = this.ctx.globalState.get<string>(ExtensionState.Version);
|
||||
|
||||
if (this.isBetaVersion()) {
|
||||
installedVersion = `${installedVersion}-beta`;
|
||||
@@ -78,7 +78,7 @@ export class Extension {
|
||||
* Set the current version information for the extension
|
||||
*/
|
||||
public setVersion(installedVersion: string): void {
|
||||
this.ctx.globalState.update(EXTENSION_STATE_VERSION, installedVersion);
|
||||
this.ctx.globalState.update(ExtensionState.Version, installedVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,18 +92,35 @@ export class Extension {
|
||||
* Migrate old settings to new settings
|
||||
*/
|
||||
public async migrateSettings(): Promise<void> {
|
||||
const versionInfo = this.getVersion();
|
||||
if (versionInfo.usedVersion === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!versionInfo.usedVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split semantic version
|
||||
const version = versionInfo.usedVersion.split('.');
|
||||
const major = parseInt(version[0]);
|
||||
const minor = parseInt(version[1]);
|
||||
const patch = parseInt(version[2]);
|
||||
|
||||
// Migration to version 3.1.0
|
||||
const folders = Settings.get<any>(SETTINGS_CONTENT_FOLDERS);
|
||||
if (folders && folders.length > 0) {
|
||||
const workspace = Folders.getWorkspaceFolder();
|
||||
const projectFolder = basename(workspace?.fsPath || "");
|
||||
if (major < 3 || (major === 3 && minor < 1)) {
|
||||
const folders = Settings.get<any>(SETTINGS_CONTENT_FOLDERS);
|
||||
if (folders && folders.length > 0) {
|
||||
const workspace = Folders.getWorkspaceFolder();
|
||||
const projectFolder = basename(workspace?.fsPath || "");
|
||||
|
||||
const paths = folders.map((folder: any) => ({
|
||||
title: folder.title,
|
||||
path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/')
|
||||
}));
|
||||
const paths = folders.map((folder: any) => ({
|
||||
...folder,
|
||||
path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/')
|
||||
}));
|
||||
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths);
|
||||
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths);
|
||||
}
|
||||
}
|
||||
|
||||
// Create team settings
|
||||
@@ -112,56 +129,110 @@ export class Extension {
|
||||
}
|
||||
|
||||
// Migration to version 4.0.0
|
||||
const dateField = Settings.get<string>(SETTING_DATE_FIELD);
|
||||
const lastModField = Settings.get<string>(SETTING_MODIFIED_FIELD);
|
||||
const description = Settings.get<string>(SETTING_SEO_DESCRIPTION_FIELD);
|
||||
const contentTypes = Settings.get<ContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
if (major < 4) {
|
||||
const dateField = Settings.get<string>(SETTING_DATE_FIELD);
|
||||
const lastModField = Settings.get<string>(SETTING_MODIFIED_FIELD);
|
||||
const description = Settings.get<string>(SETTING_SEO_DESCRIPTION_FIELD);
|
||||
const contentTypes = Settings.get<ContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
|
||||
|
||||
if (contentTypes) {
|
||||
let needsUpdate = false;
|
||||
let defaultContentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
|
||||
if (contentTypes) {
|
||||
let needsUpdate = false;
|
||||
let defaultContentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
|
||||
|
||||
if (defaultContentType) {
|
||||
if (dateField && dateField !== "date") {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "date");
|
||||
defaultContentType.fields.push({
|
||||
name: dateField,
|
||||
type: "datetime"
|
||||
});
|
||||
needsUpdate = true;
|
||||
// Check if fields need to be changed for the default content type
|
||||
if (defaultContentType) {
|
||||
if (dateField && dateField !== DefaultFields.PublishingDate) {
|
||||
const newDateField = defaultContentType.fields.find(f => f.name === dateField);
|
||||
|
||||
if (!newDateField) {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.PublishingDate);
|
||||
defaultContentType.fields.push({
|
||||
title: dateField,
|
||||
name: dateField,
|
||||
type: "datetime"
|
||||
});
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModField && lastModField !== DefaultFields.LastModified) {
|
||||
const newModField = defaultContentType.fields.find(f => f.name === lastModField);
|
||||
|
||||
if (!newModField) {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.LastModified);
|
||||
defaultContentType.fields.push({
|
||||
title: lastModField,
|
||||
name: lastModField,
|
||||
type: "datetime"
|
||||
});
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (description && description !== DefaultFields.Description) {
|
||||
const newDescField = defaultContentType.fields.find(f => f.name === description);
|
||||
|
||||
if (!newDescField) {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.Description);
|
||||
defaultContentType.fields.push({
|
||||
title: description,
|
||||
name: description,
|
||||
type: "string"
|
||||
});
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModField && lastModField !== "lastmod") {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "lastmod");
|
||||
defaultContentType.fields.push({
|
||||
name: lastModField,
|
||||
type: "datetime"
|
||||
});
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (description && description !== "description") {
|
||||
defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== "lastmod");
|
||||
defaultContentType.fields.push({
|
||||
name: description,
|
||||
type: "string"
|
||||
});
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
// Migration to version 5
|
||||
if (major <= 5) {
|
||||
const isMoved = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.MoveTemplatesFolder);
|
||||
if (!isMoved) {
|
||||
const wsFolder= Folders.getWorkspaceFolder();
|
||||
if (wsFolder) {
|
||||
const templateFolder = join(parseWinPath(wsFolder.fsPath), `.templates`);
|
||||
if (existsSync(templateFolder)) {
|
||||
window.showInformationMessage(`Would you like to move your ".templates" folder to the new ".frontmatter" folder?`, 'Yes', 'No').then(async (result) => {
|
||||
if (result === "Yes") {
|
||||
const newFolderPath = join(parseWinPath(wsFolder.fsPath), LocalStore.rootFolder, LocalStore.templatesFolder);
|
||||
renameSync(templateFolder, newFolderPath);
|
||||
commands.executeCommand(`workbench.action.reloadWindow`);
|
||||
Settings.update(SETTING_TEMPLATES_FOLDER, undefined, true);
|
||||
Settings.update(SETTING_TEMPLATES_FOLDER, undefined);
|
||||
} else if (result === "No") {
|
||||
Settings.update(SETTING_TEMPLATES_FOLDER, `.templates`, true);
|
||||
}
|
||||
|
||||
if (result === "No" || result === "Yes") {
|
||||
Extension.getInstance().setState(ExtensionState.MoveTemplatesFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async setState(propKey: string, propValue: string): Promise<void> {
|
||||
await this.ctx.globalState.update(propKey, propValue);
|
||||
public async setState<T>(propKey: string, propValue: T, type: "workspace" | "global" = "global"): Promise<void> {
|
||||
if (type === "global") {
|
||||
await this.ctx.globalState.update(propKey, propValue);
|
||||
} else {
|
||||
await this.ctx.workspaceState.update(propKey, propValue);
|
||||
}
|
||||
}
|
||||
|
||||
public async getState<T>(propKey: string): Promise<T | undefined> {
|
||||
return await this.ctx.globalState.get(propKey);
|
||||
public async getState<T>(propKey: string, type: "workspace" | "global" = "global"): Promise<T | undefined> {
|
||||
if (type === "global") {
|
||||
return await this.ctx.globalState.get(propKey);
|
||||
} else {
|
||||
return await this.ctx.workspaceState.get(propKey);
|
||||
}
|
||||
}
|
||||
|
||||
public isBetaVersion() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { EXTENSION_NAME } from '../constants';
|
||||
import { Notifications } from './Notifications';
|
||||
|
||||
export class FilesHelper {
|
||||
|
||||
43
src/helpers/FrameworkDetector.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { existsSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { FrameworkDetectors } from "../constants/FrameworkDetectors";
|
||||
import { Extension } from "./Extension";
|
||||
|
||||
export class FrameworkDetector {
|
||||
|
||||
public static get(folder: string) {
|
||||
return this.check(folder);
|
||||
}
|
||||
|
||||
public static getAll() {
|
||||
return FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
}
|
||||
|
||||
private static check(folder: string) {
|
||||
const { dependencies, devDependencies } = Extension.getInstance().packageJson;
|
||||
|
||||
for (const detector of FrameworkDetectors) {
|
||||
if (detector && folder) {
|
||||
|
||||
// Verify by dependencies
|
||||
for (const dependency of detector.requiredDependencies ?? []) {
|
||||
const inDependencies = dependencies && dependencies[dependency]
|
||||
const inDevDependencies = devDependencies && devDependencies[dependency]
|
||||
if (inDependencies || inDevDependencies) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify by files
|
||||
for (const filename of detector.requiredFiles ?? []) {
|
||||
const fileExists = existsSync(resolve(folder, filename));
|
||||
if (fileExists) {
|
||||
return detector.framework;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
81
src/helpers/ImageHelper.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Uri, window } from 'vscode';
|
||||
import { dirname, join } from "path";
|
||||
import { Field } from '../models';
|
||||
import { existsSync } from 'fs';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { SETTINGS_CONTENT_STATIC_FOLDER } from '../constants';
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
|
||||
export class ImageHelper {
|
||||
|
||||
/**
|
||||
* Parse all images to use absolute paths
|
||||
* @param field
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
public static allRelToAbs(field: Field, value: string | string[] | undefined) {
|
||||
const filePath = window.activeTextEditor?.document.uri.fsPath;
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previewUri = null;
|
||||
|
||||
if (field.multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
previewUri = value.map(v => ({
|
||||
original: v,
|
||||
absPath: ImageHelper.relToAbs(filePath, v)
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
if (typeof value === "string") {
|
||||
return {
|
||||
original: value,
|
||||
absPath: ImageHelper.relToAbs(filePath, value)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return previewUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse relative path to absolute path
|
||||
* @param filePath
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
public static relToAbs(filePath: string, value: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
|
||||
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
|
||||
|
||||
if (existsSync(staticPath)) {
|
||||
return Uri.file(staticPath);
|
||||
} else if (contentFolderPath && existsSync(contentFolderPath)) {
|
||||
return Uri.file(contentFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse absolute path to relative path
|
||||
* @param imgValue
|
||||
* @returns
|
||||
*/
|
||||
public static absToRel(imgValue: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
|
||||
|
||||
let relPath = imgValue || "";
|
||||
if (imgValue) {
|
||||
relPath = imgValue.split(parseWinPath(wsFolder?.fsPath || "")).pop() || "";
|
||||
relPath = imgValue.split(staticFolder || "").pop() || "";
|
||||
}
|
||||
return relPath;
|
||||
}
|
||||
}
|
||||
106
src/helpers/MediaLibrary.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Dashboard } from '../commands/Dashboard';
|
||||
import { workspace } from 'vscode';
|
||||
import { JsonDB } from 'node-json-db/dist/JsonDB';
|
||||
import { basename, dirname, join, parse } from 'path';
|
||||
import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders';
|
||||
import { existsSync, renameSync } from 'fs';
|
||||
import { Notifications } from './Notifications';
|
||||
import { parseWinPath } from './parseWinPath';
|
||||
import { LocalStore } from '../constants';
|
||||
|
||||
interface MediaRecord {
|
||||
description: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export class MediaLibrary {
|
||||
private db: JsonDB | undefined;
|
||||
private static instance: MediaLibrary;
|
||||
|
||||
private constructor() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
if (!wsFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.db = new JsonDB(join(parseWinPath(wsFolder?.fsPath || ""), LocalStore.rootFolder, LocalStore.contentFolder, LocalStore.mediaDatabaseFile), true, false, '/');
|
||||
|
||||
workspace.onDidRenameFiles(e => {
|
||||
e.files.forEach(f => {
|
||||
// Check if file is an image
|
||||
if (f.oldUri.path.endsWith('.jpeg') ||
|
||||
f.oldUri.path.endsWith('.jpg') ||
|
||||
f.oldUri.path.endsWith('.png') ||
|
||||
f.oldUri.path.endsWith('.gif')) {
|
||||
this.rename(f.oldUri.fsPath, f.newUri.fsPath);
|
||||
Dashboard.resetMedia();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static getInstance(): MediaLibrary {
|
||||
if (!MediaLibrary.instance) {
|
||||
MediaLibrary.instance = new MediaLibrary();
|
||||
}
|
||||
|
||||
return MediaLibrary.instance;
|
||||
}
|
||||
|
||||
public get(id: string): MediaRecord | undefined {
|
||||
try {
|
||||
const fileId = this.parsePath(id);
|
||||
if (this.db?.exists(fileId)) {
|
||||
return this.db.getData(fileId);
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set(id: string, metadata: any): void {
|
||||
const fileId = this.parsePath(id);
|
||||
this.db?.push(fileId, metadata, true);
|
||||
}
|
||||
|
||||
public rename(oldId: string, newId: string): void {
|
||||
const fileId = this.parsePath(oldId);
|
||||
const newFileId = this.parsePath(newId);
|
||||
const data = this.db?.getData(fileId);
|
||||
if (data) {
|
||||
this.db?.delete(fileId);
|
||||
this.db?.push(newFileId, data, true);
|
||||
}
|
||||
}
|
||||
|
||||
public updateFilename(filePath: string, filename: string) {
|
||||
const name = basename(filePath);
|
||||
|
||||
if (name !== filename && filename) {
|
||||
try {
|
||||
const oldFileInfo = parse(filePath);
|
||||
const newFileInfo = parse(filename);
|
||||
const newPath = join(dirname(filePath), `${newFileInfo.name}${oldFileInfo.ext}`);
|
||||
|
||||
if (existsSync(newPath)) {
|
||||
Notifications.warning(`The name "${filename}" already exists at the file location.`);
|
||||
} else {
|
||||
renameSync(filePath, newPath);
|
||||
this.rename(filePath, newPath);
|
||||
Dashboard.resetMedia();
|
||||
}
|
||||
} catch(err) {
|
||||
Notifications.error(`Something went wrong updating "${name}" to "${filename}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parsePath(path: string) {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
const isWindows = process.platform === 'win32';
|
||||
let absPath = path.replace(parseWinPath(wsFolder?.fsPath || ""), WORKSPACE_PLACEHOLDER);
|
||||
absPath = isWindows ? absPath.split('\\').join('/') : absPath;
|
||||
return absPath.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import { EXTENSION_NAME } from "../constants";
|
||||
|
||||
export class Notifications {
|
||||
|
||||
public static info(message: string) {
|
||||
window.showInformationMessage(`${EXTENSION_NAME}: ${message}`);
|
||||
public static info(message: string, items?: any): Thenable<string> {
|
||||
return window.showInformationMessage(`${EXTENSION_NAME}: ${message}`, items);
|
||||
}
|
||||
|
||||
public static warning(message: string) {
|
||||
window.showWarningMessage(`${EXTENSION_NAME}: ${message}`);
|
||||
public static warning(message: string, items?: any): Thenable<string> {
|
||||
return window.showWarningMessage(`${EXTENSION_NAME}: ${message}`, items);
|
||||
}
|
||||
|
||||
public static error(message: string) {
|
||||
window.showErrorMessage(`${EXTENSION_NAME}: ${message}`);
|
||||
public static error(message: string, items?: any): Thenable<string> {
|
||||
return window.showErrorMessage(`${EXTENSION_NAME}: ${message}`, items);
|
||||
}
|
||||
}
|
||||
94
src/helpers/Questions.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { window } from 'vscode';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { ContentType } from './ContentType';
|
||||
import { Notifications } from './Notifications';
|
||||
|
||||
export class Questions {
|
||||
|
||||
/**
|
||||
* Yes/No question
|
||||
* @param placeholder
|
||||
* @returns
|
||||
*/
|
||||
public static async yesOrNo(placeholder: string) {
|
||||
const answer = await window.showQuickPick(["yes", "no"], { canPickMany: false, placeHolder: placeholder, ignoreFocusOut: false });
|
||||
return answer === "yes";
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the name of the content to create
|
||||
* @param showWarning
|
||||
* @returns
|
||||
*/
|
||||
public static async ContentTitle(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const title = await window.showInputBox({
|
||||
prompt: `What would you like to use as a title for the content to create?`,
|
||||
placeHolder: `Content title`
|
||||
});
|
||||
|
||||
if (!title && showWarning) {
|
||||
Notifications.warning(`You did not specify a title for your content.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the folder for your content creation
|
||||
* @param showWarning
|
||||
* @returns
|
||||
*/
|
||||
public static async SelectContentFolder(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const folders = Folders.get();
|
||||
|
||||
let selectedFolder: string | undefined;
|
||||
if (folders.length > 1) {
|
||||
selectedFolder = await window.showQuickPick(folders.map(f => f.title), {
|
||||
placeHolder: `Select where you want to create your content`
|
||||
});
|
||||
} else {
|
||||
selectedFolder = folders[0].title;
|
||||
}
|
||||
|
||||
if (!selectedFolder && showWarning) {
|
||||
Notifications.warning(`You didn't select a place where you wanted to create your content.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return selectedFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the content type to create new content
|
||||
* @param showWarning
|
||||
* @returns
|
||||
*/
|
||||
public static async SelectContentType(showWarning: boolean = true): Promise<string | undefined> {
|
||||
const contentTypes = ContentType.getAll();
|
||||
if (!contentTypes || contentTypes.length === 0) {
|
||||
Notifications.warning("No content types found. Please create a content type first.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (contentTypes.length === 1) {
|
||||
return contentTypes[0].name;
|
||||
}
|
||||
|
||||
const options = contentTypes.map(contentType => ({
|
||||
label: contentType.name
|
||||
}));
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
placeHolder: `Select the content type to create your new content`,
|
||||
canPickMany: false
|
||||
});
|
||||
|
||||
if (!selectedOption && showWarning) {
|
||||
Notifications.warning("No content type was selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
return selectedOption?.label;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Notifications } from './Notifications';
|
||||
import { workspace } from 'vscode';
|
||||
import { commands, Uri, workspace, window } from 'vscode';
|
||||
import * as vscode from 'vscode';
|
||||
import { TaxonomyType } from '../models';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY } from '../constants';
|
||||
import { ContentType, TaxonomyType } from '../models';
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, CONFIG_KEY, CONTEXT, SETTINGS_CONTENT_STATIC_FOLDER, ExtensionState } from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename } from 'path';
|
||||
import { existsSync, readFileSync, watch, writeFileSync } from 'fs';
|
||||
import { Extension } from './Extension';
|
||||
|
||||
export class Settings {
|
||||
public static globalFile = "frontmatter.json";
|
||||
private static config: vscode.WorkspaceConfiguration;
|
||||
private static globalFile = "frontmatter.json";
|
||||
private static globalConfig: any;
|
||||
|
||||
public static init() {
|
||||
@@ -18,6 +18,7 @@ export class Settings {
|
||||
if (fmConfig && existsSync(fmConfig)) {
|
||||
const localConfig = readFileSync(fmConfig, 'utf8');
|
||||
Settings.globalConfig = JSON.parse(localConfig);
|
||||
commands.executeCommand('setContext', CONTEXT.isEnabled, true);
|
||||
} else {
|
||||
Settings.globalConfig = undefined;
|
||||
}
|
||||
@@ -33,6 +34,26 @@ export class Settings {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the setting is present in the workspace and ask to promote them to the global settings
|
||||
*/
|
||||
public static async checkToPromote() {
|
||||
const isPromoted = await Extension.getInstance().getState<boolean | undefined>(ExtensionState.SettingPromoted, "workspace");
|
||||
if (!isPromoted) {
|
||||
if (Settings.hasSettings()) {
|
||||
window.showInformationMessage(`You have local settings. Would you like to promote them to the global settings ("frontmatter.json")?`, 'Yes', 'No').then(async (result) => {
|
||||
if (result === "Yes") {
|
||||
Settings.promote();
|
||||
}
|
||||
|
||||
if (result === "No" || result === "Yes") {
|
||||
Extension.getInstance().setState(ExtensionState.SettingPromoted, true, "workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for config changes on global and local settings
|
||||
* @param callback
|
||||
@@ -115,6 +136,12 @@ export class Settings {
|
||||
Settings.globalConfig = JSON.parse(localConfig);
|
||||
Settings.globalConfig[`${CONFIG_KEY}.${name}`] = value;
|
||||
writeFileSync(fmConfig, JSON.stringify(Settings.globalConfig, null, 2), 'utf8');
|
||||
|
||||
const workspaceSettingValue = Settings.hasWorkspaceSettings<ContentType[]>(name);
|
||||
if (workspaceSettingValue) {
|
||||
await Settings.update(name, undefined);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -131,7 +158,10 @@ export class Settings {
|
||||
*/
|
||||
public static createTeamSettings() {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
this.createGlobalFile(wsFolder);
|
||||
}
|
||||
|
||||
public static createGlobalFile(wsFolder: Uri | undefined | null) {
|
||||
const initialConfig = {
|
||||
"$schema": `https://${Extension.getInstance().isBetaVersion() ? `beta.` : ``}frontmatter.codes/frontmatter.schema.json`
|
||||
};
|
||||
@@ -195,6 +225,16 @@ export class Settings {
|
||||
Notifications.info(`All settings promoted to team level.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the setting is present in the workspace
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
public static hasWorkspaceSettings<T>(name: string): T | undefined {
|
||||
const setting = Settings.config.inspect<T>(name);
|
||||
return (setting && typeof setting.workspaceValue !== "undefined") ? setting.workspaceValue : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any Front Matter settings in the workspace
|
||||
* @returns
|
||||
@@ -219,7 +259,6 @@ export class Settings {
|
||||
return hasSetting;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if its the project config
|
||||
* @param filePath
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { stopWords } from '../constants/stopwords-en';
|
||||
import { charMap } from '../constants/charMap';
|
||||
import { stopWords, charMap } from '../constants';
|
||||
|
||||
export class SlugHelper {
|
||||
|
||||
@@ -15,14 +14,18 @@ export class SlugHelper {
|
||||
|
||||
// Remove punctuation from input string, and split it into words.
|
||||
let cleanTitle = this.removePunctuation(articleTitle);
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
if (cleanTitle) {
|
||||
cleanTitle = cleanTitle.toLowerCase();
|
||||
// Split into words
|
||||
let words = cleanTitle.split(/\s/);
|
||||
// Removing stop words
|
||||
words = this.removeStopWords(words);
|
||||
cleanTitle = words.join("-");
|
||||
cleanTitle = this.replaceCharacters(cleanTitle);
|
||||
return cleanTitle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,9 +34,13 @@ export class SlugHelper {
|
||||
* @param value
|
||||
*/
|
||||
private static removePunctuation(value: string): string {
|
||||
const punctuationless = value.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const punctuationless = value?.replace(/[\.,-\/#!$@%\^&\*;:{}=\-_`'"~()+\?<>]/g, " ");
|
||||
// Remove double spaces
|
||||
return punctuationless.replace(/\s{2,}/g," ");
|
||||
return punctuationless?.replace(/\s{2,}/g," ");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
3
src/helpers/parseWinPath.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const parseWinPath = (path: string | undefined): string => {
|
||||
return path?.split(`\\`).join(`/`) || '';
|
||||
}
|
||||
27
src/hooks/useContentType.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
|
||||
import { Settings } from '../dashboardWebView/models';
|
||||
import { ContentType, PanelSettings } from '../models';
|
||||
|
||||
export default function useContentType(settings: PanelSettings | Settings | undefined | null, metadata: any) {
|
||||
const [contentType, setContentType] = useState<ContentType>(DEFAULT_CONTENT_TYPE);
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
const contentTypeName = metadata.type as string || DEFAULT_CONTENT_TYPE_NAME;
|
||||
let ct = settings.contentTypes.find(ct => ct.name === contentTypeName);
|
||||
|
||||
if (!ct) {
|
||||
ct = settings.contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
|
||||
}
|
||||
|
||||
if (!ct || !ct.fields) {
|
||||
ct = DEFAULT_CONTENT_TYPE;
|
||||
}
|
||||
|
||||
setContentType(ct || DEFAULT_CONTENT_TYPE)
|
||||
}
|
||||
}, [settings?.contentTypes, metadata?.data]);
|
||||
|
||||
return contentType;
|
||||
}
|
||||
5
src/models/Choice.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
export interface Choice {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||