From 626cc773f445c93f816581d4527590535cf76c9c Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 16 Sep 2021 11:46:37 +0200 Subject: [PATCH] #104 - First steps to preview image selection --- assets/media/styles.css | 17 ++++++ src/commands/Dashboard.ts | 25 +++++++-- src/extension.ts | 9 ++- src/models/DashboardData.ts | 4 ++ src/pagesView/DashboardCommand.ts | 1 + src/pagesView/DashboardMessage.ts | 2 + src/pagesView/components/Dashboard.tsx | 14 ++--- src/pagesView/components/Media/Item.tsx | 55 ++++++++++++++----- src/pagesView/components/Media/Media.tsx | 12 +++- src/pagesView/hooks/useMessages.tsx | 14 ++++- src/pagesView/state/atom/ViewDataAtom.ts | 7 +++ src/pagesView/state/atom/index.ts | 1 + .../state/selectors/ViewDataSelector.ts | 9 +++ src/pagesView/state/selectors/index.ts | 1 + src/viewpanel/CommandToCode.ts | 1 + .../components/Fields/PreviewImageField.tsx | 44 +++++++++++++++ src/viewpanel/components/Fields/Toggle.tsx | 22 ++++++-- src/viewpanel/components/Metadata.tsx | 25 ++++----- src/webview/ExplorerView.ts | 46 +++++++++++++--- 19 files changed, 249 insertions(+), 60 deletions(-) create mode 100644 src/models/DashboardData.ts create mode 100644 src/pagesView/state/atom/ViewDataAtom.ts create mode 100644 src/pagesView/state/selectors/ViewDataSelector.ts create mode 100644 src/viewpanel/components/Fields/PreviewImageField.tsx diff --git a/assets/media/styles.css b/assets/media/styles.css index 8c6ebccb..fc37e6aa 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -454,6 +454,23 @@ input:checked + .field__toggle__slider:before { width: auto; } +.metadata_field__preview_image img { + display: block; + margin: 0 auto; + max-height: 16rem; +} + +.metadata_field__preview_image__remove { + background-color: var(--vscode-inputValidation-errorBackground); + color: var(--vscode-inputValidation-errorForeground); +} + +.metadata_field__preview_image__remove:hover { + background-color: var(--vscode-inputValidation-errorBackground); + color: var(--vscode-inputValidation-errorForeground); + opacity: .9; +} + /* File list */ .file_list vscode-label { border-bottom: 1px solid var(--vscode-foreground); diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 6da79d52..97b9e2cf 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -17,10 +17,12 @@ import { Settings } from '../pagesView/models/Settings'; import { Extension } from '../helpers/Extension'; import { parseJSON } from 'date-fns'; import { ViewType } from '../pagesView/state'; -import { WebviewHelper } from '@estruyf/vscode'; +import { EditorHelper, WebviewHelper } from '@estruyf/vscode'; import { MediaInfo, MediaPaths } from './../models/MediaPaths'; import { decodeBase64Image } from '../helpers/decodeBase64Image'; import { DefaultFields } from '../constants'; +import { DashboardData } from '../models/DashboardData'; +import { ExplorerView } from '../webview/ExplorerView'; export class Dashboard { @@ -28,6 +30,7 @@ export class Dashboard { private static isDisposed: boolean = true; private static media: MediaInfo[] = []; private static timers: { [folder: string]: any } = {}; + private static viewData: DashboardData | undefined; /**  * Init the dashboard @@ -43,7 +46,9 @@ export class Dashboard { /** * Open or reveal the dashboard */ - public static async open() { + public static async open(data?: DashboardData) { + Dashboard.viewData = data; + if (Dashboard.isOpen) { Dashboard.reveal(); } else { @@ -93,8 +98,8 @@ export class Dashboard { Dashboard.webview.webview.html = Dashboard.getWebviewContent(Dashboard.webview.webview, extensionUri); Dashboard.webview.onDidChangeViewState(() => { - if (this.webview?.visible) { - console.log(`Dashboard opened`); + if (!this.webview?.visible) { + Dashboard.viewData = undefined; } }); @@ -108,6 +113,11 @@ export class Dashboard { 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(); @@ -151,6 +161,13 @@ export class Dashboard { case DashboardMessage.deleteMedia: Dashboard.deleteFile(msg?.data); break; + case DashboardMessage.insertPreviewImage: + if (msg.data?.file && msg.data?.image) { + await commands.executeCommand(`workbench.view.extension.frontmatter-explorer`); + await EditorHelper.showFile(msg.data.file); + ExplorerView.getInstance(extensionUri).updateMetadata({field: `preview`, value: msg.data.image}); + } + break; } }); } diff --git a/src/extension.ts b/src/extension.ts index 750118c2..c8e7d93b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,14 +5,13 @@ import { Folders } from './commands/Folders'; import { Preview } from './commands/Preview'; import { Project } from './commands/Project'; import { Template } from './commands/Template'; -import { COMMAND_NAME, EXTENSION_BETA_ID, EXTENSION_ID } from './constants/Extension'; +import { COMMAND_NAME } from './constants/Extension'; import { TaxonomyType } from './models'; import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider'; import { TagType } from './viewpanel/TagType'; import { ExplorerView } from './webview/ExplorerView'; import { Extension } from './helpers/Extension'; -import { basename } from 'path'; -import { Notifications } from './helpers/Notifications'; +import { DashboardData } from './models/DashboardData'; let frontMatterStatusBar: vscode.StatusBarItem; let statusDebouncer: { (fnc: any, time: number): void; }; @@ -36,8 +35,8 @@ export async function activate(context: vscode.ExtensionContext) { // Pages dashboard Dashboard.init(); - subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, () => { - Dashboard.open(); + subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => { + Dashboard.open(data); })); if (!extension.getVersion().usedVersion) { diff --git a/src/models/DashboardData.ts b/src/models/DashboardData.ts new file mode 100644 index 00000000..27318fea --- /dev/null +++ b/src/models/DashboardData.ts @@ -0,0 +1,4 @@ +export interface DashboardData { + type: "contents" | "media"; + data?: any; +} \ No newline at end of file diff --git a/src/pagesView/DashboardCommand.ts b/src/pagesView/DashboardCommand.ts index 927864df..314125fb 100644 --- a/src/pagesView/DashboardCommand.ts +++ b/src/pagesView/DashboardCommand.ts @@ -3,4 +3,5 @@ export enum DashboardCommand { pages = "pages", settings = "settings", media = "media", + viewData = "viewData", } \ No newline at end of file diff --git a/src/pagesView/DashboardMessage.ts b/src/pagesView/DashboardMessage.ts index b8b33daa..1afc8ac0 100644 --- a/src/pagesView/DashboardMessage.ts +++ b/src/pagesView/DashboardMessage.ts @@ -1,4 +1,5 @@ export enum DashboardMessage { + getViewType = 'getViewType', getData = 'getData', openFile = 'openFile', getTheme = 'getTheme', @@ -12,4 +13,5 @@ export enum DashboardMessage { refreshMedia = 'refreshMedia', uploadMedia = 'uploadMedia', deleteMedia = 'deleteMedia', + insertPreviewImage = 'insertPreviewImage', } \ No newline at end of file diff --git a/src/pagesView/components/Dashboard.tsx b/src/pagesView/components/Dashboard.tsx index 01331f5f..f366cdd5 100644 --- a/src/pagesView/components/Dashboard.tsx +++ b/src/pagesView/components/Dashboard.tsx @@ -5,7 +5,7 @@ import useDarkMode from '../../hooks/useDarkMode'; import usePages from '../hooks/usePages'; import { WelcomeScreen } from './WelcomeScreen'; import { useRecoilValue } from 'recoil'; -import { DashboardViewSelector } from '../state'; +import { DashboardViewSelector, ViewDataAtom } from '../state'; import { Contents } from './Contents/Contents'; import { Media } from './Media/Media'; @@ -31,13 +31,9 @@ export const Dashboard: React.FunctionComponent = ({showWelcome return ; } - if (view === "contents") { - return ( - - ); - } else { - return ( - - ); + if (view === 'media') { + return ; } + + return ; }; \ No newline at end of file diff --git a/src/pagesView/components/Media/Item.tsx b/src/pagesView/components/Media/Item.tsx index 90575696..f78d35fa 100644 --- a/src/pagesView/components/Media/Item.tsx +++ b/src/pagesView/components/Media/Item.tsx @@ -1,11 +1,11 @@ import { Messenger } from '@estruyf/vscode/dist/client'; -import { ClipboardCopyIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline'; +import { CheckCircleIcon, ClipboardCopyIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline'; import { basename, dirname } from 'path'; import * as React from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { MediaInfo } from '../../../models/MediaPaths'; import { DashboardMessage } from '../../DashboardMessage'; -import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector } from '../../state'; +import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state'; import { Alert } from '../Modals/Alert'; export interface IItemProps { @@ -17,6 +17,7 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi const selectedFolder = useRecoilValue(SelectedMediaFolderSelector); const [ , setLightbox ] = useRecoilState(LightboxAtom); const [ showAlert, setShowAlert ] = React.useState(false); + const viewData = useRecoilValue(ViewDataSelector); const parseWinPath = (path: string | undefined) => { return path?.split(`\\`).join(`/`); @@ -35,7 +36,7 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi return ""; }; - const copyToClipboard = () => { + const getRelPath = () => { let relPath: string | undefined = ""; if (settings?.wsFolder && media.fsPath) { relPath = media.fsPath.split(settings.wsFolder).pop(); @@ -44,10 +45,22 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi relPath = relPath.split(settings.staticFolder).pop(); } } + return relPath; + }; + const copyToClipboard = () => { + const relPath = getRelPath(); Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || ""); }; + const insertToArticle = () => { + const relPath = getRelPath(); + Messenger.send(DashboardMessage.insertPreviewImage, { + image: parseWinPath(relPath) || "", + file: viewData?.data?.filePath + }); + }; + const deleteMedia = () => { setShowAlert(true); }; @@ -87,18 +100,32 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
- - + onClick={insertToArticle}> + + Insert into your article + + ) : ( + <> + + + + ) + }

{basename(parseWinPath(media.fsPath) || "")} diff --git a/src/pagesView/components/Media/Media.tsx b/src/pagesView/components/Media/Media.tsx index e18a5eab..4069b10d 100644 --- a/src/pagesView/components/Media/Media.tsx +++ b/src/pagesView/components/Media/Media.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { MediaInfo, MediaPaths } from '../../../models/MediaPaths'; import { DashboardCommand } from '../../DashboardCommand'; -import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector } from '../../state'; +import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state'; import { Header } from '../Header'; import { Spinner } from '../Spinner'; import { SponsorMsg } from '../SponsorMsg'; @@ -27,6 +27,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi const [ , setTotal ] = useRecoilState(MediaTotalAtom); const [ , setFolders ] = useRecoilState(MediaFoldersAtom); const [ loading, setLoading ] = useRecoilState(LoadingAtom); + const viewData = useRecoilValue(ViewDataSelector); const onDrop = useCallback((acceptedFiles: File[]) => { acceptedFiles.forEach((file) => { @@ -73,6 +74,15 @@ export const Media: React.FunctionComponent = (props: React.PropsWi

+ + { + viewData?.data?.filePath && ( +
+

Select the image you want to use for your article.

+

You can also drag and drop images from your desktop and select that once uploaded.

+
+ ) + } { isDragActive && ( diff --git a/src/pagesView/hooks/useMessages.tsx b/src/pagesView/hooks/useMessages.tsx index e20bf0c1..2f7fcccb 100644 --- a/src/pagesView/hooks/useMessages.tsx +++ b/src/pagesView/hooks/useMessages.tsx @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { DashboardCommand } from '../DashboardCommand'; import { DashboardMessage } from '../DashboardMessage'; import { Page } from '../models/Page'; -import { SettingsAtom } from '../state'; +import { DashboardViewAtom, SettingsAtom, ViewDataAtom } from '../state'; import { Messenger } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; @@ -11,12 +11,20 @@ export default function useMessages() { const [loading, setLoading] = useState(false); const [pages, setPages] = useState([]); const [settings, setSettings] = useRecoilState(SettingsAtom); + const [viewData, setViewData] = useRecoilState(ViewDataAtom); + const [, setView] = useRecoilState(DashboardViewAtom); Messenger.listen((message: MessageEvent>) => { switch (message.data.command) { case DashboardCommand.loading: setLoading(message.data.data); break; + case DashboardCommand.viewData: + setViewData(message.data.data); + if (message.data.data?.type === 'media') { + setView('media'); + } + break; case DashboardCommand.settings: setSettings(message.data.data); break; @@ -27,8 +35,9 @@ export default function useMessages() { } }); - useEffect(() => { + useEffect(() => { setLoading(true); + Messenger.send(DashboardMessage.getViewType); Messenger.send(DashboardMessage.getTheme); Messenger.send(DashboardMessage.getData); }, ['']); @@ -36,6 +45,7 @@ export default function useMessages() { return { loading, pages, + viewData, settings }; } \ No newline at end of file diff --git a/src/pagesView/state/atom/ViewDataAtom.ts b/src/pagesView/state/atom/ViewDataAtom.ts new file mode 100644 index 00000000..c05bad0d --- /dev/null +++ b/src/pagesView/state/atom/ViewDataAtom.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil'; +import { DashboardData } from '../../../models/DashboardData'; + +export const ViewDataAtom = atom({ + key: 'ViewDataAtom', + default: undefined +}); \ No newline at end of file diff --git a/src/pagesView/state/atom/index.ts b/src/pagesView/state/atom/index.ts index a8d30209..2dcf5824 100644 --- a/src/pagesView/state/atom/index.ts +++ b/src/pagesView/state/atom/index.ts @@ -14,3 +14,4 @@ export * from './SortingAtom'; export * from './TabAtom'; export * from './TagAtom'; export * from './ViewAtom'; +export * from './ViewDataAtom'; diff --git a/src/pagesView/state/selectors/ViewDataSelector.ts b/src/pagesView/state/selectors/ViewDataSelector.ts new file mode 100644 index 00000000..b1596594 --- /dev/null +++ b/src/pagesView/state/selectors/ViewDataSelector.ts @@ -0,0 +1,9 @@ +import { selector } from 'recoil'; +import { ViewDataAtom } from '..'; + +export const ViewDataSelector = selector({ + key: 'ViewDataSelector', + get: ({get}) => { + return get(ViewDataAtom); + } +}); \ No newline at end of file diff --git a/src/pagesView/state/selectors/index.ts b/src/pagesView/state/selectors/index.ts index 3c52ec37..a694a579 100644 --- a/src/pagesView/state/selectors/index.ts +++ b/src/pagesView/state/selectors/index.ts @@ -11,4 +11,5 @@ export * from './SettingsSelector'; export * from './SortingSelector'; export * from './TabSelector'; export * from './TagSelector'; +export * from './ViewDataSelector'; export * from './ViewSelector'; diff --git a/src/viewpanel/CommandToCode.ts b/src/viewpanel/CommandToCode.ts index 80fbcb3a..46fb32f1 100644 --- a/src/viewpanel/CommandToCode.ts +++ b/src/viewpanel/CommandToCode.ts @@ -25,4 +25,5 @@ export enum CommandToCode { openInEditor = "open-in-editor", updateMetadata = "update-metadata", openDashboard = "open-dashboard", + selectImage = "select-image", } \ No newline at end of file diff --git a/src/viewpanel/components/Fields/PreviewImageField.tsx b/src/viewpanel/components/Fields/PreviewImageField.tsx new file mode 100644 index 00000000..3e1f54b2 --- /dev/null +++ b/src/viewpanel/components/Fields/PreviewImageField.tsx @@ -0,0 +1,44 @@ +import { PhotographIcon } from '@heroicons/react/outline'; +import * as React from 'react'; +import { MessageHelper } from '../../../helpers/MessageHelper'; +import { PanelSettings } from '../../../models'; +import { CommandToCode } from '../../CommandToCode'; +import { VsLabel } from '../VscodeComponents'; + +export interface IPreviewImageFieldProps { + label: string; + value: string | null; + filePath: string | null; + onChange: (value: string | null) => void; +} + +export const PreviewImageField: React.FunctionComponent = ({label, onChange, value, filePath}: React.PropsWithChildren) => { + + const selectImage = () => { + MessageHelper.sendMessage(CommandToCode.selectImage, { filePath }); + }; + + return ( +
+ +
+ {label} +
+
+ +
+ { + value ? ( +
+ + + +
+ ) : ( + + ) + } +
+
+ ); +}; \ No newline at end of file diff --git a/src/viewpanel/components/Fields/Toggle.tsx b/src/viewpanel/components/Fields/Toggle.tsx index 6b70a3ed..c572f27a 100644 --- a/src/viewpanel/components/Fields/Toggle.tsx +++ b/src/viewpanel/components/Fields/Toggle.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; +import { RocketIcon } from '../Icons/RocketIcon'; +import { VsLabel } from '../VscodeComponents'; export interface IToggleProps { + label: string; checked: boolean; onChanged: (checked: boolean) => void; } -export const Toggle: React.FunctionComponent = ({checked, onChanged}: React.PropsWithChildren) => { +export const Toggle: React.FunctionComponent = ({label, checked, onChanged}: React.PropsWithChildren) => { const [ isChecked, setIsChecked ] = React.useState(checked); const onChange = (event: React.ChangeEvent) => { @@ -20,9 +23,18 @@ export const Toggle: React.FunctionComponent = ({checked, onChange }, [ checked ]); return ( - +
+ +
+ {label} +
+
+ + + +
); }; \ No newline at end of file diff --git a/src/viewpanel/components/Metadata.tsx b/src/viewpanel/components/Metadata.tsx index 61cb2cea..9ebea453 100644 --- a/src/viewpanel/components/Metadata.tsx +++ b/src/viewpanel/components/Metadata.tsx @@ -10,13 +10,13 @@ import { RocketIcon } from './Icons/RocketIcon'; import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon'; import { TagIcon } from './Icons/TagIcon'; import { TagPicker } from './TagPicker'; -import { VsLabel } from './VscodeComponents'; import { parseJSON } from 'date-fns'; import { DateTimeField } from './Fields/DateTimeField'; import { TextField } from './Fields/TextField'; import { DefaultFields } from '../../constants'; import "react-datepicker/dist/react-datepicker.css"; +import { PreviewImageField } from './Fields/PreviewImageField'; export interface IMetadataProps { settings: PanelSettings | undefined; metadata: { [prop: string]: string[] | string | null }; @@ -74,7 +74,7 @@ export const Metadata: React.FunctionComponent = ({settings, met sendUpdate(settings?.date?.pubDate, date))} /> { @@ -82,22 +82,21 @@ export const Metadata: React.FunctionComponent = ({settings, met sendUpdate(settings?.date?.modDate, date))} /> ) } -
- -
- Published -
-
+ sendUpdate("draft", !checked)} /> - sendUpdate("draft", !checked)} /> -
+ sendUpdate('preview', value))} /> { (SETTING_COMMA_SEPARATED_FIELDS); + const staticFolder = config.get(SETTINGS_CONTENT_STATIC_FOLDERS); const articleDetails = this.getArticleDetails(); @@ -225,8 +236,28 @@ export class ExplorerView implements WebviewViewProvider, Disposable { } } } + + if (updatedMetadata.preview && wsFolder) { + const staticPath = join(wsFolder.fsPath, staticFolder || "", updatedMetadata.preview); + const contentFolderPath = filePath ? join(dirname(filePath), updatedMetadata.preview) : null; + + let previewUri = null; + if (existsSync(staticPath)) { + previewUri = Uri.file(staticPath); + } else if (contentFolderPath && existsSync(contentFolderPath)) { + previewUri = Uri.file(contentFolderPath); + } + + if (previewUri) { + const preview = this.panel?.webview.asWebviewUri(previewUri); + updatedMetadata.preview = preview?.toString() || ""; + } else { + updatedMetadata.preview = ""; + } + } this.postWebviewMessage({ command: Command.metadata, data: { + filePath, ...updatedMetadata }}); } @@ -253,7 +284,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable { /** * Update the metadata of the article */ - private updateMetadata({field, value}: { field: string, value: string }) { + public async updateMetadata({field, value}: { field: string, value: string }) { const config = SettingsHelper.getConfig(); const pubDate = config.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate; const modDate = config.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified; @@ -277,7 +308,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable { } else { article.data[field] = value; } - ArticleHelper.update(editor, article); + ArticleHelper.update(editor, article); + this.pushMetadata(article.data); } /** @@ -567,7 +599,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable { - + @@ -585,4 +617,4 @@ export class ExplorerView implements WebviewViewProvider, Disposable { `; } -} \ No newline at end of file +}