Merge branch 'dev-refactor' into dev

This commit is contained in:
Elio Struyf
2021-12-15 13:51:01 +01:00
40 changed files with 16676 additions and 7651 deletions
+15571 -6901
View File
File diff suppressed because it is too large Load Diff
+12 -4
View File
@@ -3,7 +3,7 @@
"displayName": "Front Matter",
"description": "An essential Visual Studio Code extension when you want to manage the markdown pages of your static site like: Hugo, Jekyll, Hexo, NextJs, Gatsby, and many more...",
"icon": "assets/frontmatter-teal-128x128.png",
"version": "5.7.0",
"version": "5.8.0",
"preview": false,
"publisher": "eliostruyf",
"galleryBanner": {
@@ -797,6 +797,15 @@
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.media",
"title": "Open media dashboard",
"category": "Front matter",
"icon": {
"dark": "/assets/icons/frontmatter-small-dark.svg",
"light": "/assets/icons/frontmatter-small-light.svg"
}
},
{
"command": "frontMatter.dashboard.close",
"title": "Close dashboard",
@@ -1137,7 +1146,7 @@
"@types/react": "17.0.0",
"@types/react-datepicker": "^4.1.7",
"@types/react-dom": "17.0.0",
"@types/vscode": "1.51.0",
"@types/vscode": "^1.63.0",
"@vscode/codicons": "0.0.20",
"@vscode/webview-ui-toolkit": "^0.8.1",
"autoprefixer": "^10.3.2",
@@ -1171,6 +1180,5 @@
"wc-react": "github:estruyf/wc-react",
"webpack": "4.44.2",
"webpack-cli": "3.3.12"
},
"dependencies": {}
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { isValidFile } from './../helpers/isValidFile';
import { SETTING_AUTO_UPDATE_DATE, SETTING_MODIFIED_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TEMPLATES_PREFIX, CONFIG_KEY, SETTING_DATE_FORMAT, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX } from './../constants';
import * as vscode from 'vscode';
import { TaxonomyType } from "../models";
@@ -203,7 +204,7 @@ export class Article {
const file = parseWinPath(editor.document.fileName);
if (!file.endsWith(`.md`) && !file.endsWith(`.markdown`) && !file.endsWith(`.mdx`)) {
if (!isValidFile(file)) {
return;
}
+46 -590
View File
@@ -1,42 +1,21 @@
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTING_TAXONOMY_CONTENT_TYPES, DefaultFields, HOME_PAGE_NAVIGATION_ID, ExtensionState, COMMAND_NAME, SETTINGS_FRAMEWORK_ID, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, CONTEXT, SETTING_CUSTOM_SCRIPTS, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_MEDIA_SORTING_DEFAULT } from '../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { basename, dirname, extname, join, parse } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env, Position } from "vscode";
import { Settings as SettingsHelper } from '../helpers';
import { CustomScript as ICustomScript, DraftField, Framework, ScriptType, SortingSetting, SortOrder, SortType, TaxonomyType } from '../models';
import { Folders } from './Folders';
import { PagesListener } from './../listeners/PagesListener';
import { ExtensionListener } from './../listeners/ExtensionListener';
import { SETTINGS_DASHBOARD_OPENONSTART, CONTEXT } from '../constants';
import { join } from "path";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode";
import { Logger, Settings as SettingsHelper } from '../helpers';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { DashboardMessage } from '../dashboardWebView/DashboardMessage';
import { Page } from '../dashboardWebView/models/Page';
import { openFileInEditor } from '../helpers/openFileInEditor';
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Settings } from '../dashboardWebView/models/Settings';
import { Extension } from '../helpers/Extension';
import { ViewType } from '../dashboardWebView/state';
import { EditorHelper, WebviewHelper } from '@estruyf/vscode';
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
import { decodeBase64Image } from '../helpers/decodeBase64Image';
import { WebviewHelper } from '@estruyf/vscode';
import { DashboardData } from '../models/DashboardData';
import { ExplorerView } from '../explorerView/ExplorerView';
import { MediaLibrary } from '../helpers/MediaLibrary';
import { parseWinPath } from '../helpers/parseWinPath';
import { DateHelper } from '../helpers/DateHelper';
import { FrameworkDetector } from '../helpers/FrameworkDetector';
import { ContentType } from '../helpers/ContentType';
import { SortingOption } from '../dashboardWebView/models';
import { Sorting } from '../helpers/Sorting';
import imageSize from 'image-size';
import { CustomScript } from '../helpers/CustomScript';
import { DashboardListener, MediaListener, SettingsListener } from '../listeners';
export class Dashboard {
private static webview: WebviewPanel | null = null;
private static isDisposed: boolean = true;
private static media: MediaInfo[] = [];
private static timers: { [folder: string]: any } = {};
private static _viewData: DashboardData | undefined;
private static mediaLib: MediaLibrary;
private static isDisposed: boolean = true;
public static get viewData(): DashboardData | undefined {
return Dashboard._viewData;
@@ -56,12 +35,12 @@ export class Dashboard {
* Open or reveal the dashboard
*/
public static async open(data?: DashboardData) {
this.mediaLib = MediaLibrary.getInstance();
MediaLibrary.getInstance();
Dashboard._viewData = data;
if (Dashboard.isOpen) {
Dashboard.reveal();
Dashboard.reveal(!!data);
} else {
Dashboard.create();
}
@@ -79,15 +58,33 @@ export class Dashboard {
/**
* Reveal the dashboard if it is open
*/
public static reveal() {
public static reveal(hasData: boolean = false) {
if (Dashboard.webview) {
Dashboard.webview.reveal();
if (hasData) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard.viewData });
}
}
}
public static close() {
Dashboard.webview?.dispose();
}
public static reload() {
if (Dashboard.isOpen) {
Dashboard.webview?.dispose();
setTimeout(() => {
Dashboard.open();
}, 100);
}
}
public static resetViewData() {
Dashboard._viewData = undefined;
}
/**
* Create the dashboard webview
@@ -101,7 +98,8 @@ export class Dashboard {
'FrontMatter Dashboard',
ViewColumn.One,
{
enableScripts: true
enableScripts: true,
retainContextWhenHidden: true
}
);
@@ -119,6 +117,8 @@ export class Dashboard {
Dashboard._viewData = undefined;
const panel = ExplorerView.getInstance(extensionUri);
panel.getMediaSelection();
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: null });
}
await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, this.webview?.visible);
@@ -133,99 +133,26 @@ export class Dashboard {
});
SettingsHelper.onConfigChange((global?: any) => {
Dashboard.getSettings();
SettingsListener.getSettings();
});
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
switch(msg.command) {
case DashboardMessage.getViewType:
if (Dashboard._viewData) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard._viewData });
}
break;
case DashboardMessage.getData:
Dashboard.getSettings();
Dashboard.getPages();
break;
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.updateSetting:
Dashboard.updateSetting(msg.data);
break;
case DashboardMessage.initializeProject:
await commands.executeCommand(COMMAND_NAME.init, Dashboard.getSettings);
break;
case DashboardMessage.reload:
if (!Dashboard.isDisposed) {
Dashboard.webview?.dispose();
setTimeout(() => {
Dashboard.open();
}, 100);
}
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
break;
case DashboardMessage.getMedia:
Dashboard.getMedia(msg?.data?.page, msg?.data?.folder, msg?.data?.sorting);
break;
case DashboardMessage.copyToClipboard:
env.clipboard.writeText(msg.data);
break;
case DashboardMessage.refreshMedia:
Dashboard.resetMedia();
Dashboard.getMedia(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
Dashboard.saveFile(msg?.data);
break;
case DashboardMessage.deleteMedia:
Dashboard.deleteFile(msg?.data);
break;
case DashboardMessage.insertPreviewImage:
Dashboard.insertImage(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
Dashboard.updateMediaMetadata(msg?.data);
break;
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
break;
case DashboardMessage.setFramework:
Dashboard.setFramework(msg?.data);
break;
case DashboardMessage.runCustomScript:
CustomScript.run(msg?.data?.script, msg?.data?.path);
break;
case DashboardMessage.setState:
if (msg?.data?.key && msg?.data?.value) {
Extension.getInstance().setState(msg?.data?.key, msg?.data?.value, "workspace");
}
break;
}
Logger.info(`Receiving message from webview: ${msg.command}`);
DashboardListener.process(msg);
ExtensionListener.process(msg);
MediaListener.process(msg);
PagesListener.process(msg);
SettingsListener.process(msg);
});
}
/**
* Reset media array
* Return the webview
* @returns The webview
*/
public static resetMedia() {
Dashboard.media = [];
}
public static switchFolder(folderPath: string) {
Dashboard.resetMedia();
Dashboard.getMedia(0, folderPath);
public static getWebview() {
return Dashboard.webview?.webview;
}
/**
@@ -242,477 +169,6 @@ export class Dashboard {
}
}
/**
* Insert an image into the front matter or contents
* @param data
*/
private static async insertImage(data: any) {
if (data?.file && data?.image) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
await EditorHelper.showFile(data.file);
Dashboard._viewData = undefined;
const extensionUri = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extensionUri);
if (data?.position) {
const wsFolder = Folders.getWorkspaceFolder();
const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
let imgPath = data.image;
const filePath = data.file;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
const imgDir = dirname(absImgPath);
const fileDir = dirname(filePath);
if (imgDir === fileDir) {
imgPath = join('/', basename(imgPath));
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.image, imgPath);
}
}
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`));
}
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image });
}
}
}
/**
* Retrieve the settings for the dashboard
*/
private static async getSettings() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = await Template.isInitialized();
Dashboard.postWebviewMessage({
command: DashboardCommand.settings,
data: {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
initialized: isInitialized,
tags: SettingsHelper.getTaxonomy(TaxonomyType.Tag),
categories: SettingsHelper.getTaxonomy(TaxonomyType.Category),
openOnStart: SettingsHelper.get(SETTINGS_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<ViewType | undefined>(ExtensionState.PagesView, "workspace"),
mediaSnippet: SettingsHelper.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: SettingsHelper.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
draftField: SettingsHelper.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
customSorting: SettingsHelper.get<SortingSetting[]>(SETTINGS_CONTENT_SORTING),
contentFolders: Folders.get(),
crntFramework: SettingsHelper.get<string>(SETTINGS_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: (SettingsHelper.get<ICustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type && s.type !== ScriptType.Content),
dashboardState: {
contents: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
defaultSorting: SettingsHelper.get<string>(SETTINGS_CONTENT_SORTING_DEFAULT)
},
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
defaultSorting: SettingsHelper.get<string>(SETTINGS_MEDIA_SORTING_DEFAULT)
}
}
} as Settings
});
}
/**
* Set the current site-generator or framework + related settings
* @param frameworkId
*/
private static setFramework(frameworkId: string | null) {
SettingsHelper.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
if (frameworkId) {
const allFrameworks = FrameworkDetector.getAll();
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
if (framework) {
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
} else {
SettingsHelper.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
}
}
}
/**
* Update a setting from the dashboard
*/
private static async updateSetting(data: { name: string, value: any }) {
await SettingsHelper.update(data.name, data.value);
Dashboard.getSettings();
}
/**
* Retrieve all media files
*/
private static async getMedia(page: number = 0, requestedFolder: string = '', sort: SortingOption | null = null) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const contentFolders = Folders.get();
const viewData = Dashboard.viewData;
let selectedFolder = requestedFolder;
const ext = Extension.getInstance();
const crntSort = sort === null ? await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace") : sort;
// If the static folder is not set, retreive the last opened location
if (!selectedFolder) {
const stateValue = await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
// Support for page bundles
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
const folderPath = parse(viewData.data.filePath).dir;
selectedFolder = folderPath;
} else if (stateValue && existsSync(stateValue)) {
selectedFolder = stateValue;
}
}
}
// Go to the home folder
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
selectedFolder = '';
}
let relSelectedFolderPath = selectedFolder;
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
}
if (relSelectedFolderPath.startsWith('/')) {
relSelectedFolderPath = relSelectedFolderPath.substring(1);
}
let allMedia: MediaInfo[] = [];
if (relSelectedFolderPath) {
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
const media = await Dashboard.updateMediaData(Dashboard.filterMedia(files));
allMedia = [...media];
} else {
if (staticFolder) {
const folderSearch = join(staticFolder || "", '/*');
const files = await workspace.findFiles(folderSearch);
const media = await Dashboard.updateMediaData(Dashboard.filterMedia(files));
allMedia = [...media];
}
if (contentFolders && wsFolder) {
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = contentFolders[i];
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
const files = await workspace.findFiles(folderSearch);
const media = await Dashboard.updateMediaData(Dashboard.filterMedia(files));
allMedia = [...allMedia, ...media];
}
}
}
if (crntSort?.type === SortType.string) {
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
} else if (crntSort?.type === SortType.date) {
allMedia = allMedia.sort(Sorting.dateWithFallback("mtime", "fsPath"));
} else {
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
}
if (crntSort?.order === SortOrder.desc) {
allMedia = allMedia.reverse();
}
Dashboard.media = Object.assign([], allMedia);
let files: MediaInfo[] = Dashboard.media;
// Retrieve the total after filtering and before the slicing happens
const total = files.length;
// Get media set
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => {
try {
const metadata = Dashboard.mediaLib.get(file.fsPath);
return {
...file,
dimensions: imageSize(file.fsPath),
...metadata
};
} catch (e) {
return {...file};
}
});
files = files.filter(f => f.mtime !== undefined);
// Retrieve all the folders
let allContentFolders: string[] = [];
let allFolders: string[] = [];
if (selectedFolder) {
if (existsSync(selectedFolder)) {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
} else {
for (const contentFolder of contentFolders) {
const contentPath = contentFolder.path;
if (contentPath && existsSync(contentPath)) {
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticPath && existsSync(staticPath)) {
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
}
// Store the last opened folder
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
let sortedFolders = [...allContentFolders, ...allFolders];
sortedFolders = sortedFolders.sort((a, b) => {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
return 0;
});
if (crntSort?.order === SortOrder.desc) {
sortedFolders = sortedFolders.reverse();
}
Dashboard.postWebviewMessage({
command: DashboardCommand.media,
data: {
media: files,
total: total,
folders: sortedFolders,
selectedFolder
} as MediaPaths
});
}
/**
* Update the metadata of the retrieved files
* @param files
*/
private static async updateMediaData(files: MediaInfo[]) {
files = files.map((m: MediaInfo) => {
const stats = statSync(m.fsPath);
return Object.assign({}, m, stats);
});
return Object.assign([], files);
}
/**
* Retrieve all the markdown pages
*/
private static async getPages() {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = SettingsHelper.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = SettingsHelper.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (file.fileName.endsWith(`.md`) || file.fileName.endsWith(`.markdown`) || file.fileName.endsWith(`.mdx`)) {
try {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
if (article?.data.title) {
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folder.title,
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
const contentType = ArticleHelper.getContentType(article.data);
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
if (article?.data[previewField] && wsFolder) {
let fieldValue = article?.data[previewField];
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
// Revalidate as the array could have been empty
if (fieldValue) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
const contentFolderPath = join(dirname(file.filePath), fieldValue);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.webview?.webview.asWebviewUri(previewUri);
page[previewField] = preview?.toString() || "";
} else {
page[previewField] = "";
}
}
}
pages.push(page);
}
} catch (error: any) {
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
Dashboard.postWebviewMessage({
command: DashboardCommand.pages,
data: pages
});
}
/**
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
}).map((file) => ({
fsPath: file.fsPath,
vsPath: Dashboard.webview?.webview.asWebviewUri(file).toString(),
stats: undefined
} as MediaInfo));
}
/**
* Save the dropped file in the current folder
* @param fileData
*/
private static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = SettingsHelper.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "");
if (folder) {
absFolderPath = folder;
}
if (!existsSync(absFolderPath)) {
absFolderPath = join(wsPath, folder || "");
}
if (!existsSync(absFolderPath)) {
Notifications.error(`We couldn't find your selected folder.`);
return;
}
const staticPath = join(absFolderPath, fileName);
const imgData = decodeBase64Image(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
const folderPath = `${folder}`;
if (Dashboard.timers[folderPath]) {
clearTimeout(Dashboard.timers[folderPath]);
delete Dashboard.timers[folderPath];
}
Dashboard.timers[folderPath] = setTimeout(() => {
Dashboard.media = [];
Dashboard.getMedia(0, folder || "");
delete Dashboard.timers[folderPath];
}, 500);
} else {
Notifications.error(`Something went wrong uploading ${fileName}`);
}
}
}
/**
* Delete the selected file
* @param fileData
*/
private static async deleteFile({ file, page, folder }: { file: string; page: number; folder: string | null; }) {
if (!file) {
return;
}
try {
unlinkSync(file);
Dashboard.media = [];
Dashboard.getMedia(page || 0, folder || "");
} catch(err) {
Notifications.error(`Something went wrong deleting ${basename(file)}`);
}
}
/**
* Update the metadata of the selected file
*/
private static async updateMediaMetadata({ file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; }) {
Dashboard.mediaLib.set(file, metadata);
// Check if filename needs to be updated
Dashboard.mediaLib.updateFilename(file, filename);
Dashboard.getMedia(page || 0, folder || "");
}
/**
* Retrieve the webview HTML contents
* @param webView
+4 -1
View File
@@ -11,6 +11,8 @@ import { existsSync, mkdirSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
import { parseWinPath } from '../helpers/parseWinPath';
import { MediaHelpers } from '../helpers/MediaHelpers';
import { MediaListener } from '../listeners';
export const WORKSPACE_PLACEHOLDER = `[[workspace]]`;
@@ -62,7 +64,8 @@ export class Folders {
}
if (Dashboard.isOpen) {
Dashboard.switchFolder(folderName);
MediaHelpers.resetMedia();
MediaListener.sendMediaFiles(0, folderName);
}
}
+1
View File
@@ -28,6 +28,7 @@ export const COMMAND_NAME = {
collapseSections: getCommandName("collapseSections"),
preview: getCommandName("preview"),
dashboard: getCommandName("dashboard"),
dashboardMedia: getCommandName("dashboard.media"),
dashboardClose: getCommandName("dashboard.close"),
promote: getCommandName("promoteSettings"),
insertImage: getCommandName("insertImage"),
@@ -20,19 +20,17 @@ export const Contents: React.FunctionComponent<IContentsProps> = ({pages, loadin
const pageFolders = [...new Set(pageItems.map(page => page.fmFolder))];
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header
folders={pageFolders}
totalPages={pageItems.length}
settings={settings} />
<div className="flex flex-col h-full overflow-auto">
<Header
folders={pageFolders}
totalPages={pageItems.length}
settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
</div>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4">
{ loading ? <Spinner /> : <Overview pages={pageItems} settings={settings} /> }
</div>
</main>
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
);
};
@@ -3,11 +3,12 @@ import { useRecoilValue } from 'recoil';
import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon';
import { DashboardMessage } from '../../DashboardMessage';
import { Page } from '../../models/Page';
import { SettingsSelector, ViewSelector, ViewType } from '../../state';
import { SettingsSelector, ViewSelector } from '../../state';
import { DateField } from '../DateField';
import { Status } from '../Status';
import { Messenger } from '@estruyf/vscode/dist/client';
import useContentType from '../../../hooks/useContentType';
import { DashboardViewType } from '../../models';
export interface IItemProps extends Page {}
@@ -22,7 +23,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
Messenger.send(DashboardMessage.openFile, fmFilePath);
};
if (view === ViewType.Grid) {
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`}
@@ -53,7 +54,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
</button>
</li>
);
} else if (view === ViewType.List) {
} else if (view === DashboardViewType.List) {
return (
<li className="relative">
<button className={`px-5 cursor-pointer w-full text-left grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8 py-2 border-b border-gray-300 hover:bg-gray-200 dark:border-vulcan-50 dark:hover:bg-vulcan-50 hover:bg-opacity-70`} onClick={openFile}>
@@ -1,6 +1,7 @@
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { ViewSelector, ViewType } from '../../state';
import { DashboardViewType } from '../../models';
import { ViewSelector } from '../../state';
export interface IListProps {}
@@ -8,15 +9,15 @@ export const List: React.FunctionComponent<IListProps> = ({children}: React.Prop
const view = useRecoilValue(ViewSelector);
let className = '';
if (view === ViewType.Grid) {
if (view === DashboardViewType.Grid) {
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`;
} else if (view === ViewType.List) {
} else if (view === DashboardViewType.List) {
className = `-mx-4`;
}
return (
<ul role="list" className={className}>
{view === ViewType.List && (
{view === DashboardViewType.List && (
<li className="px-5 relative uppercase text-vulcan-100 dark:text-whisper-900 py-2 border-b border-vulcan-50">
<div className={`grid grid-cols-12 gap-x-4 sm:gap-x-6 xl:gap-x-8`}>
<div className="col-span-8">
+12 -6
View File
@@ -7,7 +7,7 @@ import { useRecoilValue } from 'recoil';
import { DashboardViewSelector } from '../state';
import { Contents } from './Contents/Contents';
import { Media } from './Media/Media';
import { ViewType } from '../models';
import { NavigationType } from '../models';
export interface IDashboardProps {
showWelcome: boolean;
@@ -30,9 +30,15 @@ export const Dashboard: React.FunctionComponent<IDashboardProps> = ({showWelcome
return <WelcomeScreen settings={settings} />;
}
if (view === ViewType.Media) {
return <Media />;
}
return <Contents pages={pages} loading={loading} />;
return (
<main className={`h-full w-full`}>
{
view === NavigationType.Media ? (
<Media />
) : (
<Contents pages={pages} loading={loading} />
)
}
</main>
);
};
@@ -5,7 +5,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { Sorting } from '.';
import { HOME_PAGE_NAVIGATION_ID } from '../../../constants';
import { parseWinPath } from '../../../helpers/parseWinPath';
import { ViewType } from '../../models';
import { NavigationType } from '../../models';
import { SelectedMediaFolderAtom, SettingsAtom } from '../../state';
export interface IBreadcrumbProps {}
@@ -99,7 +99,7 @@ export const Breadcrumb: React.FunctionComponent<IBreadcrumbProps> = (props: Rea
</ol>
<div className={`flex px-5`}>
<Sorting view={ViewType.Media} disableCustomSorting />
<Sorting view={NavigationType.Media} disableCustomSorting />
</div>
</nav>
);
@@ -3,7 +3,7 @@ import { Sorting } from './Sorting';
import { Searchbox } from './Searchbox';
import { Filter } from './Filter';
import { Folders } from './Folders';
import { Settings, ViewType } from '../../models';
import { Settings, NavigationType } from '../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { Startup } from '../Startup';
import { Navigation } from '../Navigation';
@@ -47,7 +47,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
Messenger.send(DashboardMessage.createByTemplate);
};
const updateView = (view: ViewType) => {
const updateView = (view: NavigationType) => {
setView(view);
resetSorting();
}
@@ -57,17 +57,17 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<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(ViewType.Contents)}>
<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(ViewType.Media)}>
<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>
{
view === ViewType.Contents && (
view === NavigationType.Contents && (
<>
<div className={`px-4 mt-3 mb-2 flex items-center justify-between`}>
<Searchbox />
@@ -112,14 +112,14 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({totalPages, folde
<Grouping />
<Sorting view={ViewType.Contents} />
<Sorting view={NavigationType.Contents} />
</div>
</>
)
}
{
view === ViewType.Media && (
view === NavigationType.Media && (
<>
<Pagination />
<Breadcrumb />
@@ -6,14 +6,14 @@ import { ExtensionState } from '../../../constants';
import { SortOrder, SortType } from '../../../models';
import { SortOption } from '../../constants/SortOption';
import { DashboardMessage } from '../../DashboardMessage';
import { ViewType } from '../../models';
import { NavigationType } from '../../models';
import { SortingOption } from '../../models/SortingOption';
import { SearchSelector, SettingsSelector, SortingAtom } from '../../state';
import { MenuButton, MenuItem, MenuItems } from '../Menu';
export interface ISortingProps {
disableCustomSorting?: boolean;
view: ViewType;
view: NavigationType;
}
export const sortOptions: SortingOption[] = [
@@ -30,7 +30,7 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
const updateSorting = (value: SortingOption) => {
Messenger.send(DashboardMessage.setState, {
key: `${view === ViewType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
key: `${view === NavigationType.Contents ? ExtensionState.Dashboard.Contents.Sorting : ExtensionState.Dashboard.Media.Sorting}`,
value: value
});
@@ -50,16 +50,16 @@ export const Sorting: React.FunctionComponent<ISortingProps> = ({disableCustomSo
let crntSortingOption = crntSorting;
if (!crntSortingOption) {
if (view === ViewType.Contents) {
if (view === NavigationType.Contents) {
crntSortingOption = settings?.dashboardState?.contents?.sorting || null;
} else if (view === ViewType.Media) {
} else if (view === NavigationType.Media) {
crntSortingOption = settings?.dashboardState?.media?.sorting || null;
}
if (crntSortingOption === null) {
if (view === ViewType.Contents && settings?.dashboardState.contents.defaultSorting) {
if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) {
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
} else if (view === ViewType.Media && settings?.dashboardState.contents.defaultSorting) {
} else if (view === NavigationType.Media && settings?.dashboardState.contents.defaultSorting) {
crntSortingOption = allOptions.find(f => f.id === settings?.dashboardState.contents.defaultSorting) || null;
}
}
@@ -1,9 +1,10 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ViewAtom, ViewType, SettingsSelector } from '../../state';
import { ViewAtom, SettingsSelector } from '../../state';
import { ViewGridIcon, ViewListIcon } from '@heroicons/react/solid';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { DashboardViewType } from '../../models';
export interface IViewSwitchProps {}
@@ -12,7 +13,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
const settings = useRecoilValue(SettingsSelector);
const toggleView = () => {
const newView = view === ViewType.Grid ? ViewType.List : ViewType.Grid;
const newView = view === DashboardViewType.Grid ? DashboardViewType.List : DashboardViewType.Grid;
setView(newView);
Messenger.send(DashboardMessage.setPageViewType, newView);
};
@@ -25,11 +26,11 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (props: Rea
return (
<div className={`flex rounded-sm bg-vulcan-50 lg:mb-1`}>
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === ViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<button className={`flex items-center px-2 py-1 rounded-l-sm ${view === DashboardViewType.Grid ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewGridIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to grid</span>
</button>
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === ViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<button className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.List ? 'bg-teal-500 text-vulcan-500' : 'text-whisper-500'}`} onClick={toggleView}>
<ViewListIcon className={`w-4 h-4`} />
<span className={`sr-only`}>Change to list</span>
</button>
+61 -63
View File
@@ -73,75 +73,73 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
}, ['']);
return (
<main className={`h-full w-full`}>
<div className="flex flex-col h-full overflow-auto">
<Header settings={settings} />
<div className="flex flex-col h-full overflow-auto">
<Header settings={settings} />
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>Select the image you want to use for your article.</p>
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
</div>
)
}
{
isDragActive && (
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
</p>
</div>
)
}
{
(media.length === 0 && folders.length === 0 && !loading) && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files.</p>
</div>
</div>
)
}
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
}
<List>
{
media.map((file) => (
<Item key={file.fsPath} media={file} />
))
}
</List>
</div>
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
loading && ( <Spinner /> )
viewData?.data?.filePath && (
<div className={`text-lg text-center mb-6`}>
<p>Select the image you want to use for your article.</p>
<p className={`opacity-80 text-base`}>You can also drag and drop images from your desktop and select that once uploaded.</p>
</div>
)
}
{
isDragActive && (
<div className="absolute top-0 left-0 w-full h-full text-whisper-500 bg-gray-900 bg-opacity-70 flex flex-col justify-center items-center z-50">
<UploadIcon className={`h-32`} />
<p className={`text-xl max-w-md text-center`}>
{selectedFolder ? `Upload to ${selectedFolder}` : `No folder selected, files you drop will be added to the ${settings?.staticFolder || "public"} folder.`}
</p>
</div>
)
}
<Lightbox />
{
(media.length === 0 && folders.length === 0 && !loading) && (
<div className={`flex items-center justify-center h-full`}>
<div className={`max-w-xl text-center`}>
<FrontMatterIcon className={`text-vulcan-300 dark:text-whisper-800 h-32 mx-auto opacity-90 mb-8`} />
<p className={`text-xl font-medium`}>No media files to show. You can drag &amp; drop new files.</p>
</div>
</div>
)
}
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
{
folders && folders.length > 0 && (
<div className={`mb-8`}>
<List>
{
folders && folders.map((folder) => (
<FolderItem key={folder} folder={folder} staticFolder={settings?.staticFolder} wsFolder={settings?.wsFolder} />
))
}
</List>
</div>
)
}
<List>
{
media.map((file) => (
<Item key={file.fsPath} media={file} />
))
}
</List>
</div>
</main>
{
loading && ( <Spinner /> )
}
<Lightbox />
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} />
</div>
);
};
@@ -3,9 +3,11 @@ 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, SortingSelector } from '../../state';
import { LoadingAtom, MediaTotalSelector, PageAtom, SelectedMediaFolderSelector, SettingsSelector, SortingSelector } from '../../state';
import { FolderCreation } from './FolderCreation';
import { LIMIT } from './Media';
import { PaginationButton } from './PaginationButton';
@@ -13,12 +15,16 @@ 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 = () => {
@@ -61,35 +67,39 @@ export const Pagination: React.FunctionComponent<IPaginationProps> = ({}: React.
}
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
setLastUpdated(new Date().getTime().toString());
}, [page]);
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page: 0,
folder: selectedFolder || '',
sorting: crntSorting
});
setPage(0);
if (prevSelectedFolder !== null || settings?.dashboardState?.media.selectedFolder !== selectedFolder) {
setLoading(true);
setPage(0);
setLastUpdated(new Date().getTime().toString());
}
}, [selectedFolder]);
React.useEffect(() => {
setLoading(true);
Messenger.send(DashboardMessage.getMedia, {
page,
folder: selectedFolder || '',
sorting: crntSorting
});
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 (
+5 -3
View File
@@ -6,7 +6,7 @@ import { Page } from '../models/Page';
import { DashboardViewAtom, SettingsAtom, ViewDataAtom } from '../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { ViewType } from '../models';
import { NavigationType } from '../models';
export default function useMessages() {
const [loading, setLoading] = useState<boolean>(false);
@@ -22,8 +22,10 @@ export default function useMessages() {
break;
case DashboardCommand.viewData:
setViewData(message.data.data);
if (message.data.data?.type === ViewType.Media) {
setView(ViewType.Media);
if (message.data.data?.type === NavigationType.Media) {
setView(NavigationType.Media);
} else if (message.data.data?.type === NavigationType.Contents) {
setView(NavigationType.Contents);
}
break;
case DashboardCommand.settings:
@@ -0,0 +1,4 @@
export enum DashboardViewType {
Grid = 1,
List
}
@@ -1,4 +1,4 @@
export enum ViewType {
export enum NavigationType {
Contents = "contents",
Media = "media"
}
+7 -3
View File
@@ -1,8 +1,8 @@
import { VersionInfo } from '../../models/VersionInfo';
import { ViewType } from '../state';
import { ContentFolder } from '../../models/ContentFolder';
import { ContentType, CustomScript, DraftField, Framework, SortingSetting } from '../../models';
import { SortingOption } from './SortingOption';
import { DashboardViewType } from '.';
export interface Settings {
beta: boolean;
@@ -14,7 +14,7 @@ export interface Settings {
categories: string[];
openOnStart: boolean | null;
versionInfo: VersionInfo;
pageViewType: ViewType | undefined;
pageViewType: DashboardViewType | undefined;
mediaSnippet: string[];
contentTypes: ContentType[];
contentFolders: ContentFolder[];
@@ -28,10 +28,14 @@ export interface Settings {
export interface DashboardState {
contents: ViewState;
media: ViewState;
media: MediaViewState;
}
export interface ViewState {
sorting: SortingOption | null | undefined;
defaultSorting: string | null | undefined;
}
export interface MediaViewState extends ViewState {
selectedFolder: string | null | undefined;
}
+2 -1
View File
@@ -1,5 +1,6 @@
export * from './DashboardViewType';
export * from './NavigationType';
export * from './Page';
export * from './Settings';
export * from './SortingOption';
export * from './Status';
export * from './ViewType';
@@ -1,7 +1,7 @@
import { atom } from 'recoil';
import { ViewType } from '../../models';
import { NavigationType } from '../../models';
export const DashboardViewAtom = atom<ViewType>({
export const DashboardViewAtom = atom<NavigationType>({
key: 'DashboardViewAtom',
default: ViewType.Contents
default: NavigationType.Contents
});
+3 -7
View File
@@ -1,11 +1,7 @@
import { atom } from 'recoil';
import { DashboardViewType } from '../../models';
export enum ViewType {
Grid = 1,
List
}
export const ViewAtom = atom<ViewType>({
export const ViewAtom = atom<DashboardViewType>({
key: 'ViewAtom',
default: ViewType.Grid
default: DashboardViewType.Grid
});
+9 -1
View File
@@ -45,7 +45,15 @@ export async function activate(context: vscode.ExtensionContext) {
// Pages dashboard
Dashboard.init();
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Dashboard.open(data);
if (!data) {
Dashboard.open({ type: "contents" });
} else {
Dashboard.open(data);
}
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => {
Dashboard.open({ type: "media" });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
+50
View File
@@ -0,0 +1,50 @@
import { Folders } from "../commands/Folders";
import { Template } from "../commands/Template";
import { ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } from "../models";
import { Extension } from "./Extension";
import { FrameworkDetector } from "./FrameworkDetector";
import { Settings } from "./SettingsHelper";
export class DashboardSettings {
public static async get() {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = await Template.isInitialized();
return {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
staticFolder: Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
initialized: isInitialized,
tags: Settings.getTaxonomy(TaxonomyType.Tag),
categories: Settings.getTaxonomy(TaxonomyType.Category),
openOnStart: Settings.get(SETTINGS_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState<DashboardViewType | undefined>(ExtensionState.PagesView, "workspace"),
mediaSnippet: Settings.get<string[]>(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
draftField: Settings.get<DraftField>(SETTINGS_CONTENT_DRAFT_FIELD),
customSorting: Settings.get<SortingSetting[]>(SETTINGS_CONTENT_SORTING),
contentFolders: Folders.get(),
crntFramework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: (Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || []).filter(s => s.type && s.type !== ScriptType.Content),
dashboardState: {
contents: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTINGS_CONTENT_SORTING_DEFAULT)
},
media: {
sorting: await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace"),
defaultSorting: Settings.get<string>(SETTINGS_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace")
}
}
} as ISettings
}
}
+15 -1
View File
@@ -1,6 +1,6 @@
import { existsSync, renameSync } from "fs";
import { basename, join } from "path";
import { extensions, Uri, ExtensionContext, window, workspace, commands } from "vscode";
import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode } from "vscode";
import { Folders, WORKSPACE_PLACEHOLDER } from "../commands/Folders";
import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants";
import { ContentType } from "../models";
@@ -74,6 +74,20 @@ export class Extension {
};
}
/**
* Get the title of the extension
*/
public get title(): string {
return this.ctx.extension.packageJSON.name;
}
/**
* Check if the extension is in production/development mode
*/
public get isProductionMode(): boolean {
return this.ctx.extensionMode === ExtensionMode.Production;
}
/**
* Set the current version information for the extension
*/
+28
View File
@@ -0,0 +1,28 @@
import { Extension } from './Extension';
import { OutputChannel, window } from 'vscode';
import { format } from 'date-fns';
export class Logger {
private static instance: Logger;
private static channel: OutputChannel | null = null;
private constructor() {
const title = Extension.getInstance().title;
Logger.channel = window.createOutputChannel(title);
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public static info(message: string, type: "INFO" | "WARNING" | "ERROR" = "INFO"): void {
if (!Logger.channel) {
Logger.getInstance();
}
Logger.channel?.appendLine(`["${type}" - ${format(new Date(), "HH:MM:ss")}] ${message}`);
}
}
+336
View File
@@ -0,0 +1,336 @@
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
import { ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTINGS_CONTENT_STATIC_FOLDER } from "../constants";
import { SortingOption } from "../dashboardWebView/models";
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
import { basename, extname, join, parse, dirname } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, workspace, window, Position } from "vscode";
import imageSize from "image-size";
import { EditorHelper } from "@estruyf/vscode";
import { ExplorerView } from "../explorerView/ExplorerView";
export class MediaHelpers {
private static media: MediaInfo[] = [];
/**
* Retrieve all media files
* @param page
* @param requestedFolder
* @param sort
* @returns
*/
public static async getMedia(page: number = 0, requestedFolder: string = '', sort: SortingOption | null = null) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const contentFolders = Folders.get();
const viewData = Dashboard.viewData;
let selectedFolder = requestedFolder;
const ext = Extension.getInstance();
const crntSort = sort === null ? await ext.getState<SortingOption | undefined>(ExtensionState.Dashboard.Media.Sorting, "workspace") : sort;
// If the static folder is not set, retreive the last opened location
if (!selectedFolder) {
const stateValue = await ext.getState<string | undefined>(ExtensionState.SelectedFolder, "workspace");
if (stateValue !== HOME_PAGE_NAVIGATION_ID) {
// Support for page bundles
if (viewData?.data?.filePath && viewData?.data?.filePath.endsWith('index.md')) {
const folderPath = parse(viewData.data.filePath).dir;
selectedFolder = folderPath;
} else if (stateValue && existsSync(stateValue)) {
selectedFolder = stateValue;
}
}
}
// Go to the home folder
if (selectedFolder === HOME_PAGE_NAVIGATION_ID) {
selectedFolder = '';
}
let relSelectedFolderPath = selectedFolder;
const parsedPath = parseWinPath(wsFolder?.fsPath || "");
if (selectedFolder && selectedFolder.startsWith(parsedPath)) {
relSelectedFolderPath = selectedFolder.replace(parsedPath, '');
}
if (relSelectedFolderPath.startsWith('/')) {
relSelectedFolderPath = relSelectedFolderPath.substring(1);
}
let allMedia: MediaInfo[] = [];
if (relSelectedFolderPath) {
const files = await workspace.findFiles(join(relSelectedFolderPath, '/*'));
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
allMedia = [...media];
} else {
if (staticFolder) {
const folderSearch = join(staticFolder || "", '/*');
const files = await workspace.findFiles(folderSearch);
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
allMedia = [...media];
}
if (contentFolders && wsFolder) {
for (let i = 0; i < contentFolders.length; i++) {
const contentFolder = contentFolders[i];
const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
const files = await workspace.findFiles(folderSearch);
const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
allMedia = [...allMedia, ...media];
}
}
}
if (crntSort?.type === SortType.string) {
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
} else if (crntSort?.type === SortType.date) {
allMedia = allMedia.sort(Sorting.dateWithFallback("mtime", "fsPath"));
} else {
allMedia = allMedia.sort(Sorting.alphabetically("fsPath"));
}
if (crntSort?.order === SortOrder.desc) {
allMedia = allMedia.reverse();
}
MediaHelpers.media = Object.assign([], allMedia);
let files: MediaInfo[] = MediaHelpers.media;
// Retrieve the total after filtering and before the slicing happens
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);
return {
...file,
dimensions: imageSize(file.fsPath),
...metadata
};
} catch (e) {
return {...file};
}
});
files = files.filter(f => f.mtime !== undefined);
// Retrieve all the folders
let allContentFolders: string[] = [];
let allFolders: string[] = [];
if (selectedFolder) {
if (existsSync(selectedFolder)) {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
} else {
for (const contentFolder of contentFolders) {
const contentPath = contentFolder.path;
if (contentPath && existsSync(contentPath)) {
const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
allContentFolders = [...allContentFolders, ...subFolders];
}
}
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "");
if (staticPath && existsSync(staticPath)) {
allFolders = readdirSync(staticPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(staticPath, dir.name)));
}
}
// Store the last opened folder
await Extension.getInstance().setState(ExtensionState.SelectedFolder, requestedFolder === HOME_PAGE_NAVIGATION_ID ? HOME_PAGE_NAVIGATION_ID : selectedFolder, "workspace");
let sortedFolders = [...allContentFolders, ...allFolders];
sortedFolders = sortedFolders.sort((a, b) => {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
}
if (a.toLowerCase() > b.toLowerCase()) {
return 1;
}
return 0;
});
if (crntSort?.order === SortOrder.desc) {
sortedFolders = sortedFolders.reverse();
}
return {
media: files,
total: total,
folders: sortedFolders,
selectedFolder
} as MediaPaths
}
/**
* Reset media array
*/
public static resetMedia() {
MediaHelpers.media = [];
}
/**
* Save the dropped file in the current folder
* @param fileData
*/
public static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "");
if (folder) {
absFolderPath = folder;
}
if (!existsSync(absFolderPath)) {
absFolderPath = join(wsPath, folder || "");
}
if (!existsSync(absFolderPath)) {
Notifications.error(`We couldn't find your selected folder.`);
return;
}
const staticPath = join(absFolderPath, fileName);
const imgData = decodeBase64Image(contents);
if (imgData) {
writeFileSync(staticPath, imgData.data);
Notifications.info(`File ${fileName} uploaded to: ${folder}`);
return true;
} else {
Notifications.error(`Something went wrong uploading ${fileName}`);
throw new Error(`Something went wrong uploading ${fileName}`);
}
}
return false;
}
/**
* Delete the selected file
* @param data
* @returns
*/
public static async deleteFile({ file, page, folder }: { file: string; page: number; folder: string | null; }) {
if (!file) {
return;
}
try {
unlinkSync(file);
MediaHelpers.media = [];
return true;
} catch(err: any) {
Notifications.error(`Something went wrong deleting ${basename(file)}`);
throw new Error(`Something went wrong deleting ${basename(file)}`);
}
}
/**
* Insert an image into the front matter or contents
* @param data
*/
public static async insertMediaToMarkdown(data: any) {
if (data?.file && data?.image) {
if (!data?.position) {
await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`);
}
await EditorHelper.showFile(data.file);
Dashboard.resetViewData();
const extensionUri = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extensionUri);
if (data?.position) {
const wsFolder = Folders.getWorkspaceFolder();
const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
let imgPath = data.image;
const filePath = data.file;
const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
const imgDir = dirname(absImgPath);
const fileDir = dirname(filePath);
if (imgDir === fileDir) {
imgPath = join('/', basename(imgPath));
// Snippets are already parsed, so update the URL of the image
if (data.snippet) {
data.snippet = data.snippet.replace(data.image, imgPath);
}
}
await editor?.edit(builder => builder.insert(new Position(line, character), data.snippet || `![${data.alt || data.caption || ""}](${imgPath})`));
}
panel.getMediaSelection();
} else {
panel.getMediaSelection();
panel.updateMetadata({field: data.fieldName, value: data.image });
}
}
}
/**
* Update the metadata of a media file
* @param data
*/
public static updateMetadata(data: any) {
const { file, filename, page, folder, ...metadata }: { file:string; filename:string; page: number; folder: string | null; metadata: any; } = data;
const mediaLib = MediaLibrary.getInstance();
mediaLib.set(file, metadata);
// Check if filename needs to be updated
mediaLib.updateFilename(file, filename);
}
/**
* Filter the media files
*/
private static filterMedia(files: Uri[]) {
return files.filter(file => {
const ext = extname(file.fsPath);
return ['.jpg', '.jpeg', '.png', '.gif', '.svg'].includes(ext);
}).map((file) => ({
fsPath: file.fsPath,
vsPath: Dashboard.getWebview()?.asWebviewUri(file).toString(),
stats: undefined
} as MediaInfo));
}
/**
* Update the metadata of the retrieved files
* @param files
*/
private static async updateMediaData(files: MediaInfo[]) {
files = files.map((m: MediaInfo) => {
const stats = statSync(m.fsPath);
return Object.assign({}, m, stats);
});
return Object.assign([], files);
}
}
+3 -4
View File
@@ -1,5 +1,4 @@
import { Dashboard } from '../commands/Dashboard';
import { MediaHelpers } from './MediaHelpers';
import { workspace } from 'vscode';
import { JsonDB } from 'node-json-db/dist/JsonDB';
import { basename, dirname, join, parse } from 'path';
@@ -34,7 +33,7 @@ export class MediaLibrary {
f.oldUri.path.endsWith('.png') ||
f.oldUri.path.endsWith('.gif')) {
this.rename(f.oldUri.fsPath, f.newUri.fsPath);
Dashboard.resetMedia();
MediaHelpers.resetMedia();
}
});
});
@@ -89,7 +88,7 @@ export class MediaLibrary {
} else {
renameSync(filePath, newPath);
this.rename(filePath, newPath);
Dashboard.resetMedia();
MediaHelpers.resetMedia();
}
} catch(err) {
Notifications.error(`Something went wrong updating "${name}" to "${filename}".`);
+4
View File
@@ -1,18 +1,22 @@
import { window } from "vscode";
import { Logger } from ".";
import { EXTENSION_NAME } from "../constants";
export class Notifications {
public static info(message: string, items?: any): Thenable<string> {
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
return window.showInformationMessage(`${EXTENSION_NAME}: ${message}`, items);
}
public static warning(message: string, items?: any): Thenable<string> {
Logger.info(`${EXTENSION_NAME}: ${message}`, "WARNING");
return window.showWarningMessage(`${EXTENSION_NAME}: ${message}`, items);
}
public static error(message: string, items?: any): Thenable<string> {
Logger.info(`${EXTENSION_NAME}: ${message}`, "ERROR");
return window.showErrorMessage(`${EXTENSION_NAME}: ${message}`, items);
}
}
+19
View File
@@ -1,8 +1,27 @@
export * from './ArticleHelper';
export * from './ContentType';
export * from './CustomScript';
export * from './DashboardSettings';
export * from './DateHelper';
export * from './Extension';
export * from './FilesHelper';
export * from './FrameworkDetector';
export * from './GroupBy';
export * from './ImageHelper';
export * from './Logger';
export * from './MediaLibrary';
export * from './MessageHelper';
export * from './Notifications';
export * from './Questions';
export * from './Sanitize';
export * from './SeoHelper';
export * from './SettingsHelper';
export * from './SlugHelper';
export * from './Sorting';
export * from './StringHelpers';
export * from './TomlEngine';
export * from './decodeBase64Image';
export * from './getNonce';
export * from './isValidFile';
export * from './openFileInEditor';
export * from './parseWinPath';
+7
View File
@@ -0,0 +1,7 @@
export const isValidFile = (fileName: string) => {
return fileName.endsWith(`.md`) ||
fileName.endsWith(`.markdown`) ||
fileName.endsWith(`.mdx`);
}
+24
View File
@@ -0,0 +1,24 @@
import { Dashboard } from "../commands/Dashboard";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Logger } from "../helpers/Logger";
export abstract class BaseListener {
public static process(msg: { command: DashboardMessage, data: any }) {}
/**
* Send a message to the webview
* @param command
* @param data
*/
public static sendMsg(command: DashboardCommand, data: any) {
Logger.info(`Sending message to webview: ${command}`);
Dashboard.postWebviewMessage({
command,
data
});
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Dashboard } from "../commands/Dashboard";
import { ExtensionState } from "../constants";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Extension } from "../helpers";
import { BaseListener } from "./BaseListener";
export class DashboardListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getViewType:
if (Dashboard.viewData) {
this.sendMsg(DashboardCommand.viewData, Dashboard.viewData);
}
break;
case DashboardMessage.reload:
Dashboard.reload();
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
break;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import { commands, env } from "vscode";
import { SettingsListener } from ".";
import { COMMAND_NAME } from "../constants";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { CustomScript, Extension } from "../helpers";
import { openFileInEditor } from "../helpers/openFileInEditor";
import { BaseListener } from "./BaseListener";
export class ExtensionListener extends BaseListener {
public static process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.openFile:
openFileInEditor(msg.data);
break;
case DashboardMessage.initializeProject:
commands.executeCommand(COMMAND_NAME.init, SettingsListener.getSettings);
break;
case DashboardMessage.copyToClipboard:
env.clipboard.writeText(msg.data);
break;
case DashboardMessage.runCustomScript:
CustomScript.run(msg?.data?.script, msg?.data?.path);
break;
case DashboardMessage.setState:
this.setState(msg?.data);
break;
}
}
private static setState(data: any) {
const { key, value } = data;
if (key && value) {
Extension.getInstance().setState(key, value, "workspace");
}
}
}
+101
View File
@@ -0,0 +1,101 @@
import { MediaHelpers } from './../helpers/MediaHelpers';
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { SortingOption } from '../dashboardWebView/models';
import { commands } from 'vscode';
import { COMMAND_NAME } from '../constants';
export class MediaListener extends BaseListener {
private static timers: { [folder: string]: any } = {};
public static async process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getMedia:
const { page, folder, sorting } = msg?.data;
this.sendMediaFiles(page, folder, sorting);
break;
case DashboardMessage.refreshMedia:
MediaHelpers.resetMedia();
this.sendMediaFiles(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
this.store(msg?.data);
break;
case DashboardMessage.deleteMedia:
this.delete(msg?.data);
break;
case DashboardMessage.insertPreviewImage:
MediaHelpers.insertMediaToMarkdown(msg?.data);
break;
case DashboardMessage.updateMediaMetadata:
this.update(msg.data);
break;
case DashboardMessage.createMediaFolder:
await commands.executeCommand(COMMAND_NAME.createFolder, msg?.data);
break;
}
}
/**
* Sends the media files to the dashboard
* @param page
* @param folder
* @param sorting
*/
public static async sendMediaFiles(page: number = 0, folder: string = '', sorting: SortingOption | null = null) {
const files = await MediaHelpers.getMedia(page, folder, sorting);
this.sendMsg(DashboardCommand.media, files);
}
/**
* Store the file and send a message after multiple uploads
* @param data
*/
private static async store(data: any) {
try {
const { folder } = data;
await MediaHelpers.saveFile(data);
const folderPath = `${folder}`;
if (this.timers[folderPath]) {
clearTimeout(this.timers[folderPath]);
delete this.timers[folderPath];
}
this.timers[folderPath] = setTimeout(() => {
MediaHelpers.resetMedia();
this.sendMediaFiles(0, folder || "");
delete this.timers[folderPath];
}, 500);
} catch {}
}
/**
* Delete a media file
* @param data
*/
private static delete(data: { file: string; page: number; folder: string | null; }) {
try {
MediaHelpers.deleteFile(data);
this.sendMediaFiles(data.page || 0, data.folder || "");
} catch {}
}
/**
* Update media metadata
* @param data
*/
private static update(data: any) {
try {
const { page, folder } = data;
MediaHelpers.updateMetadata(data);
this.sendMediaFiles(page || 0, folder || "");
} catch {}
}
}
+127
View File
@@ -0,0 +1,127 @@
import { isValidFile } from './../helpers/isValidFile';
import { existsSync } from "fs";
import { dirname, join } from "path";
import { commands, Uri } 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";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { Page } from "../dashboardWebView/models";
import { ArticleHelper, Settings } from "../helpers";
import { ContentType } from "../helpers/ContentType";
import { DateHelper } from "../helpers/DateHelper";
import { Notifications } from "../helpers/Notifications";
import { BaseListener } from "./BaseListener";
export class PagesListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static async process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getData:
this.getPagesData();
break;
case DashboardMessage.createContent:
await commands.executeCommand(COMMAND_NAME.createContent);
break;
case DashboardMessage.createByContentType:
await commands.executeCommand(COMMAND_NAME.createByContentType);
break;
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
}
}
/**
* Retrieve all the markdown pages
*/
private static async getPagesData() {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
const staticFolder = Settings.get<string>(SETTINGS_CONTENT_STATIC_FOLDER);
const folderInfo = await Folders.getInfo();
const pages: Page[] = [];
if (folderInfo) {
for (const folder of folderInfo) {
for (const file of folder.lastModified) {
if (isValidFile(file.fileName)) {
try {
const article = ArticleHelper.getFrontMatterByPath(file.filePath);
if (article?.data.title) {
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folder.title,
fmModified: file.mtime,
fmFilePath: file.filePath,
fmFileName: file.fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
// Make sure these are always set
title: article?.data.title,
slug: article?.data.slug,
date: article?.data[dateField] || "",
draft: article?.data.draft,
description: article?.data[descriptionField] || "",
};
const contentType = ArticleHelper.getContentType(article.data);
const previewField = contentType.fields.find(field => field.isPreviewImage && field.type === "image")?.name || "preview";
if (article?.data[previewField] && wsFolder) {
let fieldValue = article?.data[previewField];
if (fieldValue && Array.isArray(fieldValue)) {
if (fieldValue.length > 0) {
fieldValue = fieldValue[0];
} else {
fieldValue = undefined;
}
}
// Revalidate as the array could have been empty
if (fieldValue) {
const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue);
const contentFolderPath = join(dirname(file.filePath), fieldValue);
let previewUri = null;
if (existsSync(staticPath)) {
previewUri = Uri.file(staticPath);
} else if (existsSync(contentFolderPath)) {
previewUri = Uri.file(contentFolderPath);
}
if (previewUri) {
const preview = Dashboard.getWebview()?.asWebviewUri(previewUri);
page[previewField] = preview?.toString() || "";
} else {
page[previewField] = "";
}
}
}
pages.push(page);
}
} catch (error: any) {
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
}
}
}
this.sendMsg(DashboardCommand.pages, pages);
}
}
+69
View File
@@ -0,0 +1,69 @@
import { SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_FRAMEWORK_ID } from "../constants";
import { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { DashboardSettings, Settings } from "../helpers";
import { FrameworkDetector } from "../helpers/FrameworkDetector";
import { Framework } from "../models";
import { BaseListener } from "./BaseListener";
export class SettingsListener extends BaseListener {
/**
* Process the messages for the dashboard views
* @param msg
*/
public static process(msg: { command: DashboardMessage, data: any }) {
super.process(msg);
switch(msg.command) {
case DashboardMessage.getData:
this.getSettings();
break;
case DashboardMessage.updateSetting:
this.update(msg.data);
break;
case DashboardMessage.setFramework:
this.setFramework(msg?.data);
break;
}
}
/**
* Update a setting from the dashboard
* @param data
*/
private static async update(data: { name: string, value: any }) {
if (data.name) {
await Settings.update(data.name, data.value);
this.getSettings();
}
}
/**
* Retrieve the settings for the dashboard
*/
public static async getSettings() {
const settings = await DashboardSettings.get();
this.sendMsg(DashboardCommand.settings, settings);
}
/**
* Set the current site-generator or framework + related settings
* @param frameworkId
*/
private static setFramework(frameworkId: string | null) {
Settings.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
if (frameworkId) {
const allFrameworks = FrameworkDetector.getAll();
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
if (framework) {
Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
} else {
Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
export * from './DashboardListener';
export * from './ExtensionListener';
export * from './MediaListener';
export * from './PagesListener';
export * from './SettingsListener';
+2 -1
View File
@@ -1,4 +1,5 @@
import * as React from 'react';
import { isValidFile } from '../../helpers/isValidFile';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
import { FileIcon } from './Icons/FileIcon';
@@ -19,7 +20,7 @@ const FileItem: React.FunctionComponent<IFileItemProps> = ({ name, path }: React
<li className={`file_list__items__item`}
onClick={openFile}>
{
(name.endsWith('.md') || name.endsWith('.markdown') || name.endsWith('.mdx')) ? (
(isValidFile(name)) ? (
<MarkdownIcon />
) : (
<FileIcon />