mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-03-28 17:42:40 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
511fd48081 | ||
|
|
0039fc1555 | ||
|
|
98044187cd | ||
|
|
a6dcc1ea79 | ||
|
|
32a686227e | ||
|
|
faa74132e5 | ||
|
|
3a847f7e42 | ||
|
|
66c978891e | ||
|
|
2f31230e07 | ||
|
|
c4225c0011 | ||
|
|
4edc7a0280 | ||
|
|
a60fe5204b | ||
|
|
bb980b4afe | ||
|
|
504658d87a | ||
|
|
feff69d969 | ||
|
|
a34d77242a | ||
|
|
1b24c1277d | ||
|
|
e6750205be | ||
|
|
1da8bf3f8b | ||
|
|
90c60b6a40 | ||
|
|
ee79f89c7f | ||
|
|
0668d48fd5 | ||
|
|
d046f73d16 | ||
|
|
f144d713d1 | ||
|
|
d31c403bdc | ||
|
|
35a0327387 | ||
|
|
9b39649bde | ||
|
|
3d857463f0 | ||
|
|
0428642a2f | ||
|
|
5182a9ae1a | ||
|
|
ab3686b3b5 | ||
|
|
c5b7b7845d | ||
|
|
2f13c335ed | ||
|
|
f219ac721f | ||
|
|
0149885289 | ||
|
|
cb80a10de2 | ||
|
|
092eb0fd2a | ||
|
|
24f79d9d3f | ||
|
|
d667b19716 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
49
package.json
49
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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">
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
72
src/dashboardWebView/components/Header/Pagination.tsx
Normal file
72
src/dashboardWebView/components/Header/Pagination.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
46
src/dashboardWebView/components/Header/PaginationStatus.tsx
Normal file
46
src/dashboardWebView/components/Header/PaginationStatus.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
30
src/dashboardWebView/components/Media/MediaHeaderBottom.tsx
Normal file
30
src/dashboardWebView/components/Media/MediaHeaderBottom.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
81
src/dashboardWebView/components/Media/MediaHeaderTop.tsx
Normal file
81
src/dashboardWebView/components/Media/MediaHeaderTop.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
19
src/dashboardWebView/components/Media/QuickAction.tsx
Normal file
19
src/dashboardWebView/components/Media/QuickAction.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
75
src/dashboardWebView/hooks/useMedia.tsx
Normal file
75
src/dashboardWebView/hooks/useMedia.tsx
Normal 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()
|
||||
};
|
||||
}
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
|
||||
@@ -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 || ``));
|
||||
const selection = editor?.selection;
|
||||
await editor?.edit(builder => {
|
||||
const snippet = data.snippet || ``;
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface MediaPaths {
|
||||
}
|
||||
|
||||
export interface MediaInfo {
|
||||
filename: string;
|
||||
fsPath: string;
|
||||
vsPath: string | undefined;
|
||||
dimensions?: ISizeCalculationResult | undefined;
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface ContentType {
|
||||
name: string;
|
||||
fields: Field[];
|
||||
|
||||
fileType?: "md" | "mdx";
|
||||
previewPath?: string | null;
|
||||
pageBundle?: boolean;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"es6",
|
||||
"DOM"
|
||||
],
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
|
||||
Reference in New Issue
Block a user