mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-11 11:22:53 +02:00
#98 - Drag and drop functionality added
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Generated
+34
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -10,4 +10,5 @@ export enum DashboardMessage {
|
||||
getMedia = 'getMedia',
|
||||
copyToClipboard = 'copyToClipboard',
|
||||
refreshMedia = 'refreshMedia',
|
||||
uploadMedia = 'uploadMedia',
|
||||
}
|
||||
@@ -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) || "")}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user