#272 - Media details panel

This commit is contained in:
Elio Struyf
2022-03-01 15:14:29 +01:00
parent 8d3b5619cd
commit b99c61a0ee
7 changed files with 342 additions and 108 deletions
@@ -23,7 +23,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
if (view === DashboardViewType.Grid) {
return (
<li className="relative">
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50`}
<button className={`group cursor-pointer flex flex-wrap items-start content-start h-full w-full bg-gray-50 dark:bg-vulcan-200 text-vulcan-500 dark:text-whisper-500 text-left overflow-hidden shadow-md dark:shadow-none hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50`}
onClick={openFile}>
<div className="relative h-36 w-full overflow-hidden border-b border-gray-100 dark:border-vulcan-100 dark:group-hover:border-vulcan-200">
{
@@ -0,0 +1,268 @@
import { Dialog, Transition } from '@headlessui/react';
import { PencilAltIcon, XIcon } from '@heroicons/react/outline';
import { format } from 'date-fns';
import { basename } from 'path';
import * as React from 'react';
import { Fragment, useMemo } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { MediaInfo } from '../../../models';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { useRecoilValue } from 'recoil';
import { PageSelector, SelectedMediaFolderSelector } from '../../state';
export interface IDetailsSlideOverProps {
imgSrc: string;
size: string;
dimensions: string;
folder: string;
media: MediaInfo;
showForm: boolean;
onEdit: () => void;
onEditClose: () => void;
onDismiss: () => void;
}
export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> = ({ imgSrc, size, dimensions, media, folder, showForm, onEdit, onEditClose, onDismiss }: React.PropsWithChildren<IDetailsSlideOverProps>) => {
const [ filename, setFilename ] = React.useState<string>(media.filename);
const [ caption, setCaption ] = React.useState<string | undefined>(media.caption);
const [ alt, setAlt ] = React.useState(media.alt);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const page = useRecoilValue(PageSelector);
const createdDate = useMemo(() => DateHelper.tryParse(media.ctime), [media]);
const modifiedDate = useMemo(() => DateHelper.tryParse(media.mtime), [media]);
const fileInfo = filename ? basename(filename).split('.') : null;
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
const onSubmitMetadata = () => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
caption,
alt,
folder: selectedFolder,
page
});
onEditClose();
};
React.useEffect(() => {
setAlt(media.alt);
setCaption(media.caption);
setFilename(media.filename);
}, [media]);
return (
<Transition.Root show={true} as={Fragment}>
<Dialog onClose={onDismiss} as={'div' as any} className="fixed inset-0 overflow-hidden z-50">
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="pointer-events-auto w-screen max-w-md">
<div className="flex h-full flex-col overflow-y-scroll bg-white dark:bg-vulcan-400 border-l border-whisper-900 dark:border-vulcan-50 py-6 shadow-xl">
<div className="px-4 sm:px-6">
<div className="flex items-start justify-between">
<Dialog.Title className="text-lg font-medium text-vulcan-300 dark:text-whisper-900">View details</Dialog.Title>
<div className="ml-3 flex h-7 items-center">
<button
type="button"
className="text-vulcan-300 dark:text-whisper-900 hover:text-vulcan-500 dark:hover:text-whisper-500 focus:outline-none"
onClick={onDismiss}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div className="relative mt-6 flex-1 px-4 sm:px-6">
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
<div>
<div className="block w-full aspect-w-10 aspect-h-7 overflow-hidden border-gray-200 dark:border-vulcan-200 border">
<img src={imgSrc} alt={media.filename} className="object-cover" />
</div>
<div className="mt-4 flex items-start justify-between">
<div>
<h2 className="text-lg font-medium text-vulcan-300 dark:text-whisper-500"><span className="sr-only">Details for </span>{media.filename}</h2>
<p className="text-sm font-medium text-vulcan-100 dark:text-whisper-900">{size}</p>
</div>
</div>
</div>
<div>
{
showForm && (
<>
<h3 className="text-base text-vulcan-300 dark:text-whisper-500">Update metadata</h3>
<p className="text-sm font-medium text-vulcan-100 dark:text-whisper-900">Please specify the metadata you want to set for the file.</p>
<div className="flex flex-col py-3 space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Filename
</label>
<div className="relative mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={name}
onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">
.{extension}
</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Caption
</label>
<div className="mt-1">
<textarea
rows={3}
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={caption || ''}
onChange={(e) => setCaption(e.target.value)}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Alt tag value
</label>
<div className="mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={alt || ''}
onChange={(e) => setAlt(e.target.value)}
/>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-30"
onClick={onSubmitMetadata}
disabled={!filename}
>
Save
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-200 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm"
onClick={onEditClose}
>
Cancel
</button>
</div>
</>
)
}
{
!showForm && (
<>
<h3 className="text-base text-vulcan-300 dark:text-whisper-500 flex items-center">
<span>Metadata</span>
<button onClick={onEdit}>
<PencilAltIcon className='w-4 h-4 ml-2' aria-hidden="true" />
<span className='sr-only'>Edit</span>
</button>
</h3>
<dl className="mt-2 border-t border-b border-gray-200 dark:border-vulcan-200 divide-y divide-gray-200 dark:divide-vulcan-200">
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Filename</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.filename}</dd>
</div>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Caption</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.caption || ""}</dd>
</div>
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Alternate text</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{media.alt || ""}</dd>
</div>
</dl>
</>
)
}
</div>
{
!showForm && (
<div>
<h3 className="text-base text-vulcan-300 dark:text-whisper-500">Information</h3>
<dl className="mt-2 border-t border-b border-gray-200 dark:border-vulcan-200 divide-y divide-gray-200 dark:divide-vulcan-200">
{
createdDate && (
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Created</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{format(createdDate, 'MMM dd, yyyy')}</dd>
</div>
)
}
{
modifiedDate && (
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Last modified</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{format(modifiedDate, 'MMM dd, yyyy')}</dd>
</div>
)
}
{
dimensions && (
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Dimensions</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{dimensions}</dd>
</div>
)
}
{
folder && (
<div className="py-3 flex justify-between text-sm font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Folder</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right break-all ml-6">{folder}</dd>
</div>
)
}
</dl>
</div>
)
}
</div>
</div>
</div>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
@@ -16,7 +16,7 @@ export const FolderItem: React.FunctionComponent<IFolderItemProps> = ({ folder,
const relFolderPath = wsFolder ? folder.replace(wsFolder, '') : folder;
return (
<li className={`group relative hover:shadow-xl dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
<li className={`group relative hover:bg-gray-200 dark:hover:bg-vulcan-100 text-gray-600 hover:text-gray-700 dark:text-whisper-900 dark:hover:text-whisper-800 p-4`}>
<button className={`w-full flex flex-row items-center h-full`} onClick={() => setSelectedFolder(folder)}>
<div>
<FolderIcon className={`h-12 w-12 mr-4`} />
+56 -98
View File
@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
@@ -10,10 +10,10 @@ import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { Alert } from '../Modals/Alert';
import { Metadata } from '../Modals/Metadata';
import { DetailsSlideOver } from './DetailsSlideOver';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
@@ -25,13 +25,13 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const [ , setLightbox ] = useRecoilState(LightboxAtom);
const [ showAlert, setShowAlert ] = React.useState(false);
const [ showForm, setShowForm ] = React.useState(false);
const [ showDetails, setShowDetails ] = React.useState(false);
const [ caption, setCaption ] = React.useState(media.caption);
const [ alt, setAlt ] = React.useState(media.alt);
const [ filename, setFilename ] = React.useState<string | null>(null);
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
const page = useRecoilValue(PageSelector);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
@@ -125,25 +125,45 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
});
};
const calculateSize = () => {
let sizeDetails = [];
if (media?.dimensions) {
if (media.dimensions.width && media.dimensions.height) {
sizeDetails.push(`${media.dimensions.width}x${media.dimensions.height}`);
}
const getDimensions = () => {
if (media.dimensions) {
return `${media.dimensions.width} x ${media.dimensions.height}`;
}
return "";
};
const getSize = () => {
if (media?.size) {
const size = media.size / (1024*1024);
if (size > 1) {
sizeDetails.push(`${size.toFixed(2)} MB`);
return `${size.toFixed(2)} MB`;
} else {
sizeDetails.push(`${(size * 1024).toFixed(2)} KB`);
return `${(size * 1024).toFixed(2)} KB`;
}
}
return sizeDetails.join(" — ");
return '';
};
const getMediaDetails = () => {
let sizeDetails = [];
const dimensions = getDimensions();
if (dimensions) {
sizeDetails.push(dimensions);
}
const size = getSize();
if (size) {
sizeDetails.push(size);
}
return sizeDetails.join(" - ");
};
const viewMediaDetails = () => {
setShowDetails(true);
};
const openLightbox = () => {
@@ -152,24 +172,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
const updateMetadata = () => {
setShowForm(true);
};
const submitMetadata = () => {
Messenger.send(DashboardMessage.updateMediaMetadata, {
file: media.fsPath,
filename,
caption,
alt,
folder: selectedFolder,
page
});
setShowForm(false);
// Reset the values
setAlt(media.alt);
setCaption(media.caption);
setFilename(getFileName());
setShowDetails(true);
};
const customScriptActions = () => {
@@ -200,13 +203,9 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
}
}, [media.fsPath]);
const fileInfo = filename ? basename(filename).split('.') : null;
const extension = fileInfo?.pop();
const name = fileInfo?.join('.');
return (
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 hover:shadow-xl dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50">
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50">
<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-300 dark:text-vulcan-200`} />
@@ -218,9 +217,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
<div className={`relative py-4 pl-4 pr-12`}>
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
<div className="flex items-center border border-transparent group-hover:bg-gray-50 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
<div className='hidden group-hover:inline-block h-5'>
<QuickAction
title='View media details'
onClick={viewMediaDetails}>
<EyeIcon className={`h-5 w-5`} aria-hidden="true" />
</QuickAction>
<QuickAction
title='Edit metadata'
onClick={updateMetadata}>
@@ -325,22 +330,18 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
)
}
{
media.alt && (
(!media.caption && media.alt) && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Alt:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{media.alt}</span>
</p>
)
}
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-2`}>Folder:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getFolder()}</span>
</p>
{
(media?.size || media?.dimensions) && (
<p className="mt-2 text-xs dark:text-whisper-900 font-medium pointer-events-none flex flex-col items-start">
<b className={`mr-1`}>Size:</b>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{calculateSize()}</span>
<span className={`block mt-1 dark:text-whisper-500 text-xs`}>{getMediaDetails()}</span>
</p>
)
}
@@ -348,60 +349,17 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
</li>
{
showForm && (
<Metadata
title={`Set metadata for: ${basename(parseWinPath(media.fsPath) || "")}`}
description={`Please specify the metadata you want to set for the file.`}
okBtnText={`Save`}
cancelBtnText={`Cancel`}
dismiss={() => setShowForm(false)}
trigger={submitMetadata}
isSaveDisabled={!filename}>
<div className="flex flex-col space-y-2">
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Filename
</label>
<div className="relative mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={name}
onChange={(e) => setFilename(`${e.target.value}.${extension}`)} />
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">
.{extension}
</span>
</div>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Caption
</label>
<div className="mt-1">
<textarea
rows={3}
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={caption || ''}
onChange={(e) => setCaption(e.target.value)}
/>
</div>
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700 dark:text-whisper-900">
Alt tag value
</label>
<div className="mt-1">
<input
className="py-1 px-2 sm:text-sm bg-white dark:bg-vulcan-300 border border-gray-300 dark:border-vulcan-100 text-vulcan-500 dark:text-whisper-500 placeholder-gray-400 dark:placeholder-whisper-800 focus:outline-none w-full"
value={alt || ''}
onChange={(e) => setAlt(e.target.value)}
/>
</div>
</div>
</div>
</Metadata>
showDetails && (
<DetailsSlideOver
imgSrc={media.vsPath || ""}
size={getSize()}
dimensions={getDimensions()}
folder={getFolder()}
media={media}
showForm={showForm}
onEdit={() => setShowForm(true)}
onEditClose={() => setShowForm(false)}
onDismiss={() => { setShowDetails(false); setShowForm(false); }} />
)
}