#98 - Drag and drop functionality added

This commit is contained in:
Elio Struyf
2021-09-10 13:04:49 +02:00
parent eca6e00bf2
commit 3f237386b2
8 changed files with 168 additions and 13 deletions
+1
View File
@@ -12,6 +12,7 @@
- [#89](https://github.com/estruyf/vscode-front-matter/issues/89): Clear filter, sorting, and grouping button added
- [#90](https://github.com/estruyf/vscode-front-matter/issues/90): Refactoring to use Recoil state management
- [#91](https://github.com/estruyf/vscode-front-matter/issues/91): Support image previews from content folders
- [#98](https://github.com/estruyf/vscode-front-matter/issues/98): Add drag and drop support for media upload in a folder
## [3.0.2] - 2021-08-31
+34
View File
@@ -872,6 +872,12 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true
},
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
"dev": true
},
"autoprefixer": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.2.tgz",
@@ -2326,6 +2332,23 @@
"integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==",
"dev": true
},
"file-selector": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
"integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
"dev": true,
"requires": {
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -4796,6 +4819,17 @@
"scheduler": "^0.20.1"
}
},
"react-dropzone": {
"version": "11.3.4",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.3.4.tgz",
"integrity": "sha512-B1nzNRZ4F1cnrfEC0T6KXeBN1mCPinu4JCoTrp7NjB+442KSPxqfDrw41QIA2kAwlYs1+wj/0BTedeM5hc2+xw==",
"dev": true,
"requires": {
"attr-accept": "^2.2.1",
"file-selector": "^0.2.2",
"prop-types": "^15.7.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+1
View File
@@ -425,6 +425,7 @@
"postcss-loader": "4.3.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-dropzone": "^11.3.4",
"recoil": "^0.4.1",
"rimraf": "^3.0.2",
"style-loader": "2.0.0",
+62 -6
View File
@@ -1,7 +1,7 @@
import { SETTINGS_CONTENT_STATIC_FOLDERS, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTINGS_DASHBOARD_OPENONSTART } from './../constants/settings';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { dirname, extname, join } from "path";
import { existsSync, statSync } from "fs";
import { existsSync, statSync, writeFileSync } from "fs";
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window, workspace, env } from "vscode";
import { SettingsHelper } from '../helpers';
import { TaxonomyType } from '../models';
@@ -19,12 +19,14 @@ import { parseJSON } from 'date-fns';
import { ViewType } from '../pagesView/state';
import { WebviewHelper } from '@estruyf/vscode';
import { MediaInfo, MediaPaths } from './../models/MediaPaths';
import { decodeBase64Image } from '../helpers/decodeBase64Image';
export class Dashboard {
private static webview: WebviewPanel | null = null;
private static isDisposed: boolean = true;
private static media: MediaInfo[] = [];
private static timers: { [folder: string]: any } = {};
/** 
* Init the dashboard
@@ -142,6 +144,9 @@ export class Dashboard {
Dashboard.media = [];
Dashboard.getMedia(0, msg?.data?.folder);
break;
case DashboardMessage.uploadMedia:
Dashboard.saveFile(msg?.data);
break;
}
});
}
@@ -223,7 +228,7 @@ export class Dashboard {
}
// Filter the media
let files = Dashboard.media;
let files: MediaInfo[] = Dashboard.media;
if (folder) {
files = files.filter(f => f.fsPath.includes(folder));
}
@@ -233,10 +238,16 @@ export class Dashboard {
// Get media set
files = files.slice(page * 16, ((page + 1) * 16));
files = files.map((file) => ({
...file,
stats: statSync(file.fsPath)
}));
files = files.map((file) => {
try {
return {
...file,
stats: statSync(file.fsPath)
};
} catch (e) {
return {...file, stats: undefined};
}
}).filter(f => f.stats !== undefined);
const folders = [...new Set(Dashboard.media.map((file) => {
let relFolderPath = wsFolder ? file.fsPath.substring(wsFolder.fsPath.length + 1) : file.fsPath;
@@ -347,6 +358,51 @@ export class Dashboard {
} 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 config = SettingsHelper.getConfig();
const staticFolder = config.get<string>(SETTINGS_CONTENT_STATIC_FOLDERS);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "", 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: ${staticFolder}/${folder}`);
const folderPath = `${staticFolder}/${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}`);
}
}
}
/**
* Post data to the dashboard
* @param msg
+13
View File
@@ -0,0 +1,13 @@
export const decodeBase64Image = (dataString: string) => {
const matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
let response: any = {};
if (matches?.length !== 3) {
return null;
}
response.type = matches[1];
response.data = Buffer.from(matches[2], 'base64');
return response;
}
+1
View File
@@ -10,4 +10,5 @@ export enum DashboardMessage {
getMedia = 'getMedia',
copyToClipboard = 'copyToClipboard',
refreshMedia = 'refreshMedia',
uploadMedia = 'uploadMedia',
}
+14 -4
View File
@@ -1,5 +1,5 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { ClipboardCopyIcon, PhotographIcon } from '@heroicons/react/outline';
import { ClipboardCopyIcon, PhotographIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
@@ -45,6 +45,10 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
Messenger.send(DashboardMessage.copyToClipboard, parseWinPath(relPath) || "");
};
const deleteMedia = () => {
};
const calculateSize = () => {
if (media?.stats?.size) {
const size = media.stats.size / (1024*1024);
@@ -62,22 +66,28 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
return (
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100">
<button className="relative bg-gray-100 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<button className="relative bg-gray-200 dark:bg-vulcan-300 block w-full aspect-w-10 aspect-h-7 overflow-hidden cursor-pointer h-48" onClick={openLightbox}>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<PhotographIcon className={`h-1/2 text-gray-50 dark:text-vulcan-200`} />
<PhotographIcon className={`h-1/2 text-gray-300 dark:text-vulcan-200`} />
</div>
<div className={`absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center`}>
<img src={media.vsPath} alt={basename(media.fsPath)} className="mx-auto object-cover" />
</div>
</button>
<div className={`relative py-4 pl-4 pr-10`}>
<div className={`absolute top-4 right-4`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-2`}>
<button title={`Copy media path`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={copyToClipboard}>
<ClipboardCopyIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Copy media path</span>
</button>
<button title={`Delete media`}
className={`hover:text-teal-900 focus:outline-none`}
onClick={deleteMedia}>
<TrashIcon className={`h-5 w-5`} />
<span className={`sr-only`}>Delete media</span>
</button>
</div>
<p className="text-sm dark:text-whisper-900 font-bold pointer-events-none flex items-center">
{basename(parseWinPath(media.fsPath) || "")}
+42 -3
View File
@@ -1,16 +1,20 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { UploadIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { MediaInfo, MediaPaths } from '../../../models/MediaPaths';
import { DashboardCommand } from '../../DashboardCommand';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SettingsSelector } from '../../state';
import { LoadingAtom, MediaFoldersAtom, MediaTotalAtom, SelectedMediaFolderSelector, SettingsSelector } from '../../state';
import { Header } from '../Header';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
import { Item } from './Item';
import { Lightbox } from './Lightbox';
import { List } from './List';
import { useDropzone } from 'react-dropzone'
import { useCallback } from 'react';
import { DashboardMessage } from '../../DashboardMessage';
export interface IMediaProps {}
@@ -18,11 +22,34 @@ export const LIMIT = 16;
export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWithChildren<IMediaProps>) => {
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const [ media, setMedia ] = React.useState<MediaInfo[]>([]);
const [ , setTotal ] = useRecoilState(MediaTotalAtom);
const [ , setFolders ] = useRecoilState(MediaFoldersAtom);
const [ loading, setLoading ] = useRecoilState(LoadingAtom);
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = () => {
const contents = reader.result;
Messenger.send(DashboardMessage.uploadMedia, {
fileName: file.name,
contents,
folder: selectedFolder
});
};
reader.readAsDataURL(file)
});
}, [selectedFolder]);
const {getRootProps, isDragActive} = useDropzone({
onDrop,
accept: 'image/*'
});
const messageListener = (message: MessageEvent<EventData<MediaPaths>>) => {
if (message.data.command === DashboardCommand.media) {
setLoading(false);
@@ -30,7 +57,7 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
setTotal(message.data.data.total);
setFolders(message.data.data.folders);
}
}
};
React.useEffect(() => {
Messenger.listen<MediaPaths>(messageListener);
@@ -45,7 +72,19 @@ export const Media: React.FunctionComponent<IMediaProps> = (props: React.PropsWi
<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">
<div className="w-full flex-grow max-w-7xl mx-auto py-6 px-4" {...getRootProps()}>
{
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>
)
}
<List>
{
media.map((file) => (