Compare commits

...

39 Commits

Author SHA1 Message Date
Elio Struyf
511fd48081 Merge pull request #224 from estruyf/dev 2022-01-10 13:01:03 +01:00
Elio Struyf
0039fc1555 Release date added 2022-01-10 13:00:32 +01:00
Elio Struyf
98044187cd Update changelog 2022-01-07 13:03:26 +01:00
Elio Struyf
a6dcc1ea79 Merge pull request #222 from farmerau/onWillSaveTextDocument 2022-01-07 12:55:34 +01:00
Elio Struyf
32a686227e #221 - Logic to remove new line by gray matter 2022-01-07 12:54:39 +01:00
Austin Farmer
faa74132e5 Adjust Automatic Date Updates to Run on Save
Prefers `onWillSaveTextDocument` over `onDidChangeTextDocument` for the triggering event.
2022-01-06 15:44:36 -05:00
Elio Struyf
3a847f7e42 Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev 2022-01-05 18:49:40 +01:00
Elio Struyf
66c978891e updated changelog 2022-01-05 18:49:18 +01:00
Elio Struyf
2f31230e07 Merge pull request #220 from farmerau/markdown-file-heuristics-mdx 2022-01-05 18:40:01 +01:00
Austin Farmer
c4225c0011 Support detection by file extension 2022-01-05 11:38:32 -05:00
Elio Struyf
4edc7a0280 Updated filetype 2022-01-05 10:17:34 +01:00
Elio Struyf
a60fe5204b Updated config 2022-01-04 11:57:16 +01:00
Elio Struyf
bb980b4afe #218 - Added MDX support for template and content creation 2022-01-04 11:51:11 +01:00
Elio Struyf
504658d87a 5.10.0 2022-01-04 11:24:40 +01:00
Elio Struyf
feff69d969 Merge pull request #217 from estruyf/dev 2022-01-01 20:21:23 +01:00
Elio Struyf
a34d77242a Updated entry 2022-01-01 20:20:57 +01:00
Elio Struyf
1b24c1277d Merge pull request #216 from estruyf/dev 2022-01-01 20:15:04 +01:00
Elio Struyf
e6750205be 🎇 New Year release 🎆 2022-01-01 20:14:15 +01:00
Elio Struyf
1da8bf3f8b To lowercase links 2021-12-29 19:14:10 +01:00
Elio Struyf
90c60b6a40 Merge branch 'LuiseFreese-main' into dev 2021-12-29 19:12:35 +01:00
Luise Freese
ee79f89c7f fixes casing issues that led to links that only referred to settings site but not to the specific heading 2021-12-29 11:17:11 +01:00
Elio Struyf
0668d48fd5 updated changelog 2021-12-28 21:20:16 +01:00
Elio Struyf
d046f73d16 #214 - Open file after creation fix 2021-12-28 20:52:42 +01:00
Elio Struyf
f144d713d1 added edit quick action 2021-12-28 10:45:33 +01:00
Elio Struyf
d31c403bdc Update icon 2021-12-28 10:43:36 +01:00
Elio Struyf
35a0327387 Added quick actions + some styling fixes 2021-12-28 10:35:46 +01:00
Elio Struyf
9b39649bde Update description 2021-12-27 16:29:37 +01:00
Elio Struyf
3d857463f0 Updated extension description 2021-12-27 16:25:55 +01:00
Elio Struyf
0428642a2f Update changelog 2021-12-27 16:22:26 +01:00
Elio Struyf
5182a9ae1a Fix spinner overlapping the global navigation 2021-12-27 16:22:01 +01:00
Elio Struyf
ab3686b3b5 #199 - Search media files 2021-12-27 15:56:12 +01:00
Elio Struyf
c5b7b7845d #212 - Added deleted folder watcher 2021-12-26 20:29:44 +01:00
Elio Struyf
2f13c335ed #212 - Added folder watchers 2021-12-26 20:24:23 +01:00
Elio Struyf
f219ac721f Updated navigation header 2021-12-26 19:13:42 +01:00
Elio Struyf
0149885289 Item borders 2021-12-26 18:56:24 +01:00
Elio Struyf
cb80a10de2 #213 - Media folder design 2021-12-26 17:19:31 +01:00
Elio Struyf
092eb0fd2a #211 - Replace text selection on media insert 2021-12-26 12:11:38 +01:00
Elio Struyf
24f79d9d3f #210 - Media file extension fix 2021-12-26 11:52:45 +01:00
Elio Struyf
d667b19716 5.9.0 2021-12-26 11:40:28 +01:00
39 changed files with 744 additions and 372 deletions

View File

@@ -1,5 +1,32 @@
# Change Log
## [5.10.0] - 2022-01-10
### 🎨 Enhancements
- [#218](https://github.com/estruyf/vscode-front-matter/issues/218): Add support for creating `mdx` files from templates and content types. This introduced a new setting: `frontMatter.content.defaultFileType`.
- [#220](https://github.com/estruyf/vscode-front-matter/issues/220): Add support DateTime updates in `mdx` files when the `mdx extension` is not installed.
### 🐞 Fixes
- [#221](https://github.com/estruyf/vscode-front-matter/issues/221): Automatic DateTime switch from on text change to on save to prevent multiple updates.
## [5.9.0] - 2022-01-01 - 🎇🎆
### 🎨 Enhancements
- Fixing the spinner which overlaps the global navigation bar
- Quick actions added for media files (edit, delete, insert markdown, insert snippet)
- [#199](https://github.com/estruyf/vscode-front-matter/issues/199): Search media files in the currently selected folder
- [#211](https://github.com/estruyf/vscode-front-matter/issues/211): Replace text selection on media inserts
- [#212](https://github.com/estruyf/vscode-front-matter/issues/212): Create folder watchers for content folders. When new content gets created, the dashboard updates.
- [#213](https://github.com/estruyf/vscode-front-matter/issues/213): New media folder overview design
### 🐞 Fixes
- [#210](https://github.com/estruyf/vscode-front-matter/issues/210): Fix for adding media files with uppercase file extensions
- [#214](https://github.com/estruyf/vscode-front-matter/issues/214): Fix for opening markdown file after creating it for the specified content type
## [5.8.0] - 2021-12-21 - 🎄
### 🎨 Enhancements

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "vscode-front-matter-beta",
"version": "5.8.0",
"version": "5.10.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "vscode-front-matter-beta",
"version": "5.8.0",
"version": "5.10.0",
"license": "MIT",
"devDependencies": {
"@bendera/vscode-webview-elements": "0.6.2",

View File

@@ -1,9 +1,9 @@
{
"name": "vscode-front-matter-beta",
"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...",
"description": "Front Matter is a CMS that runs within Visual Studio Code. It gives you the power and control of a full-blown CMS while also providing you the flexibility and speed of the static site generator of your choice like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "5.8.0",
"version": "5.10.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -66,7 +66,7 @@
"onCommand:frontMatter.insertImage",
"onView:frontMatter.explorer"
],
"main": "./dist/extension",
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
@@ -97,6 +97,16 @@
"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.defaultFileType": {
"type": "string",
"default": "md",
"enum": [
"md",
"mdx"
],
"markdownDescription": "Specify the default file type for the content to create. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultfiletype)",
"scope": "Content"
},
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
@@ -113,12 +123,12 @@
"type": "string"
}
],
"markdownDescription": "Specify the default sorting option for the content dashboard. You can use one of the values from the enum or define your own ID. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.sorting.default)",
"markdownDescription": "Specify the default sorting option for the content dashboard. You can use one of the values from the enum or define your own ID. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.defaultSorting)",
"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)",
"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"
@@ -156,7 +166,7 @@
"frontMatter.content.fmHighlight": {
"type": "boolean",
"default": true,
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.fmhighlight)",
"markdownDescription": "Specify if you want to highlight the Front Matter in the Markdown file. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.fmhighlight)",
"scope": "Content"
},
"frontMatter.content.pageFolders": {
@@ -197,7 +207,7 @@
"frontMatter.content.sorting": {
"type": "array",
"default": [],
"markdownDescription": "Define the sorting options for your dashboard content. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.sorting)",
"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": {
@@ -243,7 +253,7 @@
"frontMatter.content.wysiwyg": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies if you want to enable/disable the What You See, Is What You Get (WYSIWYG) markdown controls. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.content.wysiwyg)",
"markdownDescription": "Specifies if you want to enable/disable the What You See, Is What You Get (WYSIWYG) markdown controls. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.content.wysiwyg)",
"scope": "Content"
},
"frontMatter.custom.scripts": {
@@ -303,7 +313,7 @@
"frontMatter.dashboard.mediaSnippet": {
"type": "array",
"default": [],
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.dashboard.mediaSnippet)",
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
"items": {
"type": "string",
"description": "The parts of your snippet. Use `{mediaUrl}` as placeholder where the path of the image needs to be inserted."
@@ -322,7 +332,7 @@
"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)"
"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.media.defaultSorting": {
"type": "string",
@@ -333,7 +343,7 @@
"FileNameAsc",
"FileNameDesc"
],
"markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.media.sorting.default)",
"markdownDescription": "Specify the default sorting option for the media dashboard. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.media.defaultsorting)",
"scope": "Content"
},
"frontMatter.panel.freeform": {
@@ -357,7 +367,7 @@
"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)",
"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": {
@@ -376,7 +386,7 @@
},
"frontMatter.taxonomy.commaSeparatedFields": {
"type": "array",
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.commaSeparatedFields)",
"markdownDescription": "Specify the fields names that Front Matter should treat as a comma-separated array. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.commaSeparatedFields)",
"items": {
"type": "string",
"description": "Name of the fields you want to use as comma-separated arrays."
@@ -388,7 +398,7 @@
"array",
"null"
],
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.taxonomy.contentTypes)",
"markdownDescription": "Specify the type of contents you want to use for your articles/pages/etc. Make sure the `type` is correctly set in your front matter. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.taxonomy.contentTypes)",
"items": {
"type": "object",
"description": "Define the content types you want to use in Front Matter.",
@@ -397,6 +407,15 @@
"type": "string",
"description": "Define the type of field"
},
"fileType": {
"type": "string",
"default": "",
"enum": [
"md",
"mdx"
],
"description": "Specifies the type of content you want to create."
},
"fields": {
"type": "array",
"description": "Define the fields of the content type",
@@ -725,7 +744,7 @@
"warning",
"error"
],
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontMatter.global.notifications)",
"markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)",
"scope": "Templates"
}
}

View File

@@ -15,9 +15,6 @@ import { parseWinPath } from '../helpers/parseWinPath';
export class Article {
private static prevContent = "";
/**
* Insert taxonomy
*
@@ -119,7 +116,37 @@ export class Article {
return;
}
const article = ArticleHelper.getFrontMatter(editor);
const updatedArticle = this.setLastModifiedDateInner(editor.document);
if (typeof updatedArticle === "undefined") {
return;
}
ArticleHelper.update(
editor,
updatedArticle as matter.GrayMatterFile<string>
);
}
public static async setLastModifiedDateOnSave(
document: vscode.TextDocument
): Promise<vscode.TextEdit[]> {
const updatedArticle = this.setLastModifiedDateInner(document);
if (typeof updatedArticle === "undefined") {
return [];
}
const update = ArticleHelper.generateUpdate(document, updatedArticle);
return [update];
}
private static setLastModifiedDateInner(
document: vscode.TextDocument
): matter.GrayMatterFile<string> | undefined {
const article = ArticleHelper.getFrontMatterFromDocument(document);
if (!article) {
return;
}
@@ -128,8 +155,7 @@ export class Article {
const dateField = Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
try {
cloneArticle.data[dateField] = Article.formatDate(new Date());
ArticleHelper.update(editor, cloneArticle);
return cloneArticle;
} catch (e: any) {
Notifications.error(`Something failed while parsing the date format. Check your "${CONFIG_KEY}${SETTING_DATE_FORMAT}" setting.`);
}
@@ -238,30 +264,17 @@ export class Article {
/**
* Article auto updater
* @param fileChanges
* @param event
*/
public static async autoUpdate(fileChanges: vscode.TextDocumentChangeEvent) {
const txtChanges = fileChanges.contentChanges.map(c => c.text);
const editor = vscode.window.activeTextEditor;
public static async autoUpdate(event: vscode.TextDocumentWillSaveEvent) {
const document = event.document;
if (document && ArticleHelper.isMarkdownFile(document)) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (txtChanges.length > 0 && editor && ArticleHelper.isMarkdownFile()) {
const autoUpdate = Settings.get(SETTING_AUTO_UPDATE_DATE);
if (autoUpdate) {
const article = ArticleHelper.getFrontMatter(editor);
if (!article) {
return;
}
if (article.content === Article.prevContent) {
return;
}
Article.prevContent = article.content;
Article.setLastModifiedDate();
if (autoUpdate) {
event.waitUntil(Article.setLastModifiedDateOnSave(document));
}
}
}
}
/**

View File

@@ -12,7 +12,7 @@ import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
import { MediaHelpers } from '../helpers/MediaHelpers';
import { MediaListener } from '../listeners';
import { MediaListener, PagesListener } from '../listeners';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
@@ -288,6 +288,9 @@ export class Folders {
}));
await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, folderDetails, true);
// Reinitialize the folder listeners
PagesListener.startWatchers();
}
/**

View File

@@ -5,6 +5,7 @@ import { Notifications } from "../helpers/Notifications";
import { Template } from "./Template";
import { Folders } from "./Folders";
import { Settings } from "../helpers";
import { SETTINGS_CONTENT_DEFAULT_FILETYPE } from "../constants";
export class Project {
@@ -27,6 +28,7 @@ categories: []
public static async init(sampleTemplate: boolean = true) {
try {
Settings.createTeamSettings();
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
const folder = Template.getSettings();
const templatePath = Project.templatePath();
@@ -35,7 +37,7 @@ categories: []
return;
}
const article = Uri.file(join(templatePath.fsPath, "article.md"));
const article = Uri.file(join(templatePath.fsPath, `article.${fileType}`));
if (!fs.existsSync(templatePath.fsPath)) {
await workspace.fs.createDirectory(templatePath);

View File

@@ -2,7 +2,7 @@ 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 { SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTING_TEMPLATES_FOLDER, SETTING_TEMPLATES_PREFIX } from '../constants';
import { ArticleHelper, Settings } from '../helpers';
import { Article } from '.';
import { Notifications } from '../helpers/Notifications';
@@ -12,6 +12,7 @@ import { Folders } from './Folders';
import { ContentType } from '../helpers/ContentType';
import { ContentType as IContentType } from '../models';
import { PagesListener } from '../listeners';
import { extname } from 'path';
export class Template {
@@ -50,6 +51,7 @@ export class Template {
public static async generate() {
const folder = Template.getSettings();
const editor = vscode.window.activeTextEditor;
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
if (folder && editor && ArticleHelper.isMarkdownFile()) {
const article = ArticleHelper.getFrontMatter(editor);
@@ -83,7 +85,7 @@ export class Template {
if (templatePath) {
let fileContents = ArticleHelper.stringifyFrontMatter(keepContents === "no" ? "" : clonedArticle.content, clonedArticle.data);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.md`);
const templateFile = path.join(templatePath.fsPath, `${titleValue}.${fileType}`);
fs.writeFileSync(templateFile, fileContents, { encoding: "utf-8" });
Notifications.info(`Template created and is now available in your ${folder} folder.`);
@@ -140,7 +142,8 @@ export class Template {
contentType = contentTypes?.find(t => t.name === templateData.data.type);
}
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue);
const fileExtension = extname(template.fsPath).replace(".", "");
let newFilePath: string | undefined = ArticleHelper.createContent(contentType, folderPath, titleValue, fileExtension);
if (!newFilePath) {
return;
}

View File

@@ -50,6 +50,8 @@ export const SETTINGS_CONTENT_WYSIWYG = "content.wysiwyg";
export const SETTINGS_CONTENT_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_MEDIA_SORTING_DEFAULT = "content.defaultSorting";
export const SETTINGS_CONTENT_DEFAULT_FILETYPE = "content.defaultFileType";
export const SETTINGS_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
export const SETTINGS_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";

View File

@@ -26,7 +26,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
if (view === DashboardViewType.Grid) {
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100`}
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50`}
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">
{

View File

@@ -1,28 +1,32 @@
import {CollectionIcon} from '@heroicons/react/outline';
import { CollectionIcon } from '@heroicons/react/outline';
import { basename, join } from 'path';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Sorting } from '.';
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { NavigationType } from '../../models';
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
import { SearchAtom, 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 [ , setSearchValue ] = useRecoilState(SearchAtom);
const [ folders, setFolders ] = React.useState<string[]>([]);
const settings = useRecoilValue(SettingsAtom);
if (!settings?.wsFolder) {
return null;
const updateFolder = (folder: string) => {
setSearchValue('');
setSelectedFolder(folder);
}
React.useEffect(() => {
if (!settings) {
return;
}
const { wsFolder, staticFolder, contentFolders } = settings;
const isValid = (folderPath: string) => {
const isValid = (folderPath: string) => {
if (staticFolder) {
const staticPath = parseWinPath(join(wsFolder, staticFolder)) as string;
const relPath = folderPath.replace(staticPath, '') as string;
@@ -33,7 +37,7 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
return false;
}
}
1
for (let i = 0; i < contentFolders.length; i++) {
const folder = contentFolders[i];
const contentFolder = parseWinPath(folder.path) as string;
@@ -62,45 +66,40 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
setFolders(allFolders);
}
}, [selectedFolder]);
}, [selectedFolder, settings]);
return (
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
<ol role="list" className="flex space-x-4 px-5">
<li className="flex">
<ol role="list" className="flex space-x-4 px-5 flex-1">
<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">
<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>
<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={() => updateFolder(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>
{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>
<div className={`flex px-5`}>
<Sorting view={NavigationType.Media} disableCustomSorting />
</div>
</nav>
))}
</ol>
);
};

View File

@@ -15,9 +15,9 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import { ClearFilters } from './ClearFilters';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import {PhotographIcon} from '@heroicons/react/outline';
import { Pagination } from '../Media/Pagination';
import { MediaHeaderTop } from '../Media/MediaHeaderTop';
import { ChoiceButton } from '../ChoiceButton';
import { Breadcrumb } from './Breadcrumb';
import { MediaHeaderBottom } from '../Media/MediaHeaderBottom';
export interface IHeaderProps {
settings: Settings | null;
@@ -54,16 +54,20 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
return (
<div className={`w-full sticky top-0 z-40 bg-gray-100 dark:bg-vulcan-500`}>
<div className={`px-4 bg-gray-50 dark:bg-vulcan-50 border-b-2 border-gray-200 dark:border-vulcan-200`}>
<div className={`flex items-center justify-start`}>
<button className={`p-2 flex items-center ${view === "contents" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => updateView(NavigationType.Contents)}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
<button className={`p-2 flex items-center ${view === "media" ? "bg-gray-200 dark:bg-vulcan-200" : ""} hover:bg-gray-100 dark:hover:bg-vulcan-100`} onClick={() => updateView(NavigationType.Media)}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</div>
<div className="mb-0 border-b bg-gray-100 dark:bg-vulcan-500 border-gray-200 dark:border-vulcan-300 h-12">
<ul className="flex items-center justify-start h-full -mb-px" data-tabs-toggle="#myTabContent" role="tablist">
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Contents ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="profile" aria-selected="false" onClick={() => updateView(NavigationType.Contents)}>
<MarkdownIcon className={`h-6 w-auto mr-2`} /><span>Contents</span>
</button>
</li>
<li className="mr-2" role="presentation">
<button className={`flex items-center py-2 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300 ${view === NavigationType.Media ? "border-vulcan-500 text-vulcan-500 dark:border-whisper-500 dark:text-whisper-500" : "text-gray-500 dark:text-gray-400"}`} type="button" role="tab" aria-controls="dashboard" aria-selected="true" onClick={() => updateView(NavigationType.Media)}>
<PhotographIcon className={`h-6 w-auto mr-2`} /><span>Media</span>
</button>
</li>
</ul>
</div>
{
@@ -101,7 +105,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
</div>
</div>
<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`}>
<div className={`py-4 px-5 w-full flex items-center justify-between lg:justify-end bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100 space-x-4 lg:space-x-6 xl:space-x-8`}>
<ClearFilters />
<Folders />
@@ -121,8 +125,9 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
{
view === NavigationType.Media && (
<>
<Pagination />
<Breadcrumb />
<MediaHeaderTop />
<MediaHeaderBottom />
</>
)
}

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { LIMIT } from '../../hooks/useMedia';
import { MediaTotalSelector, PageAtom } from '../../state';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export const Pagination: React.FunctionComponent<IPaginationProps> = (props: React.PropsWithChildren<IPaginationProps>) => {
const [ page, setPage ] = useRecoilState(PageAtom);
const totalMedia = useRecoilValue(MediaTotalSelector);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const getButtons = (): number[] => {
const maxButtons = 5;
const buttons: number[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
buttons.push(i);
}
}
return buttons;
};
return (
<div className="flex justify-between items-center sm:justify-end space-x-2 text-sm">
<PaginationButton
title="First"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0)
}
}} />
<PaginationButton
title="Previous"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(page - 1)
}
}} />
{getButtons().map((button) => (
<button
key={button}
disabled={button === page}
onClick={() => {
setPage(button)
}
}
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'} max-h-8`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
</div>
);
};

View File

@@ -0,0 +1,46 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaTotalSelector, PageAtom, SearchAtom, SelectedMediaFolderSelector } from '../../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { RefreshIcon } from '@heroicons/react/outline';
import { LIMIT } from '../../hooks/useMedia';
export interface IPaginationStatusProps {}
export const PaginationStatus: React.FunctionComponent<IPaginationStatusProps> = (props: React.PropsWithChildren<IPaginationStatusProps>) => {
const totalMedia = useRecoilValue(MediaTotalSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ , setSearch ] = useRecoilState(SearchAtom);
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
return mediaItems;
};
const refresh = () => {
setPage(0);
setSearch('');
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
return (
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>
);
};

View File

@@ -4,34 +4,43 @@ import { useRecoilState } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { SearchAtom } from '../../state';
export interface ISearchboxProps {}
export interface ISearchboxProps {
placeholder?: string;
}
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({}: React.PropsWithChildren<ISearchboxProps>) => {
export const Searchbox: React.FunctionComponent<ISearchboxProps> = ({placeholder}: React.PropsWithChildren<ISearchboxProps>) => {
const [ value, setValue ] = React.useState('');
const [ , setDebounceValue ] = useRecoilState(SearchAtom);
const [ debounceSearchValue, setDebounceValue ] = useRecoilState(SearchAtom);
const debounceSearch = useDebounce<string>(value, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
React.useEffect(() => {
if (!debounceSearchValue && value) {
setValue('');
}
} , [debounceSearchValue]);
React.useEffect(() => {
setDebounceValue(debounceSearch);
}, [debounceSearch]);
return (
<div className="flex space-x-4">
<div className="flex-1 min-w-0">
<div className="flex space-x-4 flex-1">
<div className="min-w-0">
<label htmlFor="search" className="sr-only">Search</label>
<div className="relative flex justify-center">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="search"
name="search"
className={`block w-full py-2 pl-10 pr-3 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`}
placeholder="Search"
placeholder={placeholder || "Search"}
value={value}
onChange={handleChange}
/>

View File

@@ -78,7 +78,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
key={option.id}
title={option.title || option.name}
value={option}
isCurrent={option.id === crntSorting?.id}
isCurrent={option.id === crntSort.id}
onClick={(value) => updateSorting(value)} />
))}
</MenuItems>

View File

@@ -26,24 +26,28 @@ export const FolderCreation: React.FunctionComponent<IFolderCreationProps> = (pr
const scripts = (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFolder);
if (scripts.length > 0) {
return (
<ChoiceButton
title={`Create new folder`}
choices={scripts.map(s => ({
title: s.title,
onClick: () => runCustomScript(s)
}))}
onClick={onFolderCreation}
disabled={!settings?.initialized} />
<div className="flex flex-1 justify-end">
<ChoiceButton
title={`Create new folder`}
choices={scripts.map(s => ({
title: s.title,
onClick: () => runCustomScript(s)
}))}
onClick={onFolderCreation}
disabled={!settings?.initialized} />
</div>
)
}
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>
<div className="flex flex-1 justify-end">
<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>
</div>
);
};

View File

@@ -16,11 +16,13 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder,
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`} />
<li className={`group relative 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-row items-center h-full`} onClick={() => setSelectedFolder(folder)}>
<div>
<FolderIcon className={`h-12 w-12 mr-4`} />
</div>
<p className="text-sm font-bold pointer-events-none flex items-center">
<p className="text-sm font-bold pointer-events-none flex items-center text-left overflow-hidden break-words">
{basename(relFolderPath)}
</p>
</button>

View File

@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import {PhotographIcon} from '@heroicons/react/outline';
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
@@ -15,6 +15,7 @@ import { MenuItem, MenuItems } from '../Menu';
import { Alert } from '../Modals/Alert';
import { Metadata } from '../Modals/Metadata';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
export interface IItemProps {
media: MediaInfo;
@@ -194,7 +195,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50">
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
@@ -206,11 +207,56 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center">
<Menu as="div" className="relative z-10 inline-block text-left">
<div className="flex items-center border border-transparent group-hover:bg-gray-50 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:inline-block h-5'>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
<PencilIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
{
viewData?.data?.filePath ? (
<>
<QuickAction
title='Insert image with markdown markup'
onClick={insertToArticle}>
<PlusIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
{
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
<QuickAction
title='Insert snippet'
onClick={insertSnippet}>
<CodeIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
)
}
</>
) : (
<>
<QuickAction
title='Copy media path'
onClick={copyToClipboard}>
<ClipboardIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Delete media file'
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
</>
)
}
</div>
<Menu as="div" className="relative z-10 inline-block text-left h-5">
<MenuButton title={`Menu`} />
<MenuItems>
<MenuItems widthClass='w-40'>
<MenuItem
title={`Edit metadata`}
onClick={updateMetadata}

View File

@@ -1,10 +1,14 @@
import * as React from 'react';
export interface IListProps {}
export interface IListProps {
gap?: number;
}
export const List: React.FunctionComponent<IListProps> = ({gap, children}: React.PropsWithChildren<IListProps>) => {
const gapClass = gap !== undefined ? `gap-y-${gap}` : `gap-y-8`;
export const List: React.FunctionComponent<IListProps> = ({children}: React.PropsWithChildren<IListProps>) => {
return (
<ul role="list" className="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
<ul role="list" className={`grid grid-cols-2 gap-x-4 ${gapClass} sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
{children}
</ul>
);

View File

@@ -17,19 +17,17 @@ import { useCallback } from 'react';
import { DashboardMessage } from '../../DashboardMessage';
import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMatterIcon';
import { FolderItem } from './FolderItem';
import useMedia from '../../hooks/useMedia';
export interface IMediaProps {}
export const LIMIT = 16;
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
const { media } = useMedia();
const settings = useRecoilValue(SettingsSelector);
const [ selectedFolder, setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ folders, setFolders ] = useRecoilState(MediaFoldersAtom);
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
const viewData = useRecoilValue(ViewDataSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderAtom);
const folders = useRecoilValue(MediaFoldersAtom);
const loading = useRecoilValue(LoadingAtom);
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach((file) => {
@@ -52,25 +50,6 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
onDrop,
accept: 'image/*'
});
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(data.media);
setTotal(data.total);
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
}
};
React.useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, ['']);
return (
<div className="flex flex-col h-full overflow-auto">
@@ -113,7 +92,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List>
<List gap={0}>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { NavigationType } from '../../models/NavigationType';
import { SettingsAtom } from '../../state';
import { Sorting } from '../Header';
import { Breadcrumb } from '../Header/Breadcrumb';
import { Pagination } from '../Header/Pagination';
export interface IMediaHeaderBottomProps {}
export const MediaHeaderBottom: React.FunctionComponent<IMediaHeaderBottomProps> = (props: React.PropsWithChildren<IMediaHeaderBottomProps>) => {
const settings = useRecoilValue(SettingsAtom);
if (!settings?.wsFolder) {
return null;
}
return (
<nav className="w-full bg-gray-200 text-vulcan-300 dark:bg-vulcan-400 dark:text-whisper-600 border-b border-gray-300 dark:border-vulcan-100 flex justify-between py-2" aria-label="Breadcrumb">
<Breadcrumb />
<Pagination />
<div className={`flex px-5 flex-1 justify-end`}>
<Sorting view={NavigationType.Media} disableCustomSorting />
</div>
</nav>
);
};

View File

@@ -0,0 +1,81 @@
import { EventData } from '@estruyf/vscode';
import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { usePrevious } from '../../../panelWebView/hooks/usePrevious';
import { DashboardCommand } from '../../DashboardCommand';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state';
import { Searchbox } from '../Header';
import { PaginationStatus } from '../Header/PaginationStatus';
import { FolderCreation } from './FolderCreation';
export interface IMediaHeaderTopProps {}
export const MediaHeaderTop: React.FunctionComponent<IMediaHeaderTopProps> = ({}: React.PropsWithChildren<IMediaHeaderTopProps>) => {
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(null);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const crntSorting = useRecoilValue(SortingSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = useRecoilState(PageAtom);
const settings = useRecoilValue(SettingsSelector);
const debounceGetMedia = useDebounce<string | null>(lastUpdated, 200);
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
const mediaUpdate = (message: MessageEvent<EventData<{ key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.mediaUpdate) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}
React.useEffect(() => {
if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) {
setLoading(true);
setPage(0);
setLastUpdated(new Date().getTime().toString());
}
}, [selectedFolder]);
React.useEffect(() => {
setLastUpdated(new Date().getTime().toString());
}, [crntSorting]);
React.useEffect(() => {
if (debounceGetMedia) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}, [debounceGetMedia]);
React.useEffect(() => {
Messenger.listen(mediaUpdate);
return () => {
Messenger.unlisten(mediaUpdate);
}
}, []);
return (
<nav
className="py-3 px-4 flex items-center justify-between border-b border-gray-300 dark:border-vulcan-100"
aria-label="Pagination"
>
<Searchbox placeholder={`Search in folder`} />
<PaginationStatus />
<FolderCreation />
</nav>
);
};

View File

@@ -1,169 +0,0 @@
import { EventData } from '@estruyf/vscode';
import { Messenger } from '@estruyf/vscode/dist/client';
import {RefreshIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebounce } from '../../../hooks/useDebounce';
import { usePrevious } from '../../../panelWebView/hooks/usePrevious';
import { DashboardCommand } from '../../DashboardCommand';
import { DashboardMessage } from '../../DashboardMessage';
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state';
import { FolderCreation } from './FolderCreation';
import { LIMIT } from './Media';
import { PaginationButton } from './PaginationButton';
export interface IPaginationProps {}
export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.PropsWithChildren<IPaginationProps>) => {
const [ lastUpdated, setLastUpdated ] = React.useState<string | null>(null);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const crntSorting = useRecoilValue(SortingSelector);
const totalMedia = useRecoilValue(MediaTotalSelector);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const [ page, setPage ] = useRecoilState(PageAtom);
const settings = useRecoilValue(SettingsSelector);
const debounceGetMedia = useDebounce<string | null>(lastUpdated, 200);
const prevSelectedFolder = usePrevious<string | null>(selectedFolder);
const totalPages = Math.ceil(totalMedia / LIMIT) - 1;
const getTotalPage = () => {
const mediaItems = ((page + 1) * LIMIT);
if (totalMedia < mediaItems) {
return totalMedia;
}
return mediaItems;
};
// Write me function to retrieve buttons before and after current page
const getButtons = (): number[] => {
const maxButtons = 5;
const buttons: number[] = [];
const start = page - maxButtons;
const end = page + maxButtons;
for (let i = start; i <= end; i++) {
if (i >= 0 && i <= totalPages) {
buttons.push(i);
}
}
return buttons;
};
const refresh = () => {
setPage(0);
Messenger.send(DashboardMessage.refreshMedia, { folder: selectedFolder });
}
const mediaUpdate = (message: MessageEvent<EventData<{ key: string, value: any }>>) => {
if (message.data.command === DashboardCommand.mediaUpdate) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}
React.useEffect(() => {
setLastUpdated(new Date().getTime().toString());
}, [page]);
React.useEffect(() => {
if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) {
setLoading(true);
setPage(0);
setLastUpdated(new Date().getTime().toString());
}
}, [selectedFolder]);
React.useEffect(() => {
setLastUpdated(new Date().getTime().toString());
}, [crntSorting]);
React.useEffect(() => {
if (debounceGetMedia) {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
}
}, [debounceGetMedia]);
React.useEffect(() => {
Messenger.listen(mediaUpdate);
return () => {
Messenger.unlisten(mediaUpdate);
}
}, []);
return (
<nav
className="py-4 px-5 flex items-center justify-between bg-gray-200 border-b border-gray-300 dark:bg-vulcan-400 dark:border-vulcan-100"
aria-label="Pagination"
>
<div className="hidden sm:flex">
<button className={`mr-2 text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500`}
title="Refresh media"
onClick={refresh}>
<RefreshIcon className={`h-5 w-5`} />
<span className="sr-only">Refresh media</span>
</button>
<p className="text-sm text-gray-500 dark:text-whisper-900">
Showing <span className="font-medium">{(page * LIMIT) + 1}</span> to <span className="font-medium">{getTotalPage()}</span> of{' '}
<span className="font-medium">{totalMedia}</span> results
</p>
</div>
<FolderCreation />
<div className="flex justify-between sm:justify-end space-x-2 text-sm">
<PaginationButton
title="First"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(0)
}
}} />
<PaginationButton
title="Previous"
disabled={page === 0}
onClick={() => {
if (page > 0) {
setPage(page - 1)
}
}} />
{getButtons().map((button) => (
<button
key={button}
disabled={button === page}
onClick={() => {
setPage(button)
}
}
className={`${page === button ? 'bg-gray-200 px-2 text-vulcan-500' : 'text-gray-500 hover:text-gray-600 dark:text-whisper-900 dark:hover:text-whisper-500'}`}
>{button + 1}</button>
))}
<PaginationButton
title="Next"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)} />
<PaginationButton
title="Last"
disabled={page >= totalPages}
onClick={() => setPage(totalPages)} />
</div>
</nav>
);
};

View File

@@ -0,0 +1,19 @@
import * as React from 'react';
export interface IQuickActionProps {
title: string;
onClick: () => void;
}
export const QuickAction: React.FunctionComponent<IQuickActionProps> = ({title, onClick, children}: React.PropsWithChildren<IQuickActionProps>) => {
return (
<button
type='button'
title={title}
onClick={onClick}
className={`px-2 group inline-flex justify-center text-sm font-medium text-vulcan-400 hover:text-vulcan-600 dark:text-gray-400 dark:hover:text-whisper-600`}>
{children}
<span className='sr-only'>{title}</span>
</button>
);
};

View File

@@ -15,7 +15,7 @@ export const MenuItem: React.FunctionComponent<IMenuItemProps> = ({title, value,
<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 disabled:bg-gray-500`}
className={`${!isCurrent ? `font-normal` : `font-bold`} text-gray-500 dark:text-whisper-900 block px-4 py-2 text-sm 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>

View File

@@ -17,7 +17,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<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`}>
<Menu.Items className={`${widthClass || ""} 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>

View File

@@ -4,7 +4,7 @@ export interface ISpinnerProps {}
export const Spinner: React.FunctionComponent<ISpinnerProps> = (props: React.PropsWithChildren<ISpinnerProps>) => {
return (
<div className={`fixed top-0 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className={`fixed top-12 left-0 right-0 bottom-0 w-full h-full flex flex-wrap items-center justify-center bg-white bg-opacity-10 z-50`}>
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-50 h-32 w-32" />
</div>
);

View File

@@ -21,7 +21,7 @@ export const Startup: React.FunctionComponent<IStartupProps> = ({settings}: Reac
}, [settings?.openOnStart]);
return (
<div className={`relative flex items-start`}>
<div className={`relative flex items-start ml-4`}>
<div className="flex items-center h-5">
<input
id="startup"

View File

@@ -0,0 +1,75 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { useState, useEffect, useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../models';
import { DashboardCommand } from '../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, PageAtom, SearchAtom, SearchSelector, SelectedMediaFolderAtom } from '../state';
import Fuse from 'fuse.js';
const fuseOptions: Fuse.IFuseOptions<MediaInfo> = {
keys: [
{ name: 'filename', weight: 0.8 },
{ name: 'fsPath', weight: 0.5 },
{ name: 'caption', weight: 0.5 },
{ name: 'alt', weight: 0.5 }
],
threshold: 0.2,
includeScore: true
};
export const LIMIT = 16;
export default function useMedia() {
const [ media, setMedia ] = useState<MediaInfo[]>([]);
const [ page, setPage ] = useRecoilState(PageAtom);
const [ searchedMedia, setSearchedMedia ] = useState<MediaInfo[]>([]);
const [ , setSelectedFolder ] = useRecoilState(SelectedMediaFolderAtom);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ , setLoading ] = useRecoilState(LoadingAtom);
const search = useRecoilValue(SearchAtom);
const getMedia = useCallback(() => {
return searchedMedia.slice(page * LIMIT, ((page + 1) * LIMIT));
}, [searchedMedia, page]);
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(data.media);
setTotal(data.total);
setFolders(data.folders);
setSelectedFolder(data.selectedFolder);
setSearchedMedia(data.media);
}
};
useEffect(() => {
if (search) {
const fuse = new Fuse(media, fuseOptions);
const results = fuse.search(search);
const newSearchedMedia = results.map(page => page.item);
setSearchedMedia(newSearchedMedia);
setTotal(results.length);
return;
}
setSearchedMedia(media);
}, [search]);
useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
return () => {
Messenger.unlisten(messageListener);
}
}, []);
return {
media: getMedia()
};
}

View File

@@ -18,6 +18,7 @@ import { Content } from './commands/Content';
import ContentProvider from './providers/ContentProvider';
import { Wysiwyg } from './commands/Wysiwyg';
import { Diagnostics } from './commands/Diagnostics';
import { PagesListener } from './listeners';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -40,6 +41,10 @@ export async function activate(context: vscode.ExtensionContext) {
SettingsHelper.checkToPromote();
// Start listening to the folders for content changes.
// This will make sure the dashboard is up to date
PagesListener.startWatchers();
collection = vscode.languages.createDiagnosticCollection('frontMatter');
// Pages dashboard
@@ -172,8 +177,7 @@ export async function activate(context: vscode.ExtensionContext) {
triggerShowDraftStatus();
// Listener for file edit changes
editDebounce = debounceCallback();
subscriptions.push(vscode.workspace.onDidChangeTextDocument(triggerFileChange));
subscriptions.push(vscode.workspace.onWillSaveTextDocument(handleAutoDateUpdate));
// Listener for file saves
subscriptions.push(vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => {
@@ -226,8 +230,8 @@ export async function activate(context: vscode.ExtensionContext) {
export function deactivate() {}
const triggerFileChange = (e: vscode.TextDocumentChangeEvent) => {
editDebounce(() => Article.autoUpdate(e), 1000);
const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
Article.autoUpdate(e);
};
const triggerShowDraftStatus = () => {

View File

@@ -3,7 +3,7 @@ import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/
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, SETTING_TEMPLATES_PREFIX } from '../constants';
import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, 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 { Extension, Settings } from '.';
@@ -27,8 +27,17 @@ export class ArticleHelper {
* @param editor
*/
public static getFrontMatter(editor: vscode.TextEditor) {
const fileContents = editor.document.getText();
return ArticleHelper.parseFile(fileContents, editor.document.fileName);
return ArticleHelper.getFrontMatterFromDocument(editor.document);
}
/**
* Get the contents of the specified document
*
* @param document The document to parse.
*/
public static getFrontMatterFromDocument(document: vscode.TextDocument) {
const fileContents = document.getText();
return ArticleHelper.parseFile(fileContents, document.fileName);
}
/**
@@ -47,11 +56,37 @@ export class ArticleHelper {
* @param article
*/
public static async update(editor: vscode.TextEditor, article: matter.GrayMatterFile<string>) {
const update = this.generateUpdate(editor.document, article);
await editor.edit(builder => builder.replace(update.range, update.newText));
}
/**
* Generate the update to be applied to the article.
* @param article
*/
public static generateUpdate(document: vscode.TextDocument, article: matter.GrayMatterFile<string>): vscode.TextEdit {
const nrOfLines = document.lineCount as number;
const range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(nrOfLines, 0));
const removeQuotes = Settings.get(SETTING_REMOVE_QUOTES) as string[];
const commaSeparated = Settings.get<string[]>(SETTING_COMMA_SEPARATED_FIELDS);
// Check if there is a line ending
const lines = article.content.split("\n");
const lastLine = lines.pop();
const endsWithNewLine = lastLine !== undefined && lastLine.trim() === "";
let newMarkdown = this.stringifyFrontMatter(article.content, Object.assign({}, article.data));
// Logic to not include a new line at the end of the file
if (!endsWithNewLine) {
const lines = newMarkdown.split("\n");
const lastLine = lines.pop();
if (lastLine !== undefined && lastLine?.trim() === "") {
newMarkdown = lines.join("\n");
}
}
// Check for field where quotes need to be removed
if (removeQuotes && removeQuotes.length) {
for (const toRemove of removeQuotes) {
@@ -68,8 +103,7 @@ export class ArticleHelper {
}
}
const nrOfLines = editor.document.lineCount as number;
await editor.edit(builder => builder.replace(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(nrOfLines, 0)), newMarkdown));
return vscode.TextEdit.replace(range, newMarkdown);
}
/**
@@ -109,9 +143,25 @@ export class ArticleHelper {
/**
* Checks if the current file is a markdown file
*/
public static isMarkdownFile() {
const editor = vscode.window.activeTextEditor;
return (editor && editor.document && (editor.document.languageId.toLowerCase() === "markdown" || editor.document.languageId.toLowerCase() === "mdx"));
public static isMarkdownFile(document: vscode.TextDocument | undefined | null = null) {
const supportedLanguages = ["markdown", "mdx"];
const supportedFileExtensions = [".md", ".mdx"];
const languageId = document?.languageId?.toLowerCase();
const isSupportedLanguage = languageId && supportedLanguages.includes(languageId);
document ??= vscode.window.activeTextEditor?.document;
/**
* It's possible that the file is a file type we support but the user hasn't installed
* language support for. In that case, we'll manually check the extension as a proxy
* for whether or not we support the file.
*/
if (!isSupportedLanguage) {
const fileName = document?.fileName?.toLowerCase();
return fileName && supportedFileExtensions.findIndex(fileExtension => fileName.endsWith(fileExtension)) > -1;
}
return isSupportedLanguage;
}
/**
@@ -188,8 +238,9 @@ export class ArticleHelper {
* @param titleValue
* @returns The new file path
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string): string | undefined {
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
const prefix = Settings.get<string>(SETTING_TEMPLATES_PREFIX);
const fileType = Settings.get<string>(SETTINGS_CONTENT_DEFAULT_FILETYPE);
// Name of the file or folder to create
const sanitizedName = ArticleHelper.sanitize(titleValue);
@@ -203,10 +254,10 @@ export class ArticleHelper {
return;
} else {
mkdirSync(newFolder);
newFilePath = join(newFolder, `index.md`);
newFilePath = join(newFolder, `index.${fileExtension || contentType.fileType || fileType}`);
}
} else {
let newFileName = `${sanitizedName}.md`;
let newFileName = `${sanitizedName}.${fileExtension || contentType?.fileType || fileType}`;
if (prefix && typeof prefix === "string") {
newFileName = `${format(new Date(), DateHelper.formatUpdate(prefix) as string)}-${newFileName}`;

View File

@@ -2,7 +2,7 @@ import { PagesListener } from './../listeners/PagesListener';
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 { Uri, workspace, window, commands } from 'vscode';
import { Folders } from "../commands/Folders";
import { Questions } from "./Questions";
import { writeFileSync } from "fs";
@@ -91,6 +91,12 @@ export class ContentType {
return Settings.get<IContentType[]>(SETTING_TAXONOMY_CONTENT_TYPES);
}
/**
* Create a new file with the specified content type
* @param contentType
* @param folderPath
* @returns
*/
private static async create(contentType: IContentType, folderPath: string) {
const titleValue = await Questions.ContentTitle();
@@ -123,10 +129,7 @@ export class ContentType {
writeFileSync(newFilePath, content, { encoding: "utf8" });
const txtDoc = await workspace.openTextDocument(Uri.parse(newFilePath));
if (txtDoc) {
window.showTextDocument(txtDoc);
}
await commands.executeCommand('vscode.open', Uri.file(newFilePath));
Notifications.info(`Your new content has been created.`);

View File

@@ -38,7 +38,7 @@ export class MediaHelpers {
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
// Support for page bundles
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
if (viewData?.data?.filePath && (viewData?.data?.filePath.endsWith('index.md') || viewData?.data?.filePath.endsWith('index.mdx'))) {
const folderPath = parse(viewData.data.filePath).dir;
selectedFolder = folderPath;
} else if (stateValue && existsSync(stateValue)) {
@@ -111,7 +111,6 @@ export class MediaHelpers {
const total = files.length;
// Get media set
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => {
try {
const metadata = MediaLibrary.getInstance().get(file.fsPath);
@@ -283,7 +282,15 @@ export class MediaHelpers {
}
}
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`));
const selection = editor?.selection;
await editor?.edit(builder => {
const snippet = data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`;
if (selection !== undefined) {
builder.replace(selection, snippet);
} else {
builder.insert(new Position(line, character), snippet);
}
});
}
panel.getMediaSelection();
} else {
@@ -313,8 +320,9 @@ export class MediaHelpers {
private static filterMedia(files: Uri[]) {
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext.toLowerCase());
}).map((file) => ({
filename: basename(file.fsPath),
fsPath: file.fsPath,
vsPath: Dashboard.getWebview()?.asWebviewUri(file).toString(),
stats: undefined

View File

@@ -27,11 +27,12 @@ export class MediaLibrary {
workspace.onDidRenameFiles(e => {
e.files.forEach(f => {
const path = f.oldUri.path.toLowerCase();
// 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')) {
if (path.endsWith('.jpeg') ||
path.endsWith('.jpg') ||
path.endsWith('.png') ||
path.endsWith('.gif')) {
this.rename(f.oldUri.fsPath, f.newUri.fsPath);
MediaHelpers.resetMedia();
}

View File

@@ -1,7 +1,7 @@
import { isValidFile } from './../helpers/isValidFile';
import { existsSync } from "fs";
import { dirname, join } from "path";
import { commands, Uri } from "vscode";
import { commands, FileSystemWatcher, RelativePattern, Uri, workspace } from "vscode";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../constants";
@@ -16,6 +16,35 @@ import { BaseListener } from "./BaseListener";
export class PagesListener extends BaseListener {
private static watchers: { [path: string]: FileSystemWatcher } = {};
/**
* Start watching the folders in the current workspace for content changes
*/
public static async startWatchers() {
const folders = Folders.get();
if (!folders || folders.length === 0) {
return;
}
// Dispose all the current watchers
const paths = Object.keys(this.watchers);
for (const path of paths) {
const watcher = this.watchers[path];
watcher.dispose();
delete this.watchers[path];
}
// Recreate all the watchers
for (const folder of folders) {
const folderUri = Uri.parse(folder.path);
let watcher = workspace.createFileSystemWatcher(new RelativePattern(folderUri, "*"));
watcher.onDidCreate(async (uri: Uri) => this.getPagesData);
watcher.onDidDelete(async (uri: Uri) => this.getPagesData);
this.watchers[folderUri.fsPath] = watcher;
}
}
/**
* Process the messages for the dashboard views

View File

@@ -8,6 +8,7 @@ export interface MediaPaths {
}
export interface MediaInfo {
filename: string;
fsPath: string;
vsPath: string | undefined;
dimensions?: ISizeCalculationResult | undefined;

View File

@@ -26,6 +26,7 @@ export interface ContentType {
name: string;
fields: Field[];
fileType?: "md" | "mdx";
previewPath?: string | null;
pageBundle?: boolean;
}

View File

@@ -7,6 +7,9 @@
"es6",
"DOM"
],
"typeRoots": [
"node_modules/@types"
],
"sourceMap": true,
"rootDir": "src",
"strict": true,