Refactoring

This commit is contained in:
Elio Struyf
2021-12-15 11:16:39 +01:00
parent 481d1f56e4
commit 050a513b48
19 changed files with 828 additions and 515 deletions
+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;
}
+31 -505
View File
@@ -12,9 +12,7 @@ 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';
@@ -29,14 +27,13 @@ import { SortingOption } from '../dashboardWebView/models';
import { Sorting } from '../helpers/Sorting';
import imageSize from 'image-size';
import { CustomScript } from '../helpers/CustomScript';
import { DashboardListener, 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 _viewData: DashboardData | undefined;
private static isDisposed: boolean = true;
public static get viewData(): DashboardData | undefined {
return Dashboard._viewData;
@@ -88,6 +85,20 @@ export class Dashboard {
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
@@ -133,65 +144,20 @@ export class Dashboard {
});
SettingsHelper.onConfigChange((global?: any) => {
Dashboard.getSettings();
SettingsListener.getSettings();
});
Dashboard.webview.webview.onDidReceiveMessage(async (msg) => {
DashboardListener.process(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;
@@ -217,10 +183,11 @@ export class Dashboard {
}
/**
* Reset media array
* Return the webview
* @returns The webview
*/
public static resetMedia() {
Dashboard.media = [];
public static getWebview() {
return Dashboard.webview?.webview;
}
public static switchFolder(folderPath: string) {
@@ -241,97 +208,6 @@ export class Dashboard {
Dashboard.webview?.webview.postMessage(msg);
}
}
/**
* 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
@@ -351,356 +227,6 @@ export class Dashboard {
}
}
/**
* 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
*/
@@ -0,0 +1,4 @@
export enum DashboardViewType {
Grid = 1,
List
}
+1
View File
@@ -1,3 +1,4 @@
export * from './DashboardViewType';
export * from './Page';
export * from './Settings';
export * from './SortingOption';
+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
});
+49
View File
@@ -0,0 +1,49 @@
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 } 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)
}
}
} as Settings
}
}
+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
*/
+27
View File
@@ -0,0 +1,27 @@
import { Extension } from './Extension';
import { OutputChannel, window } from 'vscode';
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): void {
if (!Logger.channel) {
Logger.getInstance();
}
Logger.channel?.appendLine(message);
}
}
+322
View File
@@ -0,0 +1,322 @@
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 } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, workspace } 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 });
}
}
}
/**
* 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);
}
}
+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`);
}
+26
View File
@@ -0,0 +1,26 @@
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 }) {
Logger.info(`Receiving message from webview: ${msg.command}`);
}
/**
* 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) {
Dashboard.postWebviewMessage({ command: DashboardCommand.viewData, data: Dashboard.viewData });
}
break;
case DashboardMessage.reload:
Dashboard.reload();
break;
case DashboardMessage.setPageViewType:
Extension.getInstance().setState(ExtensionState.PagesView, msg.data, "workspace");
break;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { commands, env } from "vscode";
import { SettingsListener } from ".";
import { COMMAND_NAME } from "../constants";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
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;
}
}
}
+78
View File
@@ -0,0 +1,78 @@
import { MediaHelpers } from './../helpers/MediaHelpers';
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { BaseListener } from "./BaseListener";
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { SortingOption } from '../dashboardWebView/models';
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:
}
}
/**
* Sends the media files to the dashboard
* @param page
* @param folder
* @param sorting
*/
private 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 {}
}
}
+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);
}
}
+52
View File
@@ -0,0 +1,52 @@
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 { DashboardCommand } from "../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../dashboardWebView/DashboardMessage";
import { DashboardViewType, SortingOption } from "../dashboardWebView/models";
import { DashboardSettings, Settings } from "../helpers";
import { Extension } from "../helpers/Extension";
import { FrameworkDetector } from "../helpers/FrameworkDetector";
import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } 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;
}
}
/**
* 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);
}
}
+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 />