diff --git a/.github/ISSUE_TEMPLATE/feedback_request.md b/.github/ISSUE_TEMPLATE/feedback_request.md new file mode 100644 index 00000000..68001fdf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feedback_request.md @@ -0,0 +1,7 @@ +--- +name: Feedback +about: Tell more on what you think +title: 'Feedback: ' +labels: '' +assignees: '' +--- diff --git a/CHANGELOG.md b/CHANGELOG.md index f1986bd1..6ec3bbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Change Log +## [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 diff --git a/README.beta.md b/README.beta.md index d1ca7ddc..f3b85ef7 100644 --- a/README.beta.md +++ b/README.beta.md @@ -96,18 +96,28 @@ If you have the courage to test out the beta features, we made available a beta ## 👉 Contributors 🤘 - - - +

+ + + +

-
-
+## 🖤 Sponsors 👇 🤘 -## 🖤 Sponsors +

+ + + + + + +

+ +

- +

diff --git a/README.md b/README.md index 287dd1df..5068fab6 100644 --- a/README.md +++ b/README.md @@ -94,18 +94,28 @@ If you have the courage to test out the beta features, we made available a beta ## 👉 Contributors 🤘 - - - +

+ + + +

-
-
+## 🖤 Sponsors 👇 🤘 -## 🖤 Sponsors +

+ + + + + + +

+ +

- +

diff --git a/assets/front-matter.png b/assets/front-matter.png index f6741a12..f6338d14 100644 Binary files a/assets/front-matter.png and b/assets/front-matter.png differ diff --git a/assets/frontmatter-128x128.png b/assets/frontmatter-128x128.png index 9c8ca78b..1c42ed41 100644 Binary files a/assets/frontmatter-128x128.png and b/assets/frontmatter-128x128.png differ diff --git a/assets/frontmatter-dark.svg b/assets/frontmatter-dark.svg index e4a53ab8..6930b233 100644 --- a/assets/frontmatter-dark.svg +++ b/assets/frontmatter-dark.svg @@ -1,40 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/frontmatter-mag-min.png b/assets/frontmatter-mag-min.png index 94d0ee52..bf66fe0d 100644 Binary files a/assets/frontmatter-mag-min.png and b/assets/frontmatter-mag-min.png differ diff --git a/assets/frontmatter-mag-min.svg b/assets/frontmatter-mag-min.svg index 61fbccae..0f778b79 100644 --- a/assets/frontmatter-mag-min.svg +++ b/assets/frontmatter-mag-min.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/frontmatter-mag.svg b/assets/frontmatter-mag.svg index 744f1fd8..0f778b79 100644 --- a/assets/frontmatter-mag.svg +++ b/assets/frontmatter-mag.svg @@ -1,44 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/frontmatter-short-min.png b/assets/frontmatter-short-min.png new file mode 100644 index 00000000..3dfecc48 Binary files /dev/null and b/assets/frontmatter-short-min.png differ diff --git a/assets/frontmatter-short-min.svg b/assets/frontmatter-short-min.svg new file mode 100644 index 00000000..05ad2382 --- /dev/null +++ b/assets/frontmatter-short-min.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/assets/frontmatter-teal-128x128.png b/assets/frontmatter-teal-128x128.png index 8fef61b4..0c62d274 100644 Binary files a/assets/frontmatter-teal-128x128.png and b/assets/frontmatter-teal-128x128.png differ diff --git a/assets/frontmatter-teal-min.svg b/assets/frontmatter-teal-min.svg new file mode 100644 index 00000000..22571b3c --- /dev/null +++ b/assets/frontmatter-teal-min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/frontmatter.png b/assets/frontmatter.png index 221af287..f6338d14 100644 Binary files a/assets/frontmatter.png and b/assets/frontmatter.png differ diff --git a/assets/frontmatter.svg b/assets/frontmatter.svg index 3c56d871..6d9f7e94 100644 --- a/assets/frontmatter.svg +++ b/assets/frontmatter.svg @@ -1,39 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/icons/close-dark.svg b/assets/icons/close-dark.svg index 9c5818f2..257a84b0 100644 --- a/assets/icons/close-dark.svg +++ b/assets/icons/close-dark.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/assets/icons/close-light.svg b/assets/icons/close-light.svg index 65e4e669..b9270b95 100644 --- a/assets/icons/close-light.svg +++ b/assets/icons/close-light.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/assets/icons/frontmatter-dark.svg b/assets/icons/frontmatter-dark.svg new file mode 100644 index 00000000..dc46fe08 --- /dev/null +++ b/assets/icons/frontmatter-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/frontmatter-light.svg b/assets/icons/frontmatter-light.svg new file mode 100644 index 00000000..e53d4c64 --- /dev/null +++ b/assets/icons/frontmatter-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/styles.css b/assets/media/styles.css index 38ee991b..e792f0a9 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -437,6 +437,7 @@ input:checked + .field__toggle__slider:before { margin-right: .5rem; } +.metadata_field__input, .metadata_field__input:focus, .metadata_field__textarea, .metadata_field__textarea:focus { outline: none; } @@ -451,17 +452,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__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::placeholde { - color: var(--vscode-input-placeholderForeground); +.metadata_field__choice__button_icon { + height: 1.25rem; + width: 1.25rem; + margin-left: .5rem; } .metadata_field__datetime { @@ -481,12 +557,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); diff --git a/package-lock.json b/package-lock.json index dd5761c0..d923a1fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-front-matter-beta", - "version": "4.0.1", + "version": "5.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -268,6 +268,96 @@ "integrity": "sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==", "dev": true }, + "@sentry/browser": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz", + "integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==", + "dev": true, + "requires": { + "@sentry/core": "6.13.2", + "@sentry/types": "6.13.2", + "@sentry/utils": "6.13.2", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz", + "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==", + "dev": true, + "requires": { + "@sentry/hub": "6.13.2", + "@sentry/minimal": "6.13.2", + "@sentry/types": "6.13.2", + "@sentry/utils": "6.13.2", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz", + "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==", + "dev": true, + "requires": { + "@sentry/types": "6.13.2", + "@sentry/utils": "6.13.2", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz", + "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==", + "dev": true, + "requires": { + "@sentry/hub": "6.13.2", + "@sentry/types": "6.13.2", + "tslib": "^1.9.3" + } + }, + "@sentry/react": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz", + "integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==", + "dev": true, + "requires": { + "@sentry/browser": "6.13.2", + "@sentry/minimal": "6.13.2", + "@sentry/types": "6.13.2", + "@sentry/utils": "6.13.2", + "hoist-non-react-statics": "^3.3.2", + "tslib": "^1.9.3" + } + }, + "@sentry/tracing": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz", + "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==", + "dev": true, + "requires": { + "@sentry/hub": "6.13.2", + "@sentry/minimal": "6.13.2", + "@sentry/types": "6.13.2", + "@sentry/utils": "6.13.2", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz", + "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg==", + "dev": true + }, + "@sentry/utils": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz", + "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==", + "dev": true, + "requires": { + "@sentry/types": "6.13.2", + "tslib": "^1.9.3" + } + }, "@tailwindcss/forms": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.3.tgz", @@ -2873,6 +2963,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 +3184,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", @@ -4106,6 +4214,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 +4916,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", diff --git a/package.json b/package.json index 3a1b2a92..96167159 100644 --- a/package.json +++ b/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.0.0", "preview": false, "publisher": "eliostruyf", "galleryBanner": { @@ -268,8 +268,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, @@ -306,7 +342,7 @@ "type": "datetime" }, { - "title": "Article preview", + "title": "Content preview", "name": "preview", "type": "image" }, @@ -419,7 +455,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 +511,7 @@ }, { "command": "frontMatter.generateSlug", - "title": "Generate slug based on article title", + "title": "Generate slug based on content title", "category": "Front matter" }, { @@ -490,7 +526,7 @@ }, { "command": "frontMatter.insertImage", - "title": "Insert image into article", + "title": "Insert image into your content", "category": "Front matter", "icon": "$(device-camera)" }, @@ -501,17 +537,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 +570,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 +658,8 @@ "@headlessui/react": "^1.4.1", "@heroicons/react": "1.0.4", "@iarna/toml": "2.2.3", + "@sentry/react": "^6.13.2", + "@sentry/tracing": "^6.13.2", "@tailwindcss/forms": "^0.3.3", "@types/glob": "7.1.3", "@types/js-yaml": "3.12.1", @@ -636,8 +680,10 @@ "gray-matter": "4.0.2", "html-loader": "1.3.2", "html-webpack-plugin": "4.5.0", + "image-size": "^1.0.0", "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", diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 90c75841..b6a3f44f 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -1,7 +1,6 @@ -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'); diff --git a/src/commands/Content.ts b/src/commands/Content.ts new file mode 100644 index 00000000..6f8c514d --- /dev/null +++ b/src/commands/Content.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 7de12e45..62f8588d 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -1,7 +1,7 @@ -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 } from '../constants'; import { ArticleHelper } from './../helpers/ArticleHelper'; import { basename, dirname, extname, join } from "path"; -import { existsSync, statSync, unlinkSync, writeFileSync } from "fs"; +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'; @@ -10,7 +10,6 @@ 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'; @@ -20,10 +19,11 @@ 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'; export class Dashboard { private static webview: WebviewPanel | null = null; @@ -31,6 +31,7 @@ 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; @@ -50,6 +51,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) { @@ -136,6 +139,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 +160,16 @@ export class Dashboard { } break; case DashboardMessage.setPageViewType: - Extension.getInstance().setState(EXTENSION_STATE_PAGES_VIEW, msg.data); + Extension.getInstance().setState(ExtensionState.PagesView, msg.data); 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 +181,27 @@ 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; } }); } + + /** + * 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 @@ -197,12 +224,12 @@ export class Dashboard { const line = data.position.line; const character = data.position.character; if (line) { - await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![](${data.image})`)); + await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${data.image})`)); } panel.getMediaSelection(); } else { panel.getMediaSelection(); - panel.updateMetadata({field: data.fieldName, value: data.image}); + panel.updateMetadata({field: data.fieldName, value: data.image }); } } } @@ -219,15 +246,17 @@ export class Dashboard { data: { beta: ext.isBetaVersion(), wsFolder: wsFolder ? wsFolder.fsPath : '', - staticFolder: SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDERS), + staticFolder: SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDER), folders: Folders.get(), initialized: await Template.isInitialized(), tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag), categories: SettingsHelper.getTaxonomy(TaxonomyType.Category), openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART), versionInfo: ext.getVersion(), - pageViewType: await ext.getState(EXTENSION_STATE_PAGES_VIEW), + pageViewType: await ext.getState(ExtensionState.PagesView), mediaSnippet: SettingsHelper.get(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [], + contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [], + contentFolders: Folders.get().map(f => f.path), } as Settings }); } @@ -243,50 +272,67 @@ export class Dashboard { /** * Retrieve all media files */ - private static async getMedia(page: number = 0, folder: string = '') { + private static async getMedia(page: number = 0, selectedFolder: string = '') { const wsFolder = Folders.getWorkspaceFolder(); - const staticFolder = SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDERS); + const staticFolder = SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDER); + const contentFolders = Folders.get(); + + // If the static folder is not set, retreive the last opened location + if (!selectedFolder) { + const stateValue = await Extension.getInstance().getState(ExtensionState.SelectedFolder); + if (stateValue && existsSync(stateValue)) { + selectedFolder = stateValue; + } + } + + // Go to the home folder + if (selectedFolder === HOME_PAGE_NAVIGATION_ID) { + selectedFolder = ''; + } - if (Dashboard.media.length === 0) { - const contentFolder = Folders.get(); - let allMedia: MediaInfo[] = []; + const relSelectedFolderPath = selectedFolder ? selectedFolder.substring((parseWinPath(wsFolder?.fsPath || "")).length + 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; - }); + 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); - } + Dashboard.media = Object.assign([], allMedia); - // Filter the media 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 +341,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, selectedFolder); Dashboard.postWebviewMessage({ command: DashboardCommand.media, data: { media: files, total: total, - folders + folders: [...allContentFolders, ...allFolders], + selectedFolder } as MediaPaths }); } @@ -333,7 +400,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(SETTINGS_CONTENT_STATIC_FOLDERS); + const staticFolder = SettingsHelper.get(SETTINGS_CONTENT_STATIC_FOLDER); const folderInfo = await Folders.getInfo(); const pages: Page[] = []; @@ -362,23 +429,38 @@ export class Dashboard { draft: article?.data.draft, description: article?.data[descriptionField] || "", }; + + const contentType = ArticleHelper.getContentType(article.data); + const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview"; - 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); + 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 +501,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(SETTINGS_CONTENT_STATIC_FOLDERS); + const staticFolder = SettingsHelper.get(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 || ""); @@ -475,6 +561,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 @@ -504,7 +602,7 @@ export class Dashboard { - + Front Matter Dashboard diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 61f7ca15..163cacc3 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -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 { /** - * Create content in a registered folder + * Add a media folder * @returns */ - public static async create() { - const folders = Folders.get(); + public static async addMediaFolder(data?: {selectedFolder?: string}) { + let wsFolder = Folders.getWorkspaceFolder(); + const staticFolder = Settings.get(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 (!folders || folders.length === 0) { - Notifications.warning(`There are no known content locations defined in this project.`); + if (!folderName) { + Notifications.warning(`No folder name was specified.`); return; } + + const folders = folderName.split("/").filter(f => f); + let parentFolders: string[] = []; - 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; + 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 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 ""; @@ -223,7 +285,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 +298,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; } diff --git a/src/commands/Preview.ts b/src/commands/Preview.ts index 298078ac..efbaeb5b 100644 --- a/src/commands/Preview.ts +++ b/src/commands/Preview.ts @@ -1,11 +1,10 @@ -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'; export class Preview { diff --git a/src/commands/Project.ts b/src/commands/Project.ts index 23d08556..d71d3ebf 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -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"; diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index 00ed1b6f..ac5b887c 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -1,4 +1,4 @@ -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'; diff --git a/src/commands/Template.ts b/src/commands/Template.ts index 7c205a60..da9c7120 100644 --- a/src/commands/Template.ts +++ b/src/commands/Template.ts @@ -1,3 +1,4 @@ +import { Questions } from './../helpers/Questions'; import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; @@ -7,7 +8,7 @@ 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'; @@ -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?`, } ); @@ -113,26 +114,22 @@ 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; } @@ -180,7 +177,7 @@ export class Template { vscode.window.showTextDocument(txtDoc); } - Notifications.info(`Your new article has been created.`); + Notifications.info(`Your new content has been created.`); } /** diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index e3098bbf..05c71422 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -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"), }; \ No newline at end of file diff --git a/src/constants/ExtensionState.ts b/src/constants/ExtensionState.ts new file mode 100644 index 00000000..4f6be545 --- /dev/null +++ b/src/constants/ExtensionState.ts @@ -0,0 +1,8 @@ + +export const ExtensionState = { + PagesView: `frontMatter:Pages:ViewType`, + SelectedFolder: `frontMatter:SelectedFolder`, + Version: `frontMatter:Version`, + SettingPromoted: `frontMatter:Settings:Promoted`, + MoveTemplatesFolder: `frontMatter:Templates:Move`, +}; \ No newline at end of file diff --git a/src/constants/Links.ts b/src/constants/Links.ts index 45975296..f6323a21 100644 --- a/src/constants/Links.ts +++ b/src/constants/Links.ts @@ -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"; \ No newline at end of file +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"; \ No newline at end of file diff --git a/src/constants/LocalStore.ts b/src/constants/LocalStore.ts new file mode 100644 index 00000000..a5634e5e --- /dev/null +++ b/src/constants/LocalStore.ts @@ -0,0 +1,8 @@ + + +export const LocalStore = { + rootFolder: ".frontmatter", + contentFolder: "content", + templatesFolder: "templates", + mediaDatabaseFile: "mediaDb.json" +} \ No newline at end of file diff --git a/src/constants/Navigation.ts b/src/constants/Navigation.ts new file mode 100644 index 00000000..7ebf76d3 --- /dev/null +++ b/src/constants/Navigation.ts @@ -0,0 +1 @@ +export const HOME_PAGE_NAVIGATION_ID = "FrontMatter:RootFolder"; \ No newline at end of file diff --git a/src/constants/context.ts b/src/constants/context.ts index 1cac06a8..3adfb320 100644 --- a/src/constants/context.ts +++ b/src/constants/context.ts @@ -1,5 +1,6 @@ export const CONTEXT = { canInit: "frontMatterCanInit", canOpenPreview: "frontMatterCanOpenPreview", - canOpenDashboard: "frontMatterCanOpenDashboard" + canOpenDashboard: "frontMatterCanOpenDashboard", + isEnabled: "frontMatter:enabled" }; \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index cf830ff8..fad8cc87 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -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'; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index 0d21d36c..fa0f92b2 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -36,7 +36,7 @@ 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_DASHBOARD_OPENONSTART = "dashboard.openOnStart"; diff --git a/src/dashboardWebView/DashboardCommand.ts b/src/dashboardWebView/DashboardCommand.ts index 314125fb..42442972 100644 --- a/src/dashboardWebView/DashboardCommand.ts +++ b/src/dashboardWebView/DashboardCommand.ts @@ -3,5 +3,5 @@ export enum DashboardCommand { pages = "pages", settings = "settings", media = "media", - viewData = "viewData", + viewData = "viewData" } \ No newline at end of file diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 1afc8ac0..cb86a9c6 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -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,6 @@ export enum DashboardMessage { uploadMedia = 'uploadMedia', deleteMedia = 'deleteMedia', insertPreviewImage = 'insertPreviewImage', + updateMediaMetadata = 'updateMediaMetadata', + createMediaFolder = 'createMediaFolder' } \ No newline at end of file diff --git a/src/dashboardWebView/components/ChoiceButton.tsx b/src/dashboardWebView/components/ChoiceButton.tsx new file mode 100644 index 00000000..430d0b6c --- /dev/null +++ b/src/dashboardWebView/components/ChoiceButton.tsx @@ -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 = ({onClick, disabled, choices, title}: React.PropsWithChildren) => { + return ( + + + + + + Open options + + + +
+ {choices.map((choice) => ( + + ))} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index a68ee036..31488d7c 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -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 = ({ fmFilePath, date, title, draft, description, preview }: React.PropsWithChildren) => { +export const Item: React.FunctionComponent = ({ fmFilePath, date, title, draft, description, type, ...pageData }: React.PropsWithChildren) => { 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 = ({ fmFilePath, date, ti onClick={openFile}>
{ - preview ? ( - {title} + previewField && pageData[previewField] ? ( + {title} ) : (
diff --git a/src/dashboardWebView/components/Dashboard.tsx b/src/dashboardWebView/components/Dashboard.tsx index f366cdd5..c48071b3 100644 --- a/src/dashboardWebView/components/Dashboard.tsx +++ b/src/dashboardWebView/components/Dashboard.tsx @@ -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'; diff --git a/src/dashboardWebView/components/Header/Breadcrumb.tsx b/src/dashboardWebView/components/Header/Breadcrumb.tsx new file mode 100644 index 00000000..24c70f1d --- /dev/null +++ b/src/dashboardWebView/components/Header/Breadcrumb.tsx @@ -0,0 +1,99 @@ +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 = (props: React.PropsWithChildren) => { + const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom); + const settings = useRecoilValue(SettingsAtom); + const [ folders, setFolders ] = React.useState([]); + + 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; + } + } + + for (let i = 0; i < contentFolders.length; i++) { + const contentFolder = parseWinPath(contentFolders[i]) 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 ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/Folders.tsx b/src/dashboardWebView/components/Header/Folders.tsx index 27440ad2..654c5db3 100644 --- a/src/dashboardWebView/components/Header/Folders.tsx +++ b/src/dashboardWebView/components/Header/Folders.tsx @@ -1,4 +1,4 @@ -import { Menu, Transition } from '@headlessui/react'; +import { Menu } from '@headlessui/react'; import * as React from 'react'; import { useRecoilState } from 'recoil'; import { FolderAtom } from '../../state'; diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index 9eff527a..34e13635 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -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 = ({totalPages, folde Messenger.send(DashboardMessage.createContent); }; + const createByContentType = () => { + Messenger.send(DashboardMessage.createByContentType); + }; + + const createByTemplate = () => { + Messenger.send(DashboardMessage.createByTemplate); + }; + return (
@@ -60,7 +69,19 @@ export const Header: React.FunctionComponent = ({totalPages, folde
- +
@@ -93,7 +114,10 @@ export const Header: React.FunctionComponent = ({totalPages, folde { view === "media" && ( - + <> + + + ) }
diff --git a/src/dashboardWebView/components/Media/FolderCreation.tsx b/src/dashboardWebView/components/Media/FolderCreation.tsx new file mode 100644 index 00000000..052ecc99 --- /dev/null +++ b/src/dashboardWebView/components/Media/FolderCreation.tsx @@ -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 = (props: React.PropsWithChildren) => { + const selectedFolder = useRecoilValue(SelectedMediaFolderAtom); + + const onFolderCreation = () => { + Messenger.send(DashboardMessage.createMediaFolder, { + selectedFolder + }); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/FolderItem.tsx b/src/dashboardWebView/components/Media/FolderItem.tsx new file mode 100644 index 00000000..17d9402a --- /dev/null +++ b/src/dashboardWebView/components/Media/FolderItem.tsx @@ -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 = ({ folder, wsFolder, staticFolder }: React.PropsWithChildren) => { + const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom); + + const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder; + + return ( +
  • + +
  • + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/FolderSelection.tsx b/src/dashboardWebView/components/Media/FolderSelection.tsx deleted file mode 100644 index 2281a011..00000000 --- a/src/dashboardWebView/components/Media/FolderSelection.tsx +++ /dev/null @@ -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 = (props: React.PropsWithChildren) => { - 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 ( -
    - setFocus(false)} - onSelect={(selFolder) => { - setSelectedFolder(selFolder); - setFocus(false); - }}> - { - ({ - getInputProps, - getItemProps, - getMenuProps, - isOpen, - inputValue, - getRootProps - }) => ( -
    - - -
    - 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 && ( - - ) - } -
    - -
    - {isOpen - ? allFolders - .filter((item: string) => !inputValue || item.includes(inputValue)) - .map((item, index) => ( -
    - {item} -
    - )) - : null} -
    -
    - ) - } -
    -
    - ); -}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Media/Item.tsx b/src/dashboardWebView/components/Media/Item.tsx index ef2be18b..98295957 100644 --- a/src/dashboardWebView/components/Media/Item.tsx +++ b/src/dashboardWebView/components/Media/Item.tsx @@ -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 = ({media}: React.PropsWithChildren) => { - 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(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 = ({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 = ({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 = ({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 ( <>
  • @@ -111,17 +183,24 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi {basename(media.fsPath)}
  • -
    -
    +
    +
    + + { viewData?.data?.filePath ? ( <> { (viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && ( @@ -156,19 +235,95 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi

    {basename(parseWinPath(media.fsPath) || "")}

    -

    - Folder: {getFolder()} + { + media.caption && ( +

    + Caption: + {media.caption} +

    + ) + } + { + media.alt && ( +

    + Alt: + {media.alt} +

    + ) + } +

    + Folder: + {getFolder()}

    { - media?.stats?.size && ( -

    - Size: {calculateSize()} + (media?.stats?.size || media?.dimensions) && ( +

    + Size: + {calculateSize()}

    ) }
    + { + showForm && ( + setShowForm(false)} + trigger={submitMetadata} + isSaveDisabled={!filename}> +
    +
    + +
    + setFilename(`${e.target.value}.${extension}`)} /> + +
    + + .{extension} + +
    +
    +
    +
    + +
    +